pygameを用いて簡単な格闘ゲームを自作してみた2

# 前回からの続き
[前回](https://qiita.com/yinengtadatada/items/0c47e24aff11fee9022c)の続きから始めます。そのため前回の「pygameを用いて簡単な格闘ゲームを自作してみた1」を見ていないかたは先にそちらを見てください。

# 今回の目標
前回はステージを作成しました。作成したとはいってもただ真っ白いpygame windowに緑色のブロックを表示するだけなので大したことはしてないですが‥
今回は戦闘用ステージ上にキャラクターを表示しようと思います。ただ表示するだけではつまらないので表示したキャラを操作できるようにします。左右の移動とジャンプの実装を今回の目標とします。

# 操作キャラの表示
いよいよ操作キャラクターを表示します。

```bash:ディレクトリ構造
プロジェクト名/
         ├ main.py
         └ game_parts/
                ├ __init__.py
                ├ stage.py
                ├ fighter.py
                ├ control.py
```


```python:main.py
import pygame

from game_parts import stage, fighter, control

pygame.init()
SURFACE = pygame.display.set_mode((1500, 900))
FPSCLOCK = pygame.time.Clock()
STAGE_POS = [0, 550]  # <= 表示するステージの位置


def main():
    player1_pos = [1000, 470]  # <= 操作キャラの位置
    player1_size = [50, 80]  # <= 操作キャラの大きさ [横幅, 縦幅]
    move1 = [False, False, False, False]  # <= [上方向に移動、下方向に移動、右方向に移動、左方向に移動]
    jump_speed1 = 20  # <= ジャンプの初速度
    contact1 = [False, False, False, False]  # <= [キャラクターと地面との接触判定、頭上のオブジェクトとの接触判定、右側とオブジェクトとの接触判定、左側とオブジェクトの接触判定
    action1 = [False, False, False, False]  # <= [弱攻撃、必殺技、ガード、掴み]

    while True:
        # 背景を真っ白に設定する
        SURFACE.fill((250, 250, 250))

        # プレイヤー１のキャラクター操作
        move1, action1 = control.control_character(move1, action1, contact1)

        # 戦闘ステージを作成
        stage.base_stage(STAGE_POS)

        # プレイヤー１の操作キャラの表示、行動制御
        player_1 = fighter.Fighter(player1_pos, player1_size, 16, 10, contact1)
        player_1.figure()
        player1_pos, jump_speed1, move1 = player_1.move(jump_speed1, move1)
        contact1 = player_1.contact_judgment()
        player1_pos = player_1.position_corr(STAGE_POS)

        # 画面を更新する
        pygame.display.update()

        # 画面の更新を30fps(1秒間に30枚画面が切り替わる)に設定する
        FPSCLOCK.tick(30)


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

```python:fighter.py
import pygame

import main


class Fighter:
    def __init__(self, position, size, gravity, move_speed, contact):
        self.pos_x = position[0]
        self.pos_y = position[1]
        self.width = size[0]
        self.height = size[1]
        self.gravity = gravity
        self.move_speed = move_speed
        self.contact = contact

    def figure(self):
        """
        キャラクターの大きさを決め、表示する

        """
        rect = (self.pos_x, self.pos_y, self.width, self.height)
        pygame.draw.rect(main.SURFACE, (255, 0, 0), rect)

    def move(self, jump_speed, player_move):
        """
        キャラクターの移動を制限する
        Parameters
        ----------
        jump_speed : (int) ジャンプして一定時間後の速度を表す
        player_move : プレイヤー操作(上方向、下方向、右方向、左方向)を表す

        Returns
        -------
        変化後のキャラクターの位置情報、一定時間後のジャンプ速度、プレイヤー操作(上方向、下方向、右方向、左方向)を表す

        """

        # ジャンプボタンを押した時キャラクターをジャンプさせる。ジャンプし終えた後は重力によって落下する
        if player_move[0]:
            self.pos_y -= jump_speed
            player_move[0] = False if not jump_speed else player_move[0]
            jump_speed = 20 if not jump_speed else jump_speed - 2

        elif not self.contact[0]:
            self.pos_y += self.gravity

        # 操作キャラを左右に動かす
        if player_move[2] and not self.contact[2]:
            self.pos_x += self.move_speed

        elif player_move[3] and not self.contact[3]:
            self.pos_x -= self.move_speed

        return [self.pos_x, self.pos_y], jump_speed, player_move

    def contact_judgment(self):
        """
        操作キャラとステージとの接触判定、各接触方向に対して接触した場合のみCONTACTの各接触方向の要素にTrueを返す

        Returns
        -------
        (list) 変更したcontactを返す

        """

        if self.pos_x == 1450:
            self.contact[0] = True if 550 <= self.pos_y + self.height else False
            self.contact[2] = True

        elif 0 < self.pos_x < 1450:
            self.contact[0] = True if 550 <= self.pos_y + self.height else False
            self.contact[2] = False
            self.contact[3] = False

        elif self.pos_x == 0:
            self.contact[0] = True if 550 <= self.pos_y + self.height else False
            self.contact[3] = True

        return self.contact

    def position_corr(self, stage_pos):
        """
        操作キャラとステージとの位置補正
        Parameters
        ----------
        stage_pos : 対象ステージの位置

        Returns
        -------
        補正後の操作キャラの位置(y軸方向)

        """

        if self.contact[0] and (self.pos_y + self.height - stage_pos[1]) != 0:
            self.pos_y = stage_pos[1] - self.height

        return [self.pos_x, self.pos_y]
```
fighter.pyの解説

```python
def __init__(self, position, size, gravity, move_speed, contact):
    self.pos_x = position[0]
    self.pos_y = position[1]
    self.width = size[0]
    self.height = size[1]
    self.gravity = gravity
    self.move_speed = move_speed
    self.contact = contact
```
\__init\__メソッドはインスタンスを生成した段階で働く部分なのですがFighterクラスの中で共通して使う変数をここでまとめました。

```python
def figure(self):
    """
    キャラクターの大きさを決め、表示する

    """
    rect = (self.pos_x, self.pos_y, self.width, self.height)
    pygame.draw.rect(main.SURFACE, (255, 0, 0), rect)
```
ここでは簡単なキャラクターを作成し、描画されます。描画の仕方は前回、ステージを描画したときとほぼ同じなので説明は不要ですね。

```python
def move(self, jump_speed, player_move):
    """
    キャラクターの移動を制限する
    Parameters
    ----------
    jump_speed : (int) ジャンプして一定時間後の速度を表す
    player_move : プレイヤー操作(上方向、下方向、右方向、左方向)を表す

    Returns
    -------
    変化後のキャラクターの位置情報、一定時間後のジャンプ速度、プレイヤー操作(上方向、下方向、右方向、左方向)を表す

    """

    # ジャンプボタンを押した時キャラクターをジャンプさせる。ジャンプし終えた後は重力によって落下する
    if player_move[0]:
        self.pos_y -= jump_speed
        player_move[0] = False if not jump_speed else player_move[0]
        jump_speed = 20 if not jump_speed else jump_speed - 2

    elif not self.contact[0]:
        self.pos_y += self.gravity

    # 操作キャラを左右に動かす
    if player_move[2] and not self.contact[2]:
        self.pos_x += self.move_speed

    elif player_move[3] and not self.contact[3]:
        self.pos_x -= self.move_speed

    return [self.pos_x, self.pos_y], jump_speed, player_move
```
最初のif文で上方向に跳んでいるのですがjump_speedの値を固定にしていきなり高くジャンプするのはあまりにも操作がしづらかったのでjump_speedの値を徐々に減らしつつ少しづつ上昇するようにしました。
その後、上昇しきった後重力に従って落下するのですが本来物理法則を考えれば落下する際、徐々に加速するはずです。しかし、今回は等速で落下するようにしました。一回のジャンプで跳ぶ高さはそんなに高く設定してないのでそこまで細かく再現しなくても良いかなという安易な発想です。

そして左右の操作ですが操作キャラが画面端に移動した際、それ以上先には進まないようにself.contactの値も条件に入れています。


```python
def contact_judgment(self):
    """
    操作キャラとステージとの接触判定、各接触方向に対して接触した場合のみCONTACTの各接触方向の要素にTrueを返す

    Returns
    -------
    (list) 変更したcontactを返す

    """

    if self.pos_x == 1450:
        self.contact[0] = True if 550 <= self.pos_y + self.height else False
        self.contact[2] = True

    elif 0 < self.pos_x < 1450:
        self.contact[0] = True if 550 <= self.pos_y + self.height else False
        self.contact[2] = False  # 2019/10/29 修正 
        self.contact[3] = False  # 2019/10/29 修正 

    elif self.pos_x == 0:
        self.contact[0] = True if 550 <= self.pos_y + self.height else False
        self.contact[3] = True

    return self.contact
```
上記のコードが操作キャラと前回作成したステージとの接触判定になります。
ステージの長さは横に1500あるので操作キャラの位置が0以上1500以下の範囲を移動するように設定している

```python
def position_corr(self, stage_pos):
    """
    操作キャラとステージとの位置補正
    Parameters
    ----------
    stage_pos : 対象ステージの位置

    Returns
    -------
    補正後の操作キャラの位置(y軸方向)

    """

    if self.contact[0] and (self.pos_y + self.height - stage_pos[1]) != 0:
        self.pos_y = stage_pos[1] - self.height

    return [self.pos_x, self.pos_y]
```
上記のコードは操作キャラとステージとが接触したときにキャラの位置がずれてステージにめりこんだりするのを回避するコード。
このコードがあるのと無いのとでは以下のように変わる(黒丸の部分に注目)
位置補正コードが無い時：
![位置補正コード無し.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/6f183cff-53f9-de73-d2ac-513e5d9bbd16.png)

位置補正コードがある場合：
![位置補正コードあり.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/17f43e74-8e4a-be7f-1d55-87316ec45f1f.png)

上記の違いはジャンプをしてから着地をした時にはっきりと分かる。

```python:control.py
import sys
import pygame
from pygame.locals import QUIT, KEYDOWN, KEYUP, K_q, K_w, K_e, K_c, \
    K_u, K_i, K_o, K_p


def control_character(player_move, action, contact):
    """
    対象キャラを操作する
    Parameters
    ----------
    player_move : (list) 対象キャラを移動させる
    action : (list) 攻撃、ガード、掴み
    contact : (list) 操作キャラとステージの接触判定

    Returns
    -------
    変更した移動、攻撃の判定を返す

    """
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

        elif event.type == KEYDOWN:
            if pygame.key.get_pressed()[K_w]:
                player_move[0] = True if contact[0] else player_move[0]

            if pygame.key.get_pressed()[K_c]:
                player_move[1] = True

            if pygame.key.get_pressed()[K_q]:
                player_move[3] = True

            if pygame.key.get_pressed()[K_e]:
                player_move[2] = True

            if pygame.key.get_pressed()[K_u]:
                action[0] = True

            if pygame.key.get_pressed()[K_i]:
                action[1] = True

            if pygame.key.get_pressed()[K_o]:
                action[2] = True

            if pygame.key.get_pressed()[K_p]:
                action[3] = True

        elif event.type == KEYUP:
            if event.key == K_c:
                player_move[1] = False

            if event.key == K_q:
                player_move[3] = False

            if event.key == K_e:
                player_move[2] = False

            if event.key == K_u:
                action[0] = False

            if event.key == K_i:
                action[1] = False

            if event.key == K_o:
                action[2] = False

            if event.key == K_p:
                action[3] = False

    return player_move, action

```
上記のcontrol.pyの解説

```python
for event in pygame.event.get():
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
```
上記のコードは前回main.pyファイルの中に書いたコードです。キャラクターを操作するためにpygame.event.get()が必要になるためcontrol.pyファイルに移動しました。

```python
elif event.type == KEYDOWN:
    if pygame.key.get_pressed()[K_w]:
        player_move[0] = True if contact[0] else player_move[0]

    if pygame.key.get_pressed()[K_c]:
        player_move[1] = True

    if pygame.key.get_pressed()[K_q]:
        player_move[3] = True

    if pygame.key.get_pressed()[K_e]:
        player_move[2] = True

    if pygame.key.get_pressed()[K_u]:
        action[0] = True

    if pygame.key.get_pressed()[K_i]:
        action[1] = True

    if pygame.key.get_pressed()[K_o]:
        action[2] = True

    if pygame.key.get_pressed()[K_p]:
        action[3] = True
```
上記のコードは操作キャラを移動させたり、攻撃、ガード、掴みをさせたりするコードです。対象のキーを入力し、move1,action1内の特定の要素をTrueにすることでその要素に対応した行動を取らせることができる。
ここでなぜ直接対応した行動をとる処理を書かないのか？
例えば：

```python
if pygame.key.get_pressed()[K_q]:
    player_pos[0] -= 5
```

といった処理を書かないのか。
その理由は先ほどの例のコードだとQキーを最初に入力した時しかplayer_pos[0] -= 5の処理がされないからです。本来長押ししている間は常にplayer_pos[0] -= 5の処理をし続けて欲しいのですがそれができない。
ただ、実は以下のコードを使って長押しを認識させることができる。

```python
pygame.key.set_repeat(5, 5)
```
上記のコードをmain.pyのpygame.init()の次に追加すれば可能
しかし、このコードだとジャンプの入力をした際、長押しでいくらでも画面上方向に上昇し続けてしまうため今回の格ゲー制作には不都合です。
そのためmove1,action1を一回挟んで特定の要素がTrueだったらそれに対応した行動をFighterクラスの各関数でそれぞれ処理をするようにしました。

```python
elif event.type == KEYUP:
    if event.key == K_c:
        player_move[1] = False

    if event.key == K_q:
        player_move[3] = False

    if event.key == K_e:
        player_move[2] = False

    if event.key == K_u:
        action[0] = False

    if event.key == K_i:
        action[1] = False

    if event.key == K_o:
        action[2] = False

    if event.key == K_p:
        action[3] = False
```
上記のコードでは押しているキーを離したときにTrueだった要素をFalseに変換するコードです。先ほどの話でいくとmove, actionのどれかの要素がTrueの場合対応した行動をとるためその行動をキャンセルするときはFalseに戻さないといけません。なので長押ししているキーを離した時にキャンセルされるようにしたのが上のコードになります。

ここまで進むと以下の画像のようにキャラが表示されかつ動かすことができる。
キャラ表示：
![キャラ表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/898f4a51-dd70-5917-8541-4c73e6aa2c97.png)

キャラを左方向へ移動：
![左に移動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/240822fb-131b-2fdb-b75f-2b4c8e2e2022.png)

今回はここまで次回は今のところ敵キャラを表示してその敵キャラに攻撃できる処理を書いていこうかなと考えています。
説明が分かりにくい箇所があったかもしれませんがここまで読んでいただきありがとうございました。
では、また

[pygameを用いて簡単な格闘ゲームを自作してみた3](https://qiita.com/kankitu_man/items/b62a4f8fb3b58ad08cec)