pygameで風船割りゲーム

##はじめに
[前回の投稿](https://qiita.com/TN0321/items/d0ba918eefa5d9c7e587)では、tkinterを使ったターン制の三目並べを作成したので、今回はpygameを使った常に画面が更新されるものを作ってみました。
タイトルで風船割りゲームと書きましたがそこまでのゲーム性はありません。笑
ただ、今後ゲームを作る際に使えそう？な処理は学べたかなぁ。。。と思います。
例によって、欲しい機能を寄せ集めたキメラティックコードなので読みやすさは今後の課題ですね。


|今回の完成品|
|:---:|
|<img width="800" alt="balloon_game.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/497538/677f0e9b-a7b0-a298-b325-c93957f400db.gif">|
一番最後に今回のコード全体像を載せてあります。

##表示画面作成
まずは基盤となる画面作成からですね。

```python
import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Game")
```
なにはともあれpygameをimport。
pygame.init()で、pygameのモジュールを初期化してあげます。開幕に書く場合は、お決まりの呪文レベルの認識でいいかもしれません。大量のモジュールを使用する場合は、使ってないモジュールを初期化してあげるといいとかなんとか。そのうち処理の途中で書く日が来るかも？

pygame.display.set_modeが画面サイズで横幅、縦幅。
set_captionはタイトルバーに表示される名前です。

##画面更新処理

```python
import time

FPSCLOCK = pygame.time.Clock()
FPS = 15

class CanonGame():
    def __init__(self):

        while True:
            pygame.display.update()
            FPSCLOCK.tick(FPS)

def main():
    CanonGame()

if __name__ == '__main__':
    main()
```

主役。
pygame.display.update()で画面の描画を反映した後に、pygame.time.Clock.tick(フレーム数)で1秒間に(フレーム数)を超えない速度で処理が実行されます。フレーム数を上げれば、滑らかな動きを実装することができる反面、処理が重くなってしまいます。
それと、基本的に更新し続けるものはwhile True:の範囲中に入れてあげてください。

##キーイベント取得

```python
from pygame.locals import *

pygame.key.set_repeat(5, 5)

class CanonGame():
        while True:
            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == KEYDOWN:
                    if event.key == K_SPACE:
                        self.bullet_shot = True
                    elif event.key == K_UP and self.cannon_radian < 89:
                        self.cannon_radian += 1
                    elif event.key == K_DOWN and self.cannon_radian > 0:
                        self.cannon_radian -= 1
```
ゲームとなればやはり操作したいですよね？
event.get()で行われた操作の種類を取得します。
event.typeがQUIT(画面右上の×ボタン)ならすべてを終了させます。
KEYDOWN(キーボード押下)ならその後にevent.keyを取得して押されたキーごとに処理を割り振ります。
今回は、スペースキーで球を発射。上下キーで向き調節にしました。
key.set_repeatを設定することによりキーを押しっぱなしでも反応してくれるようになります。

##画像表示、回転

```python

class CanonGame():
    def __init__(self):
        bullet_image = pygame.image.load("bullet.png")
        self.cannon_image = pygame.image.load("cannon.png")

            screen.fill((255, 255, 255))
            screen.blit(bullet_image, (self.bullet_x, self.bullet_y))

            rotate_cannon = pygame.transform.rotate(self.cannon_image, self.cannon_radian - 45)
            cannon_rect = rotate_cannon.get_rect()
            cannon_rect.center = (84, 536)
            screen.blit(rotate_cannon, cannon_rect)
```
pygame.image.load（”画像名”）で画像を読み込みます。画像の保存場所は実行ファイルを保存している場所と同じ場所で。
screen.fill(Red,Green,Blue)は背景色です。今回は背景画像を用意していないので、毎回背景を塗り替えて、直前の画像たちを消しています。これがないと前の処理たちがそのまま残ってしまいます。
screen.blit(画像,(x, y))：画像の配置で、気を付けないといけないのが、表示画面の左上がx:0、y:0になります。
画面の右下に向かうにつれてxとyの値が大きくなります。

##諸々の挙動
ゲーム内の挙動ですが、いくつかピックアップして解説します。

```python
from random import randint

self.balloon_x = randint(500, 700)
```
randint(最小値, 最大値)。これにより風船が出現するx座標をランダムにしています。intなので設定値内の整数からランダムで選ばれます。

```python
import sys

sysfont = pygame.font.SysFont(None, 36)

        while True:
            score_image = sysfont.render("score : {}".format(self.score), True, (0, 0, 255))
            screen.blit(score_image, (10, 20))

```
文字の出力。
pygame.font.Sysfont(name, size, bold, italic)と4つのパラメータを変えることが出来ます。
sysfont.renderの｛ ｝の中にはformat()が入ります。

```python
    def set_balloon(self):
        now_time = time.time()
        if self.is_collision:
            if now_time - self.balloon_break > 1:
                self.is_collision = False
        if self.is_collision is False:
            if self.is_balloon:
                self.balloon_y = self.balloon_y - 12
                self.balloon_alive = time.time()
                if self.balloon_alive - self.balloon_create > 4:
                    self.is_balloon = False
                else:
                    self.collision_check()
            else:
                self.balloon_image = pygame.image.load("balloon-red.png")
                self.balloon_x = randint(500, 700)
                self.balloon_y = 600
                self.is_balloon = True
                self.balloon_create = time.time()
```
あと、ごにょごにょ書いているset_balloon関数なんですが、ここではtimeモジュールを利用しています。
風船が出現したときに、balloon_createとして現在時刻を取得し、その後、balloon_aliveという時間を取得し続けて、差が4秒以上になれば再び風船を出現させるようにしています。
ついでにnow_timeというのも計測して、風船が割れたら1秒後に風船を出現させるようにしています。

n秒後に処理させるって調べるとsleepが出てきますが、あれを使うと処理全体が止まるので注意してください。

```python
import math

    def bullet(self):
        gravity = 9.8
        if self.bullet_shot:
            bullet_speed_x = self.bullet_speed * math.cos(math.radians(self.cannon_radian))
            bullet_speed_y = (self.bullet_speed * math.sin(math.radians(self.cannon_radian)))
            self.bullet_x = self.bullet_x + bullet_speed_x
            self.bullet_y = self.bullet_y - bullet_speed_y + gravity * self.time
            self.time += 0.2

            if self.bullet_x > 800 or self.bullet_y > 600:
                self.bullet_shot = False
                self.set_bullet()
```
発射した球の挙動は、発射角にサインコサインでそれっぽい動きになるように。
ちゃんとした物理式を使うか迷ったんですが、それっぽい動きになったからヨシっ！

球が画面外に出るまで次の球は発射させません。
※この記述の場合、画面内に球がある状態で発射すると、飛行中の球が消えてしまいます。
　なので、連射したい場合はそれぞれを別のものとして扱う必要があります。

```python
    def collision_check(self):
        distance_y = ((self.balloon_y + 15) - (self.bullet_y + 16))**2
        distance_x = ((self.balloon_x + 20) - (self.bullet_x + 16))**2
        distance = (distance_x + distance_y)**(1/2)
        if distance < 31:
            self.pang_image = pygame.image.load("pang.png")
            self.pang_rect = self.pang_image.get_rect()
            self.pang_rect.center = (self.balloon_x + 20, self.balloon_y + 15)
            self.is_collision = True
            self.is_balloon = False
            self.total_score(100)
            self.balloon_break = time.time()
```
衝突判定。
今回は球と風船のだいたいの中心からの絶対値で計算しています。久々すぎて三平方の定理ググりました。
ヒットボックスが円だと楽でいい！（四角の場合は、x軸同士とy軸同士の差を絶対値を出してandでうんぬんかんぬん）
衝突すると100点入ります。

##終わりに
色々な挙動を覚えるにはやはり触れてみるのが一番ですね。
今回の要素を組み合わせれば、簡単なゲームなら大体できそうな気がしなくもないです。
あと、イラスト描けるようになりたい・・・


##コード全体像

```python
import math
import sys
import pygame
from pygame.locals import *
from random import randint
import time

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Game")
sysfont = pygame.font.SysFont(None, 36)
pygame.key.set_repeat(5, 5)
FPSCLOCK = pygame.time.Clock()
FPS = 15


class CanonGame():
    def __init__(self):
        bullet_image = pygame.image.load("bullet.png")
        self.set_cannon()
        self.set_bullet()
        self.bullet_shot = False
        self.is_balloon = False
        self.is_collision = False
        self.score = 0

        while True:
            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == KEYDOWN:
                    if event.key == K_SPACE:
                        self.bullet_shot = True
                    elif event.key == K_UP and self.cannon_radian < 89:
                        self.cannon_radian += 1
                    elif event.key == K_DOWN and self.cannon_radian > 0:
                        self.cannon_radian -= 1

            self.set_balloon()
            self.bullet()

            screen.fill((255, 255, 255))
            screen.blit(bullet_image, (self.bullet_x, self.bullet_y))
            if not self.is_collision:
                screen.blit(self.balloon_image, (self.balloon_x, self.balloon_y))
            elif self.is_collision:
                screen.blit(self.pang_image, self.pang_rect)
            rotate_cannon = pygame.transform.rotate(self.cannon_image, self.cannon_radian - 45)
            cannon_rect = rotate_cannon.get_rect()
            cannon_rect.center = (84, 536)
            screen.blit(rotate_cannon, cannon_rect)
            score_image = sysfont.render("score : {}".format(self.score), True, (0, 0, 255))
            screen.blit(score_image, (10, 20))

            pygame.display.update()
            FPSCLOCK.tick(FPS)

    def set_bullet(self):
        self.bullet_x = 68
        self.bullet_y = 520
        self.bullet_speed = 50
        self.time = 0

    def set_cannon(self):
        self.cannon_image = pygame.image.load("cannon.png")
        self.cannon_radian = 45

    def set_balloon(self):
        now_time = time.time()
        if self.is_collision:
            if now_time - self.balloon_break > 1:
                self.is_collision = False
        if self.is_collision is False:
            if self.is_balloon:
                self.balloon_y = self.balloon_y - 12
                self.balloon_alive = time.time()
                if self.balloon_alive - self.balloon_create > 4:
                    self.is_balloon = False
                else:
                    self.collision_check()
            else:
                self.balloon_image = pygame.image.load("balloon-red.png")
                self.balloon_x = randint(500, 700)
                self.balloon_y = 600
                self.is_balloon = True
                self.balloon_create = time.time()

    def bullet(self):
        gravity = 9.8
        if self.bullet_shot:
            bullet_speed_x = self.bullet_speed * math.cos(math.radians(self.cannon_radian))
            bullet_speed_y = (self.bullet_speed * math.sin(math.radians(self.cannon_radian)))
            self.bullet_x = self.bullet_x + bullet_speed_x
            self.bullet_y = self.bullet_y - bullet_speed_y + gravity * self.time
            self.time += 0.2

            if self.bullet_x > 800 or self.bullet_y > 600:
                self.bullet_shot = False
                self.set_bullet()

    def collision_check(self):
        distance_y = ((self.balloon_y + 15) - (self.bullet_y + 16))**2
        distance_x = ((self.balloon_x + 20) - (self.bullet_x + 16))**2
        distance = (distance_x + distance_y)**(1/2)
        if distance < 31:
            self.pang_image = pygame.image.load("pang.png")
            self.pang_rect = self.pang_image.get_rect()
            self.pang_rect.center = (self.balloon_x + 20, self.balloon_y + 15)
            self.is_collision = True
            self.is_balloon = False
            self.total_score(100)
            self.balloon_break = time.time()

    def total_score(self, score):
        self.score = self.score + score


def main():
    CanonGame()


if __name__ == '__main__':
    main()
```