【Python】自作マリオ キーイベント処理

# Pygameでマリオのキーイベントを追加する
マリオをPythonで作成するハンズオン動画を見つけたので、実装してみる。
[参考：【#4 キーイベント処理】マリオを左右に動かそう！マリオを一緒に作ってみませんか！pythonで！【ハンズオン実践解説】](https://youtu.be/9D1NnNoxFeQ?si=7XW1SanAmHQJ3hXy)

## **1. マリオの動きの実装**
### **(1) キーボード入力に応じた移動の追加**
-  `update()` メソッド内で `pygame.key.get_pressed()` を使用し、右 (`→`)、左 (`←`)、ジャンプ (`スペースキー`) に対応。

```python
def update(self):
        # キーボードの状態を取得
        keys = pygame.key.get_pressed()
        if keys[pygame.K_RIGHT]:
            self.__right()
        if keys[pygame.K_LEFT]:
            self.__left()
        if keys[pygame.K_SPACE]:
            self.__jump()
```

### **(2) 移動処理の追加**
- `__right()` → 右移動 (`rect.x += 5`)
- `__left()` → 左移動 (`rect.x -= 5`)
- `__jump()` → ジャンプ (`self.__vy = -10` に設定し、地面から離れる`)

```python
    def __right(self):
        self.rect.x += 5
        self.__isleft = False
        self.__walkidx += 1

    def __left(self):
        self.rect.x -= 5
        self.__isleft = True
        self.__walkidx += 1
    
    def __jump(self):
        if self.__on_ground:
            self.__vy = -10
            self.__on_ground = False
```

### **(3) Y軸の移動処理**
- `__vy` を用いた重力処理を追加。
  - `if not self.__on_ground:` で空中にいる場合 `self.rect.y` を `__vy` の値だけ変化させる。
  - `__vy` の値を `+1` ずつ増やし、落下する動作を実現。
  - 地面 (`y = 180`) に着いたら `__on_ground = True` にしてジャンプ可能にする。

```python
    # Y軸方向に移動
        if not self.__on_ground:
            self.rect.y += self.__vy
            self.__vy += 1

            if self.rect.y >= 180:
                self.rect.y = 180
                self.__on_ground = True
```

## **2. 歩行アニメーションの追加**
### **(1) 画像のフレーム切り替え**
- **前回:** `mario001.png` という1枚の画像のみを表示。
- **今回:** `mario001.png` `mario002.png` の2枚の画像を配列 `self.__imgs` に格納し、歩行時に交互に切り替え。
  - `WALK_ANIME_IDX = [0, 0, 0, 1, 1, 1]` を用いて、6フレームのうち `0` (mario001) と `1` (mario002) を交互に表示。

```python
        # marioの画像を読み込む
        self.__imgs = [
            pygame.image.load('mario001.png'),
            pygame.image.load('mario002.png')
        ]
        self.image = self.__imgs[0]
        self.rect = pygame.Rect(150, 180, 20, 20)

```
|mario001.png|mario002.png|
| -----------| ---------- |
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782903/ab6ee7e3-691b-436c-47fd-eff67f218eda.png" width="200px">|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782903/a2d75a1e-ce1e-cc57-0ea4-7d0f17533287.png" width="200px">|

### **(2) 左右の向きの判定**
- `self.__isleft` で左右の向きを記録し、画像の反転を実現。
  - `pygame.transform.flip(self.__imgs[self.WALK_ANIME_IDX[self.__walkidx % 6]], self.__isleft, False)`

```python
        # 左右どちら向きかのフラグを定義
        self.__isleft = False
　　　　
        ～～～

        self.image = pygame.transform.flip(self.__imgs[self.WALK_ANIME_IDX[self.__walkidx % 6]], self.__isleft, False)
```
## 完全なコード
```python
import pygame

# 画面サイズを定義
W, H = 320, 270

# タイル数
TILE_X = 16
TILE_Y = 14

class Mario(pygame.sprite.Sprite):
    ''' マリオのクラス
    '''
    WALK_ANIME_IDX = [0, 0, 0, 1, 1, 1]
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)

        # 左右どちら向きかのフラグを定義
        self.__isleft = False
        # 歩くインデックス
        self.__walkidx = 0
        # Y軸方向移動郷里
        self.__vy = 0
        # マリオが地面にいるか
        self.__on_ground = True

        # marioの画像を読み込む
        self.__imgs = [
            pygame.image.load('mario001.png'),
            pygame.image.load('mario002.png')
        ]
        self.image = self.__imgs[0]
        self.rect = pygame.Rect(150, 180, 20, 20)

    def __right(self):
        self.rect.x += 5
        self.__isleft = False
        self.__walkidx += 1

    def __left(self):
        self.rect.x -= 5
        self.__isleft = True
        self.__walkidx += 1
    
    def __jump(self):
        if self.__on_ground:
            self.__vy = -10
            self.__on_ground = False


    def update(self):
        # キーボードの状態を取得
        keys = pygame.key.get_pressed()
        if keys[pygame.K_RIGHT]:
            self.__right()
        if keys[pygame.K_LEFT]:
            self.__left()
        if keys[pygame.K_SPACE]:
            self.__jump()

        # Y軸方向に移動
        if not self.__on_ground:
            self.rect.y += self.__vy
            self.__vy += 1

            if self.rect.y >= 180:
                self.rect.y = 180
                self.__on_ground = True

        self.image = pygame.transform.flip(self.__imgs[self.WALK_ANIME_IDX[self.__walkidx % 6]], self.__isleft, False)
def main():
    '''メイン関数'''
    # pygame初期化
    pygame.init()
    # 画面を構築
    win = pygame.display.set_mode((W, H))
    pygame.display.set_caption("Pygame Window")  # ウィンドウタイトルを設定
    # クロックを生成
    clock = pygame.time.Clock()

    # スプライトグループを定義
    group = pygame.sprite.RenderUpdates()
    # マリオクラスを定義
    mario = Mario()
    # マリオをグループに追加
    group.add(mario)


    # 背景色の初期値（黒）
    bg_color = (0, 0, 0)

    # イベントループ
    running = True
    while running:
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False  # ループを抜ける

        # 背景を塗りつぶす
        win.fill((135, 206, 235))

        # グループを更新
        group.update()

        # グループを描画
        group.draw(win)

        # 画面の更新
        pygame.display.flip()

        clock.tick(30)  # FPS制限 (60FPS)

    # pygameを終了する
    pygame.quit()

# Pythonのファイルが直接実行された場合のみmain()を実行
if __name__ == '__main__':
    main()

```
## **まとめ**
| 変更点 | 前回 | 今回 |
|--------|------|------|
| キーボード入力 | なし | `pygame.key.get_pressed()` を使用 |
| 横移動 | なし | `__right()` `__left()` で移動可能 |
| ジャンプ | なし | `__jump()` を追加 |
| 重力処理 | なし | `self.__vy` を用いた落下処理 |
| 画像 | `mario001.png` のみ | `mario001.png` `mario002.png` を交互に表示 |
| 向き判定 | なし | `pygame.transform.flip()` で左右反転 |

この修正により、マリオが**移動・ジャンプし、歩行アニメーションがつく**ようになりました。

![mario.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3782903/0bb63818-0c76-6358-17f2-35610cd79c1b.gif)