ゲームで一儲けしたいので、手始めにpyxelで作ってみた【未完】

# まえがき
ARISE analyticsの北サブ(@Toitu_Prince)です。
本記事は [ARISE analytics Advent Calendar 2022](https://qiita.com/advent-calendar/2022/arise-analytics) の18日目の記事になります。

# 作ったもの
(2022/12/15現在)
<img width="500" alt="result.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/c3417d85-d952-4c9e-a58d-348ff43de797.gif">

# あらすじ
生きてきた中で皆さん一度は「ゲームを作って売って一儲けしたい・・・！」って思ったことありますよね？
ただ、ゲームを作るのってハードルが高いと思ってる方は少なくないと思います。僕もその一人です。ゲームはやるほうが楽しい。
パンピーがゲームを作る時の選択肢としては、UnityとかC＃が思い浮かぶかなと思うのですが、Unityは３Dのモデルをこねこねした上にコーディングしなきゃいけないのが難しそうだし、C#はそもそも言語覚えるのが辛いし・・・となりがちだと思います。
僕はPythonしか書けないけど、ゲーム作ってみたいよ・・・と思っているそこのあなた！なんとピッタリなライブラリがこの世にはあるのです！！
さぁ！皆さんもゲームで一山当てて働かずに銀行にお金が振り込まれる未来に向けて、一歩を踏み出しましょう！

# pyxelとは？

前置きが長くなってしまいましたが、そのような素敵な夢を叶えられるライブラリが**pyxel**になります。

https://github.com/kitao/pyxel

pyxelとは、Pythonを用いてドット絵スタイルのゲームを作ることが出来るエンジンになります。
Pythonでゲームを作れるだけではなく、ドット絵を描くためのツールや、BGMやSEを作るためのツールも内包されており、至れり尽くせりなエンジンとなっております。
作ったゲームを公開することも簡単に行えるらしいのですが、こちらを執筆している12月15日ですとそこまで辿り着いていないため、今後の記事に期待してください。

ちなみに導入方法は簡単です。
```sh
pip install pyxel
```
ただ、家のWindowsでWSL2とubuntuで作った環境にインストールしてみたのですが、ゲーム画面を出すことが出来なかったので、Macでの開発をオススメいたします。
Windowでも出来るよ！という有識者の方がいらっしゃったらコメントで教えていただけたら嬉しいです・・・！

また、以下のコマンドを実行することで、サンプルゲームもコピーできるため、しておくことをオススメします。コードめちゃめちゃ参考になります。
```sh
pyxel copy_examples
```

# 作ってみよう

## どんなゲームを作ろうか？

さて、一通り作る環境が揃ったところで、どのようなゲームを作るかという問題についてですが・・・
作りたいゲームがあればそれを作ればヨシ！思いつかなければ、最近やっているゲームを簡単にしたゲームを作れば良いかなと思っております。
そこで、最近ドハマリ中のゲームである「NIKKE」っぽいゲームを作って見ようかなと考えました。
「NIKKE」は2022年11月にリリースされたばかりのスマホゲームになります。ジャンルとしてはTPSかな？
このゲームの特徴としては、ゲーム画面の構図が挙げられます。キャラが敵と戦っている姿を後ろから眺める、という構図は今までなかなか無かった構図じゃないかな、と思います。
ちなみに、手動で操作しているときはかなり忙しいので、キャラクターを見ている余裕とか無いです。ホントに。
キャラクターも可愛くて、ストーリーも面白いので、興味がある方はぜひ検索してみてください！
多分会社の人に怒られるので画像は貼りません。
- - - 
脇道にそれてしまいましたが、作りたいゲームの初版イメージは以下のような感じです。
<img width="500" alt="いめーじず" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/af4db67a-eac5-a189-f0f8-d135457703a0.png">

手前にプレイヤーがいて、画面の上の方に敵がいて、画面の中だとマウスが照準になり、クリックしたら弾が発射される、みたいなイメージです。
ある程度イメージが決まったところで、実際にゲームを作ってみましょう！細かいところは作りながら決めればOK!

## 実装編
### とりあえずそれっぽく画面に置いてみる

pyxelは基本的に3つの関数で構成することが出来ます（多分）。
- `__init__`
initでは、ゲーム画面の確保、グラフィックやサウンドをまとめたアセットのロード、ゲームの実行を記載します。

```python
    def __init__(self) -> None:
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT)
        pyxel.load("./assets/my_resource.pyxres")
        pyxel.run(self.update, self.draw)
```

- `update`
updateは、システム部分の処理を記載する感じです。下のコードだと、Q押したらゲーム終了、ということが書いてあります。
```python
    def update(self) -> None:
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()
```
- draw
drawはその名の通り、色々なオブジェクトを描画する箇所になります。
```python
    def draw(self) -> None:
        pyxel.cls(col=pyxel.COLOR_GRAY)  # 背景
        pyxel.rect(0, 00, 50, 50, col=pyxel.COLOR_WHITE)  # 白い四角を描写する
```

この3つの関数を組み合わせて、ゲームを作っていくことになります。

```main.py
import pyxel

SCREEN_WIDTH = 256
SCREEN_HEIGHT = 256


class Player:
    def __init__(self, y_ground: int) -> None:
        self.x = (pyxel.width / 2) - 25
        self.y = y_ground - 50

    def draw(self):
        pyxel.rect(self.x, self.y, 50, 50, 9)


class Enemy:
    def __init__(self):
        self.r = 5
        self.x = pyxel.rndf(self.r, SCREEN_WIDTH - self.r)
        self.y = pyxel.rndf(self.r, SCREEN_HEIGHT - self.r)

    def draw(self):
        pyxel.circ(self.x, self.y, self.r, pyxel.COLOR_PURPLE)


class App:
    def __init__(self) -> None:
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT)
        pyxel.load("./assets/my_resource.pyxres")

        self.player = Player(pyxel.height)
        self.enemy = Enemy()

        pyxel.run(self.update, self.draw)

    def update(self) -> None:
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

    def draw(self) -> None:
        pyxel.cls(col=pyxel.COLOR_GRAY)
        self.player.draw()
        self.enemy.draw()

        pyxel.blt(pyxel.mouse_x - 7, pyxel.mouse_y - 7, 0, 0, 0, 16, 16, 0)  # レティクルを描写


App()

```

#### 作ったもの
作成したファイルを実行すると、以下のようにゲーム画面が表示されます。
```sh
python main.py
```
<img width="500" alt="はじめの一歩" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/8ed88657-36e2-e5d7-7621-d642da2de203.png">

人類が一番最初に作ったNIKKEみたいですね。

ちなみにレティクルは、pyxelのグラフィック作成機能を使って描きました。図形だけだと寂しかったので。
<img width="500" alt="1ドット1魂" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/38e7ea05-888e-d941-ecd4-ec2d13b7e916.png">

### 打ったら倒せるようにしてみる

さて、このままですと只々図形を眺めるだけのものになってしまいますので、とりあえず敵を打ったら（クリック）倒せるようにしてみます。
まず、敵に打たれたかどうかの判定を実装したいと思います。

```python
class Enemy:
    def __init__(self):
        self.is_shooted = False 

    def draw(self):
        if not self.is_shooted:
            pyxel.circ(self.x, self.y, self.r, pyxel.COLOR_PURPLE)  # 打たれてなかったら描写
```

次に、敵にレティクルを合わせた状態でマウスをクリックしたら、is_shootedがTrueになって敵が消える、というような処理を実装します。
サンプルにある`06_click_game.py`を参考にしました。

```python
class App:
    def update(self) -> None:
        if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
            dx = self.enemy.x - pyxel.mouse_x
            dy = self.enemy.y - pyxel.mouse_y

            if dx * dx + dy * dy < self.enemy.r * self.enemy.r:
                self.enemy.is_shooted = True
```

#### 作ったもの
![0.5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/6a31a735-9dd9-601c-b623-ddc62bba6682.gif)

### 弾が出るようにしてみる

現状ですと、AIMLAB見たいな感じになってしまっているので、よりNIKKEに近づけるために、クリックしたら弾が発射されるようにしてみたいと思います。
サンプルにある`09_shooter.py`を参考にしました。

複数の弾を処理するために、リストの中に発射した弾のオブジェクトを入れ、それらをfor文でupdateしたりdrawしたり、消すための処理を実装します。
```python
bullets = []


def update_list(list):
    for elem in list:
        elem.update()


def draw_list(list):
    for elem in list:
        elem.draw()


def cleanup_list(list):
    i = 0
    while i < len(list):
        elem = list[i]
        if not elem.is_alive:
            list.pop(i)
        else:
            i += 1
```

次に、弾のクラスを実装します。

```python
class Bullet:
    def __init__(self, x: int, y: int):
        self.x = x
        self.y = SCREEN_HEIGHT
        self.y_target = y
        self.w = BULLET_WIDTH
        self.h = BULLET_HEIGHT
        self.is_alive = True
        bullets.append(self)

    def update(self):
        self.y -= BULLET_SPEED
        if self.y + self.h - 1 < 0:
            self.is_alive = False  # 画面端まで達したら消える

    def draw(self):
        pyxel.rect(self.x, self.y - BULLET_HEIGHT, self.w, self.h, BULLET_COLOR)
```

次に、Appのupdateに、クリックしたらBulletクラスを生成する処理、bulletsのリストの中にあるBulletをupdateしたりcleanupしたりする処理を実装します。
```python
class App:
    def update(self) -> None:
        update_list(bullets)
        cleanup_list(bullets)

        if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
            Bullet(
                pyxel.mouse_x,
                pyxel.mouse_y,
            )
```

最後に、bulletsのリストの中にあるBulletをfor文で回してdrawする処理を実装します。
```python
class App:
    def draw(self) -> None:
        draw_list(bullets)
```

#### 作ったもの
![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/bcd071a6-db1b-eb95-4afe-883d3e701661.gif)

シューティングゲーっぽいですね！

### 弾が発射される位置を調整してみる

さて、NIKKEでは弾を発射するのは真ん中にいるプレイヤーになります。
なので、発射位置をプレイヤーにして、そこからクリックした場所に向かって弾が飛んでいくようにしましょう！


プレイヤーの場所から、クリックした場所までの縦の距離を計算し、その距離を弾の速度で割って、到達するまでのフレーム数みたいなものを割り出しています。
そのフレーム数を用いて、クリックした場所の横の距離をN等分して、毎フレームxを移動させています。
もっといい計算方法がある気がしますが、数学がヨワヨワな自分にとってはこれぐらいしか思いつきませんでした。
```python
class Bullet:
    def __init__(self, x: int, y: int):
        self.x_start = SCREEN_WIDTH / 2
        self.x = SCREEN_WIDTH / 2
        self.y = SCREEN_HEIGHT
        self.x_target = x
        self.y_target = y
        self.w = BULLET_WIDTH
        self.h = BULLET_HEIGHT
        self.is_alive = True
        bullets.append(self)

        self.x_s = (self.x - self.x_target) / (
            (SCREEN_HEIGHT - self.y_target) / BULLET_SPEED
        )

    def update(self):
        self.y -= BULLET_SPEED
        self.x -= self.x_s
        if self.y + self.h - 1 < 0:
            self.is_alive = False
```

#### 作ったもの
![2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/39a3ecfd-c18c-4a32-605b-74536b192f63.gif)

NIKKE感出てきましたね！

### 弾が当たったら敵を倒せるようにしてみる

さて、シューティングゲーですので、発射した弾が的に当たったら敵を倒せるようにしてみたいと思います。

ごちゃごちゃしてますが、敵の当たり判定を作って、その範囲内に弾が存在していたら、敵を消す、みたいな感じです。

```python
class App:
    def update(self) -> None:
        for b in bullets:
            is_enemy_hit_x = ((self.enemy.x - self.enemy.r) <= (b.x + BULLET_WIDTH)) & (
                (self.enemy.x + self.enemy.r) >= (b.x)
            )
            is_enemy_hit_y = (
                (self.enemy.y - self.enemy.r) <= (b.y + BULLET_HEIGHT)
            ) & ((self.enemy.y + self.enemy.r) >= (b.y))
            if is_enemy_hit_x & is_enemy_hit_y:
                self.enemy.is_shooted = True
```

#### 作ったもの

![3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/ca014a96-d1e3-4515-6492-26b4738bb13a.gif)

やっとゲームと呼んで良さそうな気がします。

### 敵が動くようにしてみる

さて、このままだと敵が動かないし、一体倒したら終わりというRTA御用達のゲームになってしまいそうなので、
敵を動かしたり、敵がリポップするようにしてみたいと思います。

とりあえず、一番上でポップして、そこから下に動くようにします。
```python
ENEMY_SPEED = 5

class Enemy:
    def __init__(self):
        self.y = 0

    def update(self):
        self.y += ENEMY_SPEED

        if self.y >= SCREEN_HEIGHT:
            self.is_alive = False  # 一番下まで辿り着いたら消える

    def draw(self):
        pyxel.circ(self.x, self.y, self.r, pyxel.COLOR_PURPLE)
```

また、敵がリポップされるように、敵もリストで管理するようにしてみたいと思います。

```python
enemies = []

class Enemy:
    def __init__(self):
        enemies.append(self)


class App:
    def update(self) -> None:
        if not enemies:
            Enemy()
```

そして最後に、弾の当たり判定と描画部分を追加します。
当たり判定は敵ごと・弾ごとで判定するようにしてみたのですが、計算が間に合っているかは知りません。弾抜けするような気もしますが、この世界観なら許されるでしょう。

```python
class App:
    def update(self) -> None:
        for e in enemies:
            for b in bullets:
                is_enemy_hit_x = ((e.x - e.r) <= (b.x + BULLET_WIDTH)) & (
                    (e.x + e.r) >= (b.x)
                )
                is_enemy_hit_y = ((e.y - e.r) <= (b.y + BULLET_HEIGHT)) & (
                    (e.y + e.r) >= (b.y)
                )
                if is_enemy_hit_x & is_enemy_hit_y:
                    e.is_alive = False

        update_list(bullets)
        update_list(enemies)
        cleanup_list(bullets)
        cleanup_list(enemies)

    def draw(self) -> None:
        draw_list(enemies)
```

#### 作ったもの

![5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/c9633f47-8220-11a1-6698-1c60fa8cbb39.gif)

割と無限に遊べます（自分で作った補正込）

### スコアを実装してみる

敵を倒すならスコア表示があったほうがいいですよね！
と、いうことで実装してみます。めっちゃ簡単です。

```python
class App:
    def __init__(self) -> None:
        self.score = 0

    def update(self) -> None:
        for e in enemies:
            for b in bullets:
                is_enemy_hit_x = ((e.x - e.r) <= (b.x + BULLET_WIDTH)) & (
                    (e.x + e.r) >= (b.x)
                )
                is_enemy_hit_y = ((e.y - e.r) <= (b.y + BULLET_HEIGHT)) & (
                    (e.y + e.r) >= (b.y)
                )
                if is_enemy_hit_x & is_enemy_hit_y:
                    e.is_alive = False
                    self.score += 1  # 敵を倒したら+1

    def draw(self) -> None:
        s = f"SCORE {self.score:>4}"
        pyxel.text(5, 4, s, 1)
```
これだけ！

#### 作ったもの

![6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/c04bd2b8-8f98-f4e5-a30f-e4bad9e4a340.gif)

倒し甲斐がありますね！

### プレイヤーのHPを実装してみる

このままだと、プレイヤー側が一方的に敵をいじめるゲームになってしまうため、敵側にも抵抗のチャンスを与えたいと思います。
プレイヤーにHPの概念を与え、敵が一番下まで辿り着いてしまったらHPが減るようにしてみます。

```python
class App:
    def __init__(self) -> None:
        self.hit_point = 3

    def update(self) -> None:
        for e in enemies:
            if e.y >= SCREEN_HEIGHT:
                e.is_alive = False
                self.hit_point -= 1

    def draw(self) -> None:
        h = f"HP {self.hit_point:>2}"
        pyxel.text(5, SCREEN_HEIGHT - 8, h, 1)
```

### HPが無くなったらゲームオーバーになるようにしてみる

せっかくHPの概念を与えたので、HPが無くなったらゲームオーバーになるようにしてみましょう！

こちらの記事を参考にしました。

https://mekatamatama.hatenablog.com/entry/2020/11/07/143831

まず、各画面にIDを付与します。どうせタイトルも作るので今のうちに採番しておきます。
```python
# SCENE_TITLE = 0 # タイトル画面
SCENE_PLAY = 1  # ゲーム画面
SCENE_GAMEOVER = 2  # ゲームオーバー画面
```

また、既存のAppの中にあるupdateとdrawをプレイ画面用として関数を切り分けます。

```python
    def update_play_scene(self) -> None:
        if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
            Bullet(
                pyxel.mouse_x,
                pyxel.mouse_y,
            )
        ...

    def draw_play_scene(self) -> None:
        pyxel.cls(col=pyxel.COLOR_GRAY)
        draw_list(bullets)
        draw_list(enemies)
        self.player.draw()
        ...
```

次に、ゲームオーバー画面用のupdateとdrawを実装します。

```python
    def update_gameover_scene(self):
        """初期化して、スペースキーを押したらプレイ画面に戻る"""
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.score = 0
            self.hit_point = 3
            self.scene = SCENE_PLAY

    def draw_gameover_scene(self):
        pyxel.cls(col=pyxel.COLOR_BLACK)
        pyxel.text(
            (SCREEN_HEIGHT / 2) - 20, (SCREEN_WIDTH / 2), "GAME OVER", pyxel.COLOR_WHITE
        )
        pyxel.text(
            (SCREEN_HEIGHT / 2) - 40,
            (SCREEN_WIDTH / 2) + 10,
            "- PRESS SPACE KEY -",
            pyxel.COLOR_WHITE,
        )

```

最後に、シーンの切り替え部分を実装します。
```python
class App:
    def update(self) -> None:
        if self.scene == SCENE_PLAY:
            self.update_play_scene()
        elif self.scene == SCENE_GAMEOVER:
            self.update_gameover_scene()

    def draw(self) -> None:
        if self.scene == SCENE_PLAY:
            self.draw_play_scene()
        elif self.scene == SCENE_GAMEOVER:
            self.draw_gameover_scene()
```

#### 作ったもの

![8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/2ddde8a2-d59b-486c-2aae-943e34ee9a7e.gif)

何歳になってもゲームオーバー画面は少し悲しいものですね。
ちなみに一番好きなゲームオーバー画面は、たけしの挑戦状の画面です。子供の頃怖かった。

### タイトル画面も作っちゃおう！

どうせなのでタイトル画面も作っちゃおうと思います。
タイトルは、NIKKEを勝手に自分でつくろうとしているので、egoisticNIKKE 略して「EGO NIKKE」か、genericNIKKE 略して「GENE NIKKE」のどちらかにしようと思ったのですが、なんとなく「EGO NIKKE」にしようと思います。
※怒られたら変えます

text関数で使えるフォントだと小さいので、別途ドット絵のロゴを作ります。
<img width="500" alt="title edit" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/9e00b160-6011-0f72-3a4a-f5cdd1c65abe.png">

タイトル画面の実装もしちゃいます！
```python
SCENE_TITLE = 0  # タイトル画面

class App:
    def __init__(self) -> None:
        self.scene = SCENE_TITLE

    def update(self) -> None:
        if self.scene == SCENE_TITLE:
            self.update_title_scene()
        elif self.scene == SCENE_PLAY:
            self.update_play_scene()
        elif self.scene == SCENE_GAMEOVER:
            self.update_gameover_scene()

    def update_title_scene(self) -> None:
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.scene = SCENE_PLAY

    def draw(self) -> None:
        if self.scene == SCENE_TITLE:
            self.draw_title_scene()
        elif self.scene == SCENE_PLAY:
            self.draw_play_scene()
        elif self.scene == SCENE_GAMEOVER:
            self.draw_gameover_scene()

    def draw_title_scene(self) -> None:
        pyxel.cls(col=pyxel.COLOR_BLACK)
        pyxel.blt(
            (SCREEN_HEIGHT / 2) - 61, (SCREEN_WIDTH / 2) - 10, 0, 48, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) - 46, (SCREEN_WIDTH / 2) - 10, 0, 64, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) - 29, (SCREEN_WIDTH / 2) - 10, 0, 80, 16, 16, 16, 0
        )
        pyxel.blt((SCREEN_HEIGHT / 2) - 6, (SCREEN_WIDTH / 2) - 10, 0, 0, 16, 16, 16, 0)
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 5, (SCREEN_WIDTH / 2) - 10, 0, 16, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 17, (SCREEN_WIDTH / 2) - 10, 0, 32, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 32, (SCREEN_WIDTH / 2) - 10, 0, 32, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 46, (SCREEN_WIDTH / 2) - 10, 0, 48, 16, 16, 16, 0
        )
        pyxel.text(
            (SCREEN_HEIGHT / 2) - 40,
            (SCREEN_WIDTH / 2) + 10,
            "- PRESS SPACE KEY -",
            pyxel.COLOR_WHITE,
        )
```

ちょっとロゴを配置するところがごちゃごちゃしてますね・・・

#### 作ったもの

<img width="400" alt="title tukutta" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/420547/f4821b00-265a-9d98-0ea4-f29d5151541b.png">

それっぽい！！

## （未）完成物

[（未）完成物](#作ったもの)

<details><summary>全体のコード</summary>

<div>

```python
import pyxel

SCREEN_WIDTH = 256
SCREEN_HEIGHT = 256

SCENE_TITLE = 0  # タイトル画面
SCENE_PLAY = 1  # ゲーム画面
SCENE_GAMEOVER = 2  # ゲームオーバー画面


BULLET_WIDTH = 1.5
BULLET_HEIGHT = 5
BULLET_COLOR = pyxel.COLOR_WHITE
BULLET_SPEED = 20

ENEMY_SPEED = 2

bullets = []
enemies = []


def update_list(list):
    for elem in list:
        elem.update()


def draw_list(list):
    for elem in list:
        elem.draw()


def cleanup_list(list):
    i = 0
    while i < len(list):
        elem = list[i]
        if not elem.is_alive:
            list.pop(i)
        else:
            i += 1


class Player:
    def __init__(self, y_ground: int) -> None:
        self.x = (pyxel.width / 2) - 25
        self.y = y_ground - 50

    def update(self):
        pass

    def draw(self):
        pyxel.rect(self.x, self.y, 50, 50, 9)


class Enemy:
    def __init__(self):
        self.r = 5
        self.x = pyxel.rndf(self.r, SCREEN_WIDTH - self.r)
        self.y = 0
        self.is_alive = True

        enemies.append(self)

    def update(self):
        self.y += ENEMY_SPEED

    def draw(self):
        pyxel.circ(self.x, self.y, self.r, pyxel.COLOR_PURPLE)


class Bullet:
    def __init__(self, x: int, y: int):
        self.x_start = SCREEN_WIDTH / 2
        self.x = SCREEN_WIDTH / 2
        self.y = SCREEN_HEIGHT
        self.x_target = x
        self.y_target = y
        self.w = BULLET_WIDTH
        self.h = BULLET_HEIGHT
        self.is_alive = True
        bullets.append(self)

        self.x_s = (self.x - self.x_target) / (
            (SCREEN_HEIGHT - self.y_target) / BULLET_SPEED
        )

    def update(self):
        self.y -= BULLET_SPEED
        self.x -= self.x_s
        if self.y + self.h - 1 < 0:
            self.is_alive = False

    def draw(self):
        pyxel.rect(self.x, self.y - BULLET_HEIGHT, self.w, self.h, BULLET_COLOR)


class App:
    def __init__(self) -> None:
        pyxel.init(SCREEN_WIDTH, SCREEN_HEIGHT)
        pyxel.load("./assets/my_resource.pyxres")

        self.player = Player(pyxel.height)

        self.score = 0
        self.hit_point = 3

        self.scene = SCENE_TITLE

        pyxel.run(self.update, self.draw)

    def update(self) -> None:
        if pyxel.btnp(pyxel.KEY_Q):
            pyxel.quit()

        if self.scene == SCENE_TITLE:
            self.update_title_scene()
        elif self.scene == SCENE_PLAY:
            self.update_play_scene()
        elif self.scene == SCENE_GAMEOVER:
            self.update_gameover_scene()

    def update_title_scene(self) -> None:
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.scene = SCENE_PLAY

    def update_play_scene(self) -> None:
        if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
            Bullet(
                pyxel.mouse_x,
                pyxel.mouse_y,
            )

        if not enemies:
            Enemy()

        for e in enemies:
            for b in bullets:
                is_enemy_hit_x = ((e.x - e.r) <= (b.x + BULLET_WIDTH)) & (
                    (e.x + e.r) >= (b.x)
                )
                is_enemy_hit_y = ((e.y - e.r) <= (b.y + BULLET_HEIGHT)) & (
                    (e.y + e.r) >= (b.y)
                )
                if is_enemy_hit_x & is_enemy_hit_y:
                    e.is_alive = False
                    self.score += 1

            if e.y >= SCREEN_HEIGHT:
                e.is_alive = False
                self.hit_point -= 1

        if self.hit_point == 0:
            self.scene = SCENE_GAMEOVER

        self.player.update()
        update_list(bullets)
        update_list(enemies)
        cleanup_list(bullets)
        cleanup_list(enemies)

    def update_gameover_scene(self):
        if pyxel.btnp(pyxel.KEY_SPACE):
            self.score = 0
            self.hit_point = 3
            self.scene = SCENE_TITLE

    def draw(self) -> None:
        if self.scene == SCENE_TITLE:
            self.draw_title_scene()
        elif self.scene == SCENE_PLAY:
            self.draw_play_scene()
        elif self.scene == SCENE_GAMEOVER:
            self.draw_gameover_scene()

    def draw_title_scene(self) -> None:
        pyxel.cls(col=pyxel.COLOR_BLACK)
        pyxel.blt(
            (SCREEN_HEIGHT / 2) - 61, (SCREEN_WIDTH / 2) - 10, 0, 48, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) - 46, (SCREEN_WIDTH / 2) - 10, 0, 64, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) - 29, (SCREEN_WIDTH / 2) - 10, 0, 80, 16, 16, 16, 0
        )
        pyxel.blt((SCREEN_HEIGHT / 2) - 6, (SCREEN_WIDTH / 2) - 10, 0, 0, 16, 16, 16, 0)
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 5, (SCREEN_WIDTH / 2) - 10, 0, 16, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 17, (SCREEN_WIDTH / 2) - 10, 0, 32, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 32, (SCREEN_WIDTH / 2) - 10, 0, 32, 16, 16, 16, 0
        )
        pyxel.blt(
            (SCREEN_HEIGHT / 2) + 46, (SCREEN_WIDTH / 2) - 10, 0, 48, 16, 16, 16, 0
        )
        pyxel.text(
            (SCREEN_HEIGHT / 2) - 40,
            (SCREEN_WIDTH / 2) + 10,
            "- PRESS SPACE KEY -",
            pyxel.COLOR_WHITE,
        )

    def draw_play_scene(self) -> None:
        pyxel.cls(col=pyxel.COLOR_GRAY)
        draw_list(bullets)
        draw_list(enemies)
        self.player.draw()
        pyxel.blt(pyxel.mouse_x - 7, pyxel.mouse_y - 7, 0, 0, 0, 16, 16, 0)

        s = f"SCORE {self.score:>4}"
        pyxel.text(5, 4, s, 1)

        h = f"HP {self.hit_point:>2}"
        pyxel.text(5, SCREEN_HEIGHT - 8, h, 1)

    def draw_gameover_scene(self):
        pyxel.cls(col=pyxel.COLOR_BLACK)
        pyxel.text(
            (SCREEN_HEIGHT / 2) - 20, (SCREEN_WIDTH / 2), "GAME OVER", pyxel.COLOR_WHITE
        )
        pyxel.text(
            (SCREEN_HEIGHT / 2) - 40,
            (SCREEN_WIDTH / 2) + 10,
            "- PRESS SPACE KEY -",
            pyxel.COLOR_WHITE,
        )


App()

```

</div>

</details>

## 今後やりたいこと
### システム関係
- 弾数の概念を作って、弾をリロードするようにする
- 敵にHPを与える
- 色んな武器を作る
    - AR
    - SMG
    - SG
    - SR
    - etc...

### グラフィック関係
- 背景を描く
- キャラクターの絵を描く
- 敵の絵を描く


### サウンド関係
- BGMを作る
- 弾を打つときのSEを作る

# あとがき

いかがでしたか？
今回はPythonで書けるゲームエンジンのpyxelを用いて簡単なゲームを作ってみました。
皆様もお金持ちになるためにゲームで一山を当てるための第一歩を踏み出してみてはいかがでしょうか？
ちなみに、今後実装したいものについては、また別で記事を作成したいと思っています。ご期待ください。
では、良いお年を。