Python&arcadeで簡単！レトロ風シューティングゲームを作ろう！🚀

# 本記事のゲームの完成版を作りました！
**コード**
- [Githubのリンク](https://github.com/programingbigger/SimpleShooter)

**改善されたところ**
- コードを管理しやすいようにしました
- ユニットから出てくる弾丸をユニットの現在位置から射出されるようにしました
- スタート画面・ゲーム画面・ゲームオーバー画面の作成
- 生存時間と敵撃墜数を設定

## Pythonで簡単！レトロ風シューティングゲームを作ろう！🚀

Pythonで手軽にゲーム開発を楽しみたいと思ったことはありませんか？今回は、`arcade`ライブラリを使って、昔懐かしい横スクロール型のシューティングゲームを開発する方法を紹介します！🚀

**完成イメージ**
![ezgif-4-87c3f13a8d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769985/138b7586-7a03-b4c3-7396-3c5910c40689.gif)


**この記事を読むことで、以下のことができるようになります。**

* `arcade`ライブラリを使ったゲーム開発の基本を理解する
* スプライトの描画、更新、衝突判定の実装方法を学ぶ
* キーボード入力によるキャラクター操作の実装方法を学ぶ

**動作環境:**

* Python 3.10.10
* macOS
* `arcade` ライブラリ

**ゲームの始め方:**

1. 必要な画像ファイルをダウンロードし、指定のフォルダに配置します。（後述）
2. ターミナルで `python3 main.py` と入力して実行します。

**まずはコード全体を見てみましょう！**

```python
import arcade
import random

# 画面サイズとタイトルの設定
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Simple Game"
SCALING = 0.1  # スプライトの拡大縮小率

# 飛行するスプライトのクラス
class FlyingSprite(arcade.Sprite):
    """敵と雲のスプライトの基底クラス"""

    def update(self):
        """スプライトの更新処理"""
        super().update()
        # 画面外に出たらスプライトを削除
        if self.right < 0:
            self.remove_from_sprite_lists()

# 弾丸のスプライトクラス
class Bullet(arcade.Sprite):
    """弾丸のスプライト"""

    def update(self):
        """弾丸の更新処理"""
        super().update()
        # 画面外に出たら弾丸を削除
        if self.left > SCREEN_WIDTH:
            self.remove_from_sprite_lists()

# ゲームのメインウィンドウ
class SpaceShooter(arcade.Window):
    """スペースシューターゲームのメインウィンドウ"""

    def __init__(self, width, height, title):
        super().__init__(width, height, title)
        # スプライトリストの初期化
        self.enemies_list = arcade.SpriteList()
        self.clouds_list = arcade.SpriteList()
        self.all_sprites = arcade.SpriteList()
        self.bullets_list = arcade.SpriteList()
        self.paused = False

    def setup(self):
        """ゲームの初期設定"""
        arcade.set_background_color(arcade.color.SKY_BLUE)
        # プレイヤーのスプライトを作成
        self.player = arcade.Sprite("images/airplane.png", SCALING)
        self.player.center_y = self.height / 2
        self.player.left = 10
        self.all_sprites.append(self.player)

        # 敵と雲を定期的に生成
        arcade.schedule(self.add_enemy, 0.25)
        arcade.schedule(self.add_cloud, 0.5)

    def add_enemy(self, delta_time: float):
        """敵を生成"""
        enemy = FlyingSprite("images/enemy.png", SCALING * 2)
        enemy.left = random.randint(self.width, self.width + 80)
        enemy.top = random.randint(10, self.height - 10)
        enemy.velocity = (random.randint(-7, -3), random.randint(-1, 1))
        self.enemies_list.append(enemy)
        self.all_sprites.append(enemy)

    def add_cloud(self, delta_time: float):
        """雲を生成"""
        cloud = FlyingSprite("images/cloud.png", SCALING * 2)
        cloud.center_x = random.randint(self.width - 80, self.width)
        cloud.center_y = random.randint(self.height - 500, self.height)
        cloud.velocity = (random.randint(-3, -2), 0)
        self.clouds_list.append(cloud)
        self.all_sprites.append(cloud)

    def shoot_bullet(self):
        """弾丸を発射"""
        bullet = Bullet("images/bullet.png", SCALING)
        bullet.center_y = self.player.center_y
        bullet.left = self.player.right # 弾丸の位置を修正
        bullet.velocity = (10, 0)
        self.bullets_list.append(bullet)
        self.all_sprites.append(bullet)

    def on_key_press(self, symbol: int, modifiers: int):
        """キーボード押下時の処理"""
        if symbol == arcade.key.Q:
            arcade.close_window()
        if symbol == arcade.key.P:
            self.paused = not self.paused
        if symbol == arcade.key.I or symbol == arcade.key.UP:
            self.player.change_y = 5
        if symbol == arcade.key.K or symbol == arcade.key.DOWN:
            self.player.change_y = -5
        if symbol == arcade.key.J or symbol == arcade.key.LEFT:
            self.player.change_x = -5
        if symbol == arcade.key.L or symbol == arcade.key.RIGHT:
            self.player.change_x = 5
        if symbol == arcade.key.SPACE:
            self.shoot_bullet()

    def on_key_release(self, symbol: int, modifiers: int):
        """キーボード解放時の処理"""
        if (symbol == arcade.key.I or symbol == arcade.key.K or
                symbol == arcade.key.UP or symbol == arcade.key.DOWN):
            self.player.change_y = 0
        if (symbol == arcade.key.J or symbol == arcade.key.L or
                symbol == arcade.key.LEFT or symbol == arcade.key.RIGHT):
            self.player.change_x = 0

    def on_update(self, delta_time):
        """ゲームの更新処理"""
        if self.paused:
            return

        # 衝突判定
        if self.player.collides_with_list(self.enemies_list):
            arcade.exit()  # 衝突したらゲーム終了

        for bullet in self.bullets_list:
            hit_enemies = arcade.check_for_collision_with_list(bullet, self.enemies_list)
            for enemy in hit_enemies:
                bullet.remove_from_sprite_lists()
                enemy.remove_from_sprite_lists()

        self.all_sprites.update()

        # プレイヤーが画面外に出ないようにする
        self.player.left = max(0, self.player.left)
        self.player.right = min(self.width, self.player.right)
        self.player.top = min(self.height, self.player.top)
        self.player.bottom = max(0, self.player.bottom)

    def on_draw(self):
        """ゲームの描画処理"""
        arcade.start_render()
        self.all_sprites.draw()


def main():
    """メイン関数"""
    game = SpaceShooter(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    game.setup()
    arcade.run()

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

### 1. 準備：`arcade`ライブラリのインストールと画像の準備

まずは、`arcade`ライブラリをインストールしましょう。ターミナルで以下のコマンドを実行します。

```bash
pip install arcade
```

次に、ゲームで使用する画像ファイルを準備します。以下の4つの画像が必要です。

* airplane.png：プレイヤーの飛行機
* enemy.png：敵機
* cloud.png：雲
* bullet.png：弾丸

これらの画像ファイルは各自適当に設定しましょう！  
その後、プロジェクトフォルダ内に`images`というフォルダを作成して、その中に配置してください。  
ぜひ、自由に好きな画像を使ってカスタマイズしてみてください！😊

### 2. スプライトクラスの作成

`arcade`ライブラリでは、ゲーム内のオブジェクトをスプライトとして扱います。まずは、飛ぶオブジェクトのスプライトクラス`FlyingSprite`と、弾丸のスプライトクラス`Bullet`を作成します。

```python
# 飛行するスプライトのクラス
class FlyingSprite(arcade.Sprite):
    """敵と雲のスプライトの基底クラス"""

    def update(self):
        """スプライトの更新処理"""
        super().update()
        # 画面外に出たらスプライトを削除
        if self.right < 0:
            self.remove_from_sprite_lists()

# 弾丸のスプライトクラス
class Bullet(arcade.Sprite):
    """弾丸のスプライト"""

    def update(self):
        """弾丸の更新処理"""
        super().update()
        # 画面外に出たら弾丸を削除
        if self.left > SCREEN_WIDTH:
            self.remove_from_sprite_lists()
```

`FlyingSprite`クラスは、敵機と雲の共通の基底クラスとして、画面外に出たスプライトを削除する機能を持っています。`Bullet`クラスも同様に、画面外に出た弾丸を削除します。

### 3. ゲームウィンドウクラスの作成

次に、ゲームのメインウィンドウとなる`SpaceShooter`クラスを作成します。

```python
# ゲームのメインウィンドウ
class SpaceShooter(arcade.Window):
    """スペースシューターゲームのメインウィンドウ"""

    def __init__(self, width, height, title):
        # 省略（__init__～add_cloud()まで）

    def shoot_bullet(self):
        """弾丸を発射"""
        bullet = Bullet("images/bullet.png", SCALING)
        bullet.center_y = self.player.center_y
        bullet.left = self.player.right # 弾丸の位置を修正: プレイヤーの右側から発射
        bullet.velocity = (10, 0)
        self.bullets_list.append(bullet)
        self.all_sprites.append(bullet)

     # 省略 (on_key_press()～on_draw()まで)
```

このクラスでは、スプライトリストの初期化、プレイヤーの配置、敵機と雲の生成、キーボード入力の処理、ゲームの更新、描画など、ゲームの主要な処理を行います。

### 4. ゲームの実行

最後に、ゲームを実行する`main()`関数を作成します。

```python
def main():
    """メイン関数"""
    game = SpaceShooter(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    game.setup()
    arcade.run()

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

これで、レトロ風スペースシューターゲームの完成です！🎉  `python3 main.py`コマンドでゲームを実行し、遊んでみましょう！

### 5. さらにカスタマイズしてみよう！

このコードをベースに、様々なカスタマイズを加えて、自分だけのオリジナルゲームを作ってみましょう！例えば、以下のようなカスタマイズが考えられます。

* 敵の種類を増やす
* パワーアップアイテムを追加する
* 背景をスクロールさせる
* スコア表示を追加する
* サウンドエフェクトを追加する

`arcade`ライブラリのドキュメントを参考に、色々な機能を試してみてください！  楽しいゲーム開発ライフを！🎮

# 参考文献
- [Arcade: A Primer on the Python Game Framework](https://realpython.com/arcade-python-game-framework/)
    - このサイトをほぼ模写しました。arcadeを理解するのに非常に有益なサイトです
- [The Python Arcade Library](https://api.arcade.academy/en/latest/index.html)
    - arcade公式サイト
- Gemini

# 最後に
この記事はAIアシスタントによって生成されました
コードは頑張って生成AIの力を借りながら頑張りました。
AIアシスタントは具体的には、Gemini 1.5Pro 002です。
文章は適宜修正しました。

# ほかにもこんなのやってます
- [pythonのarcadeでhexmapを作成してみた！](https://qiita.com/nami_fumi/items/dc679b47f57fa5a18f68)
- [pythonのarcadeを使って、ユニット選択とボタンの表示をやってみた](https://qiita.com/nami_fumi/items/c819bd71dac6123f942d)
- [PythonのArcadeでHexmap上のユニットを移動させる](https://qiita.com/nami_fumi/items/296adbdce9ef94a30852)