【Python】Tkinterを使ってシューティングゲーム制作

# はじめに
Pythonの標準GUIフレームワークであるTkinterを使用してインベーダーゲームのようなシューティングゲームを作ってみました。作っていて痛感したのですがTkinterはとにかく参考資料が少なすぎます...。なので、これからTkinterを使おうとしている人たちの参考に少しでもなれればとても嬉しいです。

# 追記 (2020/2/12)
今更ですがGitHubで[レポジトリ](https://github.com/ishikawa08/invader_game
)を公開しました。よろしければご確認ください。

# 環境
Windows10
Python3.7 (Anaconda)

# ゲーム画面
<img width="450" alt="[gamedisplay.PNG]" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363619/6c16cb38-b0b9-e900-a0bc-4b14af652de0.png">


# 画像
自機と敵機の画像を用意しました。それぞれダウンロードして`cannon.jpeg`、`crab.jpeg`という名前で保存し、pyファイルと同じフォルダに入れておいてください。

![cannon.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363619/1ca88a07-3f1e-09f5-db44-52415fc1aa8b.jpeg)

![crab.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363619/e9d3c2c8-2aba-6a66-fb5a-29c768cb21ab.jpeg)


#ソースコード
1行ずつ解説したいのですが時間の都合上諦めました...コメントを見てもらえれば流れは分かると思います。


```python
import tkinter as tk
import random
from PIL import Image, ImageTk

WINDOW_HEIGHT = 600  # ウィンドウの高さ
WINDOW_WIDTH = 600   # ウィンドウの幅

CANNON_Y = 550       # 自機のy座標

ENEMY_SPACE_X = 100  # 敵の間隔(x座標)
ENEMY_SPACE_Y = 60   # 敵の間隔(y座標)
ENEMY_MOVE_SPACE_X = 20  # 敵の移動間隔(x座標)
ENEMY_MOVE_SPEED = 2000  # 敵の移動スピード(2000 ms)
NUMBER_OF_ENEMY = 18     # 敵の数
ENEMY_SHOOT_INTERVAL = 200  # 敵がランダムに弾を打ってくる間隔

COLLISION_DETECTION = 300  # 当たり判定

BULLET_HEIGHT = 10  # 弾の縦幅
BULLET_WIDTH = 2    # 弾の横幅
BULLET_SPEED = 10   # 弾のスピード(10 ms)

TEXT_GOOD_SIZE = 10             # goodのサイズ
TEXT_CONGRATULATIONS_SIZE = 50  # congratularionsのサイズ
TEXT_GAMECLEAR_SIZE = 60        # gameclearのサイズ
TEXT_GAMEOVER_SIZE = 90         # gameoverのサイズ


class Cannon:  # 自機

    def __init__(self, x, y=CANNON_Y):
        self.x = x
        self.y = y
        self.draw()
        self.bind()

    def draw(self):
        self.id = cv.create_image(
            self.x, self.y, image=cannon_tkimg, tag="cannon")

    def bind(self):
        cv.tag_bind(self.id, "<ButtonPress-3>", self.pressed)
        cv.tag_bind(self.id, "<Button1-Motion>", self.dragged)

    def pressed(self, event):
        mybullet = MyBullet(event.x, self.y)
        mybullet.draw()
        mybullet.shoot()

    def dragged(self, event):
        dx = event.x - self.x
        self.x, self.y = cv.coords(self.id)
        cv.coords(self.id, self.x+dx, self.y)
        self.x = event.x

    def destroy(self):
        cv.delete(self.id)


class MyBullet:  # 自分の弾

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def draw(self):
        self.id = cv.create_rectangle(
            self.x-BULLET_WIDTH, self.y+BULLET_HEIGHT, self.x+BULLET_WIDTH, self.y-BULLET_HEIGHT, fill="blue")

    def shoot(self):
        if self.y >= 0:
            cv.move(self.id, 0, -BULLET_HEIGHT)
            self.y -= BULLET_HEIGHT
            self.defeat()
            root.after(BULLET_SPEED, self.shoot)

    def defeat(self):
        for enemy in enemies:
            if ((self.x-enemy.x)**2+(self.y-enemy.y)**2) < COLLISION_DETECTION:
                enemy.exist = False
                enemy.destroy()
                cv.create_text(enemy.x, enemy.y, text="good!", fill="cyan", font=(
                    "System", TEXT_GOOD_SIZE), tag="good")

    def destroy(self):
        cv.delete(self.id)


class Enemy:  # 敵

    def __init__(self, x, y):
        self.x = x % WINDOW_WIDTH
        self.y = y+x//WINDOW_WIDTH*ENEMY_SPACE_Y
        self.exist = True
        self.draw()
        self.move()

    def draw(self):
        self.id = cv.create_image(
            self.x, self.y, image=crab_tkimg, tag="enemy")

    def enemy_shoot(self):
        if self.exist:
            enemybullet = EnemyBullet(self.x, self.y)
            enemybullet.draw()
            enemybullet.shoot()

    def move(self):
        if self.exist:
            if self.x > WINDOW_WIDTH:
                self.x -= ENEMY_MOVE_SPACE_X
                self.y += ENEMY_SPACE_Y
            elif self.x < 0:
                self.x += ENEMY_MOVE_SPACE_X
                self.y += ENEMY_SPACE_Y
            if self.y % (ENEMY_SPACE_Y*2) == ENEMY_SPACE_Y:
                self.x += ENEMY_MOVE_SPACE_X
            else:
                self.x -= ENEMY_MOVE_SPACE_X
            cv.coords(self.id, self.x, self.y)
            root.after(ENEMY_MOVE_SPEED, self.move)

    def destroy(self):
        cv.delete(self.id)


class EnemyBullet:  # 敵の弾

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def draw(self):
        self.id = cv.create_rectangle(
            self.x-BULLET_WIDTH, self.y+BULLET_HEIGHT, self.x+BULLET_WIDTH, self.y-BULLET_HEIGHT, fill="red")

    def shoot(self):
        if self.y <= WINDOW_HEIGHT:
            cv.move(self.id, 0, BULLET_HEIGHT)
            self.y += BULLET_HEIGHT
            self.collision()
            root.after(BULLET_SPEED, self.shoot)

    def collision(self):
        if ((self.x-cannon.x)**2+(self.y-cannon.y)**2) < COLLISION_DETECTION:
            gameover()

    def destroy(self):
        cv.delete(self.id)


def gameclear():  # ゲームクリア判定
    winflag = 0
    for enemy in enemies:
        if enemy.exist == False:
            winflag += 1
    if winflag == NUMBER_OF_ENEMY:
        cv.delete("good")
        cv.create_text(WINDOW_WIDTH//2, WINDOW_HEIGHT//2-80, text="Congratulations!",
                       fill="lime", font=("System", TEXT_CONGRATULATIONS_SIZE))
        cv.create_text(WINDOW_WIDTH//2, WINDOW_HEIGHT//2+20, text="GAME CLEAR!",
                       fill="lime", font=("System", TEXT_GAMECLEAR_SIZE))
    root.after(1000, gameclear)


def gameover():  # ゲームオーバー判定
    cv.delete("cannon", "good")
    cv.create_text(WINDOW_WIDTH//2, WINDOW_HEIGHT//2, text="GAME OVER",
                   fill="red", font=("System", TEXT_GAMEOVER_SIZE))


def enemy_randomshoot():  # ランダムに敵の弾が発射
    enemy = random.choice(enemies)
    enemy.enemy_shoot()
    root.after(ENEMY_SHOOT_INTERVAL, enemy_randomshoot)


if __name__ == "__main__":
    # 初期描画
    root = tk.Tk()
    root.title("invader")
    cv = tk.Canvas(root, width=WINDOW_WIDTH, height=WINDOW_HEIGHT, bg="black")
    cv.pack()

    # 画像の読み込み
    cannon_img = Image.open("cannon.jpeg")
    cannon_tkimg = ImageTk.PhotoImage(cannon_img)
    crab_img = Image.open("crab.jpeg")
    crab_tkimg = ImageTk.PhotoImage(crab_img)

    # メニューバー
    menubar = tk.Menu(root)
    root.configure(menu=menubar)
    menubar.add_command(label="QUIT", underline=0, command=root.quit)

    # インスタンス生成
    cannon = Cannon(WINDOW_WIDTH//2, CANNON_Y)
    enemies = []
    for i in range(NUMBER_OF_ENEMY):
        enemy_i = Enemy(i*ENEMY_SPACE_X+50, ENEMY_SPACE_Y)
        enemies.append(enemy_i)

    enemy_randomshoot()

    gameclear()

    root.mainloop()
```



# さいごに
ご覧いただきありがとうございました。Python3系でゲーム制作をしたい場合はぜひTkinterを！