見出し画像

Pythonでマイクラを作る ⑳プレイヤーのモーションを実装する

Pythonでマイクラを作る 第20回です。今回はプレイヤーのモーションを実装します。プレイヤーのモデルは、頭、体、両手両足の6つのブロックで構成されています。両手両足のブロックを適切に動かすことで歩行モーションを実現できます。
それから2段ジャンプ自動ジャンプを実装して、プレイヤーの操作性をアップします。操作性を向上させると、ユーザーの満足度もアップさせることができます。

歩行モーションの実装

Playerクラスは、プレイヤーを管理します。今回は Playerクラスを中心に改造を進めていきます。

インスタンス変数

"""src/player.py"""
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock
from .player_model import PlayerModel
from .camera import Camera
from .target import Target


class Player(PlayerModel, Camera, Target):
    heading_angular_velocity = 15000
    pitch_angular_velocity = 5000
    max_pitch_angle = 60
    speed = 10
    eye_height = 1.6
    gravity_force = 9.8
    jump_speed = 10

    # コンストラクタ
    def __init__(self, base):
        self.base = base
        PlayerModel.__init__(self)
        Camera.__init__(self)
        Target.__init__(self)

        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)
        self.mouse_pos_x = 0
        self.mouse_pos_y = 0
        self.target_position = None
        self.is_on_ground = True  # 地面に接している
        self.is_flying = False  # 空中に浮かんでいる
        self.is_walking = False  # 歩いている
        self.walking_count = 0  # 歩行のカウント  # (1)

歩行モーションは、「①右足前・左手上」「②左足前・右手上」の2つの状態を切り替えることで実現できます。これらの状態を管理するインスタンス変数 walking_count を追加します((1))。walking_count が偶数のとき、①に、奇数のとき、②にして、モデルが歩いているように見せかけます。

タスクマネージャー

# タスクマネージャーを追加

    # プレイヤーのアップデート
    base.taskMgr.add(self.player_update, "player_update")
    base.taskMgr.doMethodLater(0.5, self.update_motion, "update_motion")  # (1)

taskMgr(タスクマネージャー)クラスは、画面の1フレームごとに実行されるメソッドを指定できます。doMethodLaterメソッドは、指定した秒数(今回は 0.5秒)の後に実装されるメソッド(今回は update_motion)を指定できます(遅延実行のタスク)。これで、0.5秒ごとに update_motionメソッドを実行できます((1))。

メソッド

# メソッドを追加

    def update_motion(self, task):
        if self.is_walking:
            self.walking_count += 1
            if self.walking_count % 2:  # (1)
                self.base.player_right_hand_node.setP(70)  # (1)
                self.base.player_left_hand_node.setP(110)  # (1)
                self.base.player_right_leg_node.setP(20)  # (1)
                self.base.player_left_leg_node.setP(-20)  # (1)
            else:  # (1)
                self.base.player_right_hand_node.setP(110)  # (1)
                self.base.player_left_hand_node.setP(70)  # (1)
                self.base.player_right_leg_node.setP(-20)  # (1)
                self.base.player_left_leg_node.setP(20)  # (1)
        else:
            self.base.player_right_hand_node.setP(90)  # (2)
            self.base.player_left_hand_node.setP(90)  # (2)
            self.base.player_right_leg_node.setP(0)  # (2)
            self.base.player_left_leg_node.setP(0)  # (2)
        return task.again

update_motionメソッドは、プレイヤーのモーション(動作)を実行します。歩行モーションは、「①右足前、左手上」「②左足前、右手上」の2つの状態を切り替えて実現します。
歩行している(インスタンス変数 is_walking が True )のとき、両手両足のモデルを setPメソッドで X軸基準で回転させることで、プレイヤーが歩いているように見せることができます。条件式「if self.walking_count % 2:」でインスタンス変数 walking_count が偶数か、奇数であることを判定して、状態を切り替えます。((1))
歩行していない(インスタンス変数 is_walking が False )のとき、両手両足の位置を基準の位置に戻します。((2))

walking player

17_01_main.py を実行して、歩行モーションが実現できているかテストしてください。歩行中は、プレイヤーの状態が 0.5秒ごとに切り替わり、歩いているように見せることができるようになりました。

2段ジャンプの実装

2段ジャンプの実装は、Playerクラスの修正だけで実現できます。

ライブラリー

ライブラリーを追加

"""src/player.py"""
from time import time  # (1)
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock
from .player_model import PlayerModel
from .camera import Camera
from .target import Target

timeライブラリーから、timeメソッドをインポートします((1))。timeライブラリーは自国関連の機能を追加できます。

インスタンス変数

インスタンス変数を追加

class Player(PlayerModel, Camera, Target):
    heading_angular_velocity = 15000
    pitch_angular_velocity = 5000
    max_pitch_angle = 60
    speed = 10
    eye_height = 1.6
    gravity_force = 9.8
    jump_speed = 10

    # コンストラクタ
    def __init__(self, base):
        self.base = base
        PlayerModel.__init__(self)
        Camera.__init__(self)
        Target.__init__(self)

        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)
        self.mouse_pos_x = 0
        self.mouse_pos_y = 0
        self.target_position = None
        self.is_on_ground = True  # 地面に接している
        self.is_flying = False  # 空中に浮かんでいる
        self.is_walking = False  # 歩いている
        self.walking_count = 0  # 歩行のカウント
        self.jump_start_time = None  # ジャンプを開始した時間  # (1)
        self.is_double_jumping = False  # 2段ジャンプしている  # (1)

インスタンス変数を2つ追加します((1))。インスタンス変数 jump_start_time はジャンプ開始時の時間を保存します。インスタンス変数 is_double_jumping は、2段ジャンプしている状態を真偽値(True、または False)で保存します。

メソッド

メソッドを修正

    def update_velocity(self):
        key_map = self.key_map
        walk_sound = self.base.walk_sound

        if self.is_on_ground or self.is_flying:
            if key_map['w'] or key_map['a'] or key_map['s'] or key_map['d']:
                self.is_walking = True
                if walk_sound.status() is not walk_sound.PLAYING:
                    walk_sound.play()
                heading = self.direction.x
                if key_map['w'] and key_map['a']:
                    angle = 135
                elif key_map['a'] and key_map['s']:
                    angle = 225
                elif key_map['s'] and key_map['d']:
                    angle = 315
                elif key_map['d'] and key_map['w']:
                    angle = 45
                elif key_map['w']:
                    angle = 90
                elif key_map['a']:
                    angle = 180
                elif key_map['s']:
                    angle = 270
                else:  # key_map['d']
                    angle = 0
                self.velocity = \
                    Vec3(
                        cos(radians(angle + heading)),
                        sin(radians(angle + heading)),
                        0
                    ) * Player.speed
            else:
                self.is_walking = False
                if walk_sound.status() is walk_sound.PLAYING:
                    walk_sound.stop()
                self.velocity = Vec3(0, 0, 0)

            if key_map['space']:
                if self.is_on_ground:
                    self.base.jump_sound.play()
                self.is_on_ground = False
                self.is_flying = False
                self.jump_start_time = time()  # (1)
                self.velocity.setZ(Player.jump_speed)
        elif self.jump_start_time and time() - self.jump_start_time > 0.5:  # start (2)
            if key_map['space'] and not self.is_double_jumping:
                self.is_double_jumping = True
                self.base.jump_sound.play()
                self.velocity.setZ(Player.jump_speed)  # end (2)

update_velocityメソッドを修正します。
「スペース」キーが押された(if key_map['space']:)とき、「self.jump_start_time = time()」で、ジャンプの開始時間をインスタンス変数 jump_start_time に保存します。
(2)で、2段ジャンプを実行します。条件式「elif self.jump_start_time and time() - self.jump_start_time > 0.5:」は、1回目のジャンプが開始してから、0.5秒以上経っていることを判定します。この判定をしないと、1フレームごとに連続してジャンプしてしまうので、それを防止するためです。
そして、インスタンス変数 is_double_jump を True にして、「self.base.jump_sound.play()」でジャンプ音を再生します。「self.velocity.setZ(Player.jump_speed)」でプレイヤーの上向き(Z方向)の速度をクラス変数 jump_speed の大きさ増やします。これで2段ジャンプできるようになりました。

メソッドを追加

    def update_position(self):
        self.update_velocity()
        dt = globalClock.getDt()
        self.position = self.position + self.velocity * dt

        floor_height = self.base.block.get_floor_height() + 1

        # # ジャンプ中の位置情報を保存
        # self.record_jump_positions_with_time(dt, floor_height)

        key_map = self.key_map
        if key_map['arrow_up']:
            self.is_on_ground = False
            self.is_flying = True
            self.position.setZ(self.position.getZ() + Player.jump_speed * dt)
        elif key_map['arrow_down']:
            self.position.setZ(self.position.getZ() - Player.jump_speed * dt)
            if self.position.z <= floor_height:
                self.position.z = floor_height
                self.is_on_ground = True
                self.is_flying = False

        if not self.is_flying:
            if not self.is_on_ground:
                if self.position.z <= floor_height:
                    self.position.z = floor_height
                    self.is_on_ground = True
                    self.jump_start_time = None  # (1)
                    self.is_double_jumping = False  # (1)
                    self.base.landing_of_jump_sound.play()
                else:
                    self.velocity.setZ(self.velocity.getZ() - Player.gravity_force * dt)
            else:
                if floor_height < self.position.z:
                    self.is_on_ground = False
                    self.velocity.setZ(-Player.gravity_force * dt)

最後に、update_positionメソッドを修正します。
地上に浮いている状態(if not self.is_on_ground:)で、プレイヤーのZ方向の位置が着地点の高さ(floor_height)より低くなったとき、プレイヤーは着地したと判定できます。このとき、「self.jump_start_time = None」「self.is_double_jumping = False」の2行を追記して、プレイヤーのジャンプ状態を元に戻してやります。これで完成です。

double jump

17_01_main.py を実行して、プレイヤーが2段ジャンプできることを確認してください。うまく操作すれば、家の屋根に飛び乗ることができるようになりました。

自動ジャンプの実装

自動ジャンプ(オートジャンプ)は プレイヤーがブロックにぶつかったとき、自動でそのブロックに飛び乗る機能です。自動ジャンプを実装するために、Playerクラスと Blockクラスを修正します。

Blockクラスのメソッド

Blockクラスにメソッドを追加

    def get_floor_height_at(self, x, y):
        x, y = floor(x), floor(y)
        s = re.compile(f'{x}_{y}_.+')
        heights = []
        for key in self.block_dictionary:
            if s.search(key):  # (1)
                _, _, block_z = [int(value) for value in key.split('_')]  # (1)
                heights.append(block_z)  # (1)

        if len(heights):
            return max(heights)  # (2)
        else:
            return None  # (3)

Blockクラスに get_floor_height_atメソッドを追加します。
引数 x, y の位置にブロックがあるとき、リストheights にその Z座標の位置(z)を保存します((1))。そしてブロックがあるとき、z の最大値を返します(2)。もしブロックが見つからないときは None を返します((3))。

Playerクラスのメソッド

メソッドを修正

    def change_position_when_interfering_with_block(self):
        velocity_x, velocity_y, velocity_z = self.velocity
        x, y, z = self.position
        # X方向の干渉チェック
        if 0 < velocity_x:
            x_to_check = x + 0.5
            if self.base.block.is_block_at(Point3(x_to_check, y, z)) or \
                    self.base.block.is_block_at(Point3(x_to_check, y, z + 1)):
                floor_height = self.base.block.get_floor_height_at(x_to_check, y)
                if floor_height is not None and floor_height - z <= 1:
                    z = floor_height + 1
                else:
                    x = floor(x_to_check) - 1
        elif velocity_x < 0:
            x_to_check = x - 0.5
            if self.base.block.is_block_at(Point3(x_to_check, y, z)) or \
                    self.base.block.is_block_at(Point3(x_to_check, y, z + 1)):
                floor_height = self.base.block.get_floor_height_at(x_to_check, y)  # start (1)
                if floor_height is not None and floor_height - z <= 1:
                    z = floor_height + 1
                else:
                    x = floor(x_to_check) + 2  # end (1)
        # Y方向の 干渉チェック
        if 0 < velocity_y:
            y_to_check = y + 0.5
            if self.base.block.is_block_at(Point3(x, y_to_check, z)) or \
                    self.base.block.is_block_at(Point3(x, y_to_check, z + 1)):
                floor_height = self.base.block.get_floor_height_at(x, y_to_check)  # start (2)
                if floor_height is not None and floor_height - z <= 1:
                    z = floor_height + 1
                else:
                    y = floor(y_to_check) - 1  # end (2)
        elif velocity_y < 0:
            y_to_check = y - 0.5
            if self.base.block.is_block_at(Point3(x, y_to_check, z)) or \
                    self.base.block.is_block_at(Point3(x, y_to_check, z + 1)):
                floor_height = self.base.block.get_floor_height_at(x, y_to_check)  # start (3)
                if floor_height is not None and floor_height - z <= 1:
                    z = floor_height + 1
                else:
                    y = floor(y_to_check) + 2  # end (3)
        # Z方向の干渉チェック
        if 0 < velocity_z:
            z_to_check = z + 2
            if self.base.block.is_block_at(Point3(x, y, z_to_check)):  # start (4)
                z = floor(z_to_check) - 2
                self.velocity.setZ(0)
        self.position = Point3(x, y, z)  # end (4)

Playerクラスの change_position_when_interfering_with_blockメソッドを修正します。このメソッドは、プレイヤーが進む方向にブロックがあるとき、プレイヤーがそちらに進めなくするメソッドです。このメソッドに、自動ジャンプ機能を追加します。
修正は4箇所あります。全てやっていることは同じなので、(1) の部分だけ説明します。プレイヤーが進む方向にブロックがあったら、block.get_floor_height_atメソッドを使って、そのブロックの高さ(floor_height)を求ます。そのブロックの高さが、プレイヤーと同じか、1段上のとき、「z = floor_height + 1」によりプレイヤーはブロックに飛び乗ります。そうでないときは、「x = floor(x_to_check) - 1」によりプレイヤーをブロックに跳ね返らせます。

auto jump

15_01_main.py を実行して、階段ワールドを生成します。階段に歩いて行くと自動で階段を登ることができるようになっています。+X方向、-X方向、+Y方向、-Y方向全て、階段を登ることができたら、今回のミッションは完成です。

今回は歩行モーションと、2段ジャンプ、自動ジャンプの3つを実装しました。自作のゲームの良いところは、「こんな機能を追加したい」と思ったら、ソースコードのレベルで改造することができる点です。つまり「根本からゲームの世界を作り替えることができる」ということです。
読者の皆さんもゲームで遊んでいて、追加したい機能を思いついたら、ぜひご自分で実装してみてください。
また追加したい機能があればコメント欄でリクエストしてください。実装するとお約束はできませんが、今後の開発の参考にさせていただきます。

次回は、雲を実装します。「パーリン・ノイズ」を使った、地形の自動生成を研究したいと思います。お楽しみに。


前の記事
Pythonでマイクラを作る ⑲サウンドを実装する
次の記事
Pythonでマイクラを作る ㉑パーリンノイズで雲を実装する

その他のタイトルはこちら


この記事が気に入ったらサポートをしてみませんか?