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

# 今回の目標
[前回](https://qiita.com/kankitu_man/items/4a19d48a34508a8ed7ed)は自分が操作するキャラを作成、描画し左右の移動とジャンプができるようになりました。そして今回目指すゴールは敵キャラの表示と自分の操作キャラで敵キャラを攻撃しダメージを与えることです。
また、諸事情により前回とソースコードに多少の違いがあります。

# 敵キャラの表示と攻撃判定の処理、攻撃が当たった時の判定処理

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

```python:fighter.py
import pygame

import main
from game_parts.caluclation_parts import pythagoras_theorem as pyth


class Fighter:
    def __init__(self, position, size, gravity, move_speed, contact, rigit_time):
        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
        self.rigit_time = rigit_time

    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, enemy_pos, enemy_size):
        """
        操作キャラとステージ(または敵キャラ)との接触判定、各接触方向に対して接触した場合のみCONTACTの各接触方向の要素にTrueを返す
        enemy_pos : 敵キャラの位置
        enemy_size : 敵キャラのサイズ

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

        """

        if self.pos_x == 1000 - self.width or (enemy_pos[0] <= self.pos_x + self.width <= enemy_pos[0] + enemy_size[0] and self.pos_y == enemy_pos[1]):
            self.contact[0] = True if 550 <= self.pos_y + self.height else False
            self.contact[2] = True

        elif self.pos_x == 0 or (enemy_pos[0] <= self.pos_x <= enemy_pos[0] + enemy_size[0] and self.pos_y == enemy_pos[1]):
            self.contact[0] = True if 550 <= self.pos_y + self.height else False
            self.contact[3] = True

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

        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]

    def character_action(self, player_move, action, hit_judg, enemy_pos, enemy_size, enemy_damage):
        """
        敵プレイヤーを攻撃した際の判定
        Parameters
        ----------
        player_move : (list) 操作キャラの移動判定
        action : (list) 操作キャラの動作判定
        hit_judg : (list) 自分の攻撃と攻撃対象との当たり判定
        enemy_pos: (list) 攻撃対象キャラの位置
        enemy_size : (list) 攻撃対象キャラの幅と高さ
        enemy_damage : (int) 敵に与えるダメージ

        Returns
        -------

        """
        enemy_rigit = blow_speed = 0

        if self.rigit_time != 0:
            self.rigit_time -= 1
            return action, self.rigit_time, hit_judg, enemy_damage, enemy_rigit, blow_speed

        if action[0]:
            circle_pos = (self.pos_x, self.pos_y + self.height // 2)
            radius = 30
            pygame.draw.circle(main.SURFACE, (0, 0, 250), circle_pos, radius)
            distance = pyth(circle_pos[0], enemy_pos[0] + enemy_size[0] / 2, circle_pos[1], enemy_pos[1] + enemy_size[1] / 2)
            if 0 <= distance <= radius + enemy_size[1] / 2:
                hit_judg = [False, False, False, True]
                enemy_damage += 13
                enemy_rigit = 10
                self.rigit_time = 15
                blow_speed = 20

        action[0] = False

        return action, self.rigit_time, hit_judg, enemy_damage, enemy_rigit, blow_speed

    def hit_action(self, hit_judg, rigit_time, blow_speed):
        """
        相手の攻撃を受けた際に操作キャラの吹っ飛ぶ方向と距離を制御
        Parameters
        ----------
        hit_judg : (list) 上下左右のどちらの方向に飛ぶかの判定
        rigit_time : (int) 操作キャラの硬直時間
        blow_speed : (int) 攻撃が当たったときに吹っ飛ぶスピード

        Returns
        -------

        """
        if rigit_time == 0:
            hit_judg = [False, False, False, False]
            return [self.pos_x, self.pos_y], rigit_time, hit_judg

        if hit_judg[3] is True and self.contact[3] is False:
            self.pos_x -= blow_speed

        rigit_time -= 1

        return [self.pos_x, self.pos_y], rigit_time, hit_judg

    def life(self, damage, view):
        """
        キャラクターの体力ゲージ
        Parameters
        ----------
        damage : ダメージの蓄積量
        view : 体力ゲージの表示場所(left or right)

        """
        if view == "left":
            pygame.draw.rect(main.SURFACE, (120, 120, 120), (30, 20, 400, 30))
            if damage >= 390:
                return

            pygame.draw.rect(main.SURFACE, (250, 200, 0), (35 + damage, 25, 390 - damage, 20))

        elif view == "right":
            pygame.draw.rect(main.SURFACE, (120, 120, 120), (570, 20, 400, 30))
            if damage >= 390:
                return

            pygame.draw.rect(main.SURFACE, (250, 200, 0), (575, 25, 390 - damage, 20))
```
以下fighter.pyの解説

今回作成する格闘ゲームでは硬直時間の処理をいれていこうと思います。硬直時間とは例えば攻撃した時や相手から攻撃を受けた時に発生する何も操作ができない時間のことです。硬直時間が無いと一方的に攻撃を与えることができたりしてしまうので面白くないからです。

```python:fighter.Fighter.character_action
if self.rigit_time != 0:
    self.rigit_time -= 1
    return action, self.rigit_time, hit_judg, enemy_damage, enemy_rigit, blow_speed
```
上記のコードでは攻撃をしたり、攻撃を受けたりした際に硬直時間が加算されその硬直時間が0になるまで次の行動ができないという処理です。

```python:fighter.Fighter.character_action
if action[0]:
    circle_pos = (self.pos_x, self.pos_y + self.height // 2)
    radius = 30
    pygame.draw.circle(main.SURFACE, (0, 0, 250), circle_pos, radius)
    distance = pyth(circle_pos[0], enemy_pos[0] + enemy_size[0] / 2, circle_pos[1], enemy_pos[1] + enemy_size[1] / 2)
    if 0 <= distance <= radius + enemy_size[1] / 2:
        hit_judg = [False, False, False, True]
        enemy_damage += 13
        enemy_rigit = 10
        self.rigit_time = 15
        blow_speed = 20

action[0] = False

return action, self.rigit_time, hit_judg, enemy_damage, enemy_rigit, blow_speed
```
上記の部分は自分のキャラで弱攻撃(左方向)をした際の当たり判定、敵に与えるダメージ量と硬直時間、攻撃技を出してから次行動するまでの自分のキャラの硬直時間、敵の吹っ飛ぶスピードを設定しています。
また、攻撃の判定は円で設定しており、円の中心と敵キャラの中心との距離を計算する際に使用しているpyth(pythagoras_theorem関数)はcaluclation_parts.pyファイルで作成しています。このファイルの解説は後にします。
**action[0] = False**では基本的に一回のボタン入力で発生する当たり判定は一回だけにしたいので攻撃ボタンを押しっぱなしにしても連続で同じ技が出ることはありません。
また上記のコードで注意して欲しいのは**pygame.draw.circle()**のcircle_posの値は必ず整数を使用することです。そのため**self.height // 2**とすることで2で割った値を整数で返すようにしています。

```python:fighter.Fighter.hit_action
if rigit_time == 0:
    hit_judg = [False, False, False, False]
    return [self.pos_x, self.pos_y], rigit_time, hit_judg
```
上記の部分では硬直時間が0の時は何もせず相手からの攻撃をもらったかどうかの判定を初期化している

```python:fighter.Fighter.hit_action
if hit_judg[3] is True and self.contact[3] is False:
    self.pos_x -= blow_speed

rigit_time -= 1

return [self.pos_x, self.pos_y], rigit_time, hit_judg
```
上記の部分では硬直時間が設定されている間、相手の攻撃を受けていてかつプレイヤーから見て左側に障害物がない場合、左方向に吹っ飛ぶ処理になっています。

```python:fighter.Fighter.life
if view == "left":
    pygame.draw.rect(main.SURFACE, (120, 120, 120), (30, 20, 400, 30))
    if damage >= 390:
        return

    pygame.draw.rect(main.SURFACE, (250, 200, 0), (35 + damage, 25, 390 - damage, 20))

elif view == "right":
    pygame.draw.rect(main.SURFACE, (120, 120, 120), (570, 20, 400, 30))
    if damage >= 390:
        return

    pygame.draw.rect(main.SURFACE, (250, 200, 0), (575, 25, 390 - damage, 20))
```
上記のコードでは自分の操作キャラと敵キャラのライフを表示しています。
左か右のどちらのライフを減らすかによって処理の仕方が若干変わります。
具体的には画面左側のライフバーの場合、ライフの減り方は左側から減っていくpygameのrect関数の仕様により図形の左上角のx座標を受けたダメージ分減らし、ライフバーの横幅をダメージ分短くする必要がある。
逆に画面右側のライフバーの場合、減り方は右側から減っていくため図形の横幅だけをダメージ分短くするだけです。とても簡単ですね。

```python:caluclation_parts
import math


def pythagoras_theorem(a1, a2, b1, b2):
    z_2 = (a2 - a1) ** 2 + (b2 - b1) ** 2
    z = math.sqrt(z_2)

    return z
```
上記のコードは攻撃の当たり判定である円の中心の座標と敵キャラの中心の座標との距離をピタゴラスの定理を使って求めています。もしかしたらpythonのモジュールで上記のような処理をするものがあるのかもしれませんが見つけられなかったため自作してみました。

```python:main.py
import pygame

from game_parts import control, fighter, stage

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


def main():
    player1_pos = [600, 470]  # <= 操作キャラの位置
    player1_size = [50, 80]  # <= 操作キャラの大きさ [横幅, 縦幅]
    move1 = [False, False, False, False]  # <= [上方向に移動、下方向に移動、右方向に移動、左方向に移動]
    jump_speed1 = 20  # <= ジャンプの初速度
    contact1 = [False, False, False, False]  # <= [キャラクターと地面との接触判定、頭上のオブジェクトとの接触判定、右側とオブジェクトとの接触判定、左側とオブジェクトの接触判定
    action1 = [False, False, False, False]  # <= [弱攻撃、必殺技、ガード、掴み]
    hit_judg1 = [False, False, False, False]  # <= [上方向にhit、下方向にhit、右方向にhit、左方向にhit]
    damage1 = 0  # <= 操作キャラが受けたトータルダメージ量
    rigit_time1 = 0  # <= 操作キャラの硬直時間
    blow_speed1 = 0  # <= 操作キャラが吹っ飛ばされたときの速度
    player2_pos = [200, 470]
    player2_size = [50, 80]
    move2 = [False, False, False, False]
    jump_speed2 = 20
    contact2 = [False, False, False, False]
    hit_judg2 = [False, False, False, False]
    damage2 = 0
    rigit_time2 = 0
    blow_speed2 = 0

    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, rigit_time1)
        player_1.figure()
        player1_pos, jump_speed1, move1 = player_1.move(jump_speed1, move1)
        contact1 = player_1.contact_judgment(player2_pos, player2_size)
        player1_pos = player_1.position_corr(STAGE_POS)
        action1, rigit_time1, hit_judg2, damage2, rigit_time2, blow_speed2 = player_1.character_action(move1, action1, hit_judg2, player2_pos, player2_size, damage2)
        player_1.life(damage1, "right")

        # プレイヤー2のキャラ表示
        player_2 = fighter.Fighter(player2_pos, player2_size, 16, 10, contact2, rigit_time2)
        player_2.figure()
        player2_pos, jump_speed2, move2 = player_2.move(jump_speed2, move2)
        contact2 = player_2.contact_judgment(player1_pos, player1_size)
        player2_pos = player_2.position_corr(STAGE_POS)
        player2_pos, rigit_time2, hit_judg2 = player_2.hit_action(hit_judg2, rigit_time2, blow_speed2)
        player_2.life(damage2, "left")

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

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


if __name__ == '__main__':
    main()
```
最後にmain.pyのコードを載せます。

```python
hit_judg1 = [False, False, False, False]  # <= [上方向にhit、下方向にhit、右方向にhit、左方向にhit]
damage1 = 0  # <= 操作キャラが受けたトータルダメージ量
rigit_time1 = 0  # <= 操作キャラの硬直時間
blow_speed1 = 0  # <= 操作キャラが吹っ飛ばされたときの速度
player2_pos = [200, 470]
player2_size = [50, 80]
move2 = [False, False, False, False]
jump_speed2 = 20
contact2 = [False, False, False, False]
hit_judg2 = [False, False, False, False]
damage2 = 0
rigit_time2 = 0
blow_speed2 = 0
```
main関数内で共有して扱う変数を追加、操作キャラのhit判定、ダメージ値の変数に加え
敵キャラの場所や大きさ等のplayer1のために用意した変数と同じ種類の変数をplayer2用に用意。

```python
action1, rigit_time1, hit_judg2, damage2, rigit_time2, blow_speed2 = player_1.character_action(move1, action1, hit_judg2, player2_pos, player2_size, damage2)
player_1.life(damage1, "right")

# プレイヤー2のキャラ表示
player_2 = fighter.Fighter(player2_pos, player2_size, 16, 10, contact2, rigit_time2)
player_2.figure()
player2_pos, jump_speed2, move2 = player_2.move(jump_speed2, move2)
contact2 = player_2.contact_judgment(player1_pos, player1_size)
player2_pos = player_2.position_corr(STAGE_POS)
player2_pos, rigit_time2, hit_judg2 = player_2.hit_action(hit_judg2, rigit_time2, blow_speed2)
player_2.life(damage2, "left")
```
以上のコードは今まで紹介したfighter.pyの各関数を操作キャラ、敵キャラそれぞれに対して使用しています。

# ゲーム起動、実際に動かしてみた
<img width="998" alt="game_start.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/e845395d-22b0-ab13-3214-c56b6b8911fc.png">

<img width="1000" alt="contact.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/4a87b7c6-7b19-936c-a8d8-ac146a54af71.png">

<img width="998" alt="hit.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515048/879ed0be-de04-7225-bc0d-b6aaab8be27a.png">

今回はここまで、操作しているキャラと敵キャラが長方形だとやっぱり味気ないですよね〜
なので次回は自作した格闘ゲーム用のキャラを表示し動かしていきたいと思います。
また、本記事は格闘ゲームを作りながら書いているので次回の投稿まで結構時間がかかります。
ではまた
[次回 pygameを用いて簡単な格闘ゲームを自作してみた4(まだ制作途中)]()