見出し画像

Pythonでマイクラを作る ⑥プレイヤーを動かす

Pythonでマイクラを作る 第6回目です。前回の記事でマイクラブロックを設置、破壊することができるようになりました。今回はプレイヤーを画面上に表示して動かせるようにします。プレイヤーの名前ですが、スティーブだとマイクラと同じになってしまうので、「パンダさん(仮)」としておきます。(良い名前が思い付いたら、コメント欄で教えてくださいねー)
では始めましょう。

プレイヤーモデルを表示する

 ディレクトリ構造
Documents/
  ├ pynecrafter/
  │  ├ images/
  │  │  ├ 48-488312_blockcss-minecraft-terrain-png-1-0-0.py
  │  │  
  │  ├ textures/
  │  │  ├ 0-1.png
  │  │  ├ 0-2.png
  │  │  
  │  ├ models/
  │  │  ├ grass_block.egg
  │  │  ├ stone.egg
  │  │  ├ dirt.egg
  │  │  
  │  ├ src/
  │  │  ├ __init__.py
  │  │  ├ block.py  # ブロック関連
  │  │  ├ mc.py  # 統合クラス
  │  │  ├ player.py  # プレイヤー関連
  │  │  
  │  ├ 01_01_showbase.py
  │  ├ 01_02_showbase.py
  │  ├ 01_03_showbase.py
  │  ├ xxx.py
  │  ├ 05_01_main.py
  │  ├ 06_01_main.py  # 統合クラスをインポートしてゲームを起動する

ディレクトリ構造は上図のようになります。
srcディレクトリに Playerクラスを定義する player.py を追加してください。それからsrcディレクトリと同じ階層に 06_01_main.py を追加します。今回追加するのはこの2つです。

"""src/__init__.py"""
from .block import Block
from .player import Player
from .mc import MC

__init__.py は、srcディレクトリ内のモジュールを呼び出す初期化処理を行います。player.py を追加したので、from .player import Player を追記してください。このとき、インポートする順番に注意が必要です。MCクラスをインポートするに Playerクラスをインポートしないとエラーになってゲームを実行できません。

"""src/player.py"""
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock


class Player:
    # コンストラクタ
    def __init__(self, base):
        self.base = base
        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)

        self.base.player_node = self.base.render.attachNewNode(PandaNode('player_node'))
        self.player_model = self.base.loader.loadModel('models/panda')
        self.player_model.setH(180)
        self.player_model.reparentTo(self.base.player_node)

Playerクラスはプレイヤー「パンダさん」の表示、回転、移動を操作するクラスです。インスタンス変数は base(MCインスタンス)、position(位置情報)、direction(向き)、そして velocity(移動速度)を持ちます。インスタンス変数の値は、特殊な型を使用しています。
VBase3、Vec3、Point3 の説明をします。

  • VBase3 --- float型(小数)の値を3つ持つ基本の型

  • Vec3 --- VBase3 を継承した型(ベクト専用)

  • Point3 --- VBase3 を継承した型(位置ベクトル専用)

Panda3D において、3次元空間の位置や色などを扱う専用の型が用意されています。Python のタプルとの違いは、型同士の足し算、引き算、スカラー倍にするなどの計算が簡単に行えることです。3Dゲームにおいては、これらの計算を多用しますので、タプルではなく、専用の型を使うことに慣れておきましょう。
本プロジェクトでは、RGB色、方向はVBase3型、速度はVec3型、位置はPoint3型を使用します。

        self.base.player_node = self.base.render.attachNewNode(PandaNode('player_node'))
        self.player_model = self.base.loader.loadModel('models/panda')
        self.player_model.setH(180)
        self.player_model.reparentTo(self.base.player_node)

プレイヤーモデルは直接render に表示するのではなく、player_nodeノードを挟んで表示します。player_nodeノードは、次回以降でカメラや体の各パーツなどを配置させていきますが、今回は簡単にパンダのモデルだけを配置させています。
Y軸方向を正面にするため、self.player_model.setH(180)を実行し、Z軸中心で モデルを180度回転させました。

"""src/mc.py"""
from direct.showbase.ShowBase import ShowBase
from panda3d.core import *
from . import *


class MC(ShowBase):
    def __init__(self, ground_size=128):
        # ShowBaseを継承する
        ShowBase.__init__(self)

        # ウインドウの設定
        self.properties = WindowProperties()
        self.properties.setTitle('Pynecrafter')
        self.properties.setSize(1200, 800)
        self.win.requestProperties(self.properties)
        self.setBackgroundColor(0, 1, 1)

        # マウス操作を禁止
        self.disableMouse()
        # カメラの設定
        self.camera.setPos(60, -150, 90)
        # self.camera.setPos(30, -75, 45)
        self.camera.lookAt(0, 0, 0)

        # ブロック
        self.block = Block(self, ground_size)

        # プレイヤー
        self.player = Player(self)

    def get(self, var):
        try:
            return getattr(self, var)
        except AttributeError:
            return None

    def set(self, var, val):
        setattr(self, var, val)

mcクラスは、全てのクラスを統合するクラスです。
self.player = Player(self) により、Playerインスタンスを作成します。これでプレイヤーを表示する準備ができました。

"""06_01_main.py"""
from math import *
from src import MC


class Game(MC):
    def __init__(self):
        # MCを継承する
        MC.__init__(self)

        # 座標軸
        self.axis = self.loader.loadModel('models/zup-axis')
        self.axis.setPos(0, 0, 0)
        self.axis.setScale(1.5)
        self.axis.reparentTo(self.render)


game = Game()
game.run()
06_01_main.py

06_01_main.py はMCクラスを読み込んでゲームを起動します。05_01_main.py と全く同じコードですから、打ち込むのが面倒ならコピペしてください。実行すると、Panda3D のデフォルトのモデルであるパンダが表示できました。パンダが Y軸方法を向いているか確認してください。
次は「パンダさん」を動かします。まずはマウス操作でプレイヤーの向きを変更する方法を学びます。

プレイヤーの向きを動かす

"""src/player.py"""
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock


class Player:
    heading_angular_velocity = 100
    pitch_angular_velocity = 50
    max_pitch_angle = 30

    # コンストラクタ
    def __init__(self, base):
        self.base = base
        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)

        self.base.player_node = self.base.render.attachNewNode(PandaNode('player_node'))
        self.player_model = self.base.loader.loadModel('models/panda')
        self.player_model.setH(180)
        self.player_model.reparentTo(self.base.player_node)

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

    def update_direction(self):
        if self.base.mouseWatcherNode.hasMouse():
            dt = globalClock.getDt()
            mouse_pos = self.base.mouseWatcherNode.getMouse()
            x = mouse_pos.x
            y = mouse_pos.y
            heading = self.direction.x
            pitch = self.direction.y
            if x < -0.1 or 0.1 < x:
                heading -= x * Player.heading_angular_velocity * dt
            if y < -0.1 or 0.1 < y:
                pitch += y * Player.pitch_angular_velocity * dt
            if pitch < -Player.max_pitch_angle:
                pitch = -Player.max_pitch_angle
            elif pitch > Player.max_pitch_angle:
                pitch = Player.max_pitch_angle
            self.direction = VBase3(heading, pitch, 0)

    def draw(self):
        self.base.player_node.setH(self.direction.x)
        self.base.player_node.setP(self.direction.y)

    def player_update(self, task):
        self.update_direction()
        # self.update_position()
        self.draw()
        return task.cont

ユーザーのマウス操作でプレイヤーの角度を変更できるようにしましょう。

ゲームを起動すると、MCクラスのコンストラクタが1回だけ実行され、そこで動きがストップしてしまいます。ゲームを進行するために taskMgr(タスクマネージャー)クラスを使います。

self.base.taskMgr.add(self.player_update, "player_update")

taskMgr.addメソッド は1フレームごとに実行するメソッドを指定します。。Playerクラスの player_updateメソッドを指定しました。

ゲームや動画では、短時間で画面を描画更新を繰り返して、画像が動いているように錯覚させています。この画面のことをフレームと言います。1秒間に更新する回数をフレームレートと言います。
フレームレート 60 FPS のとき、画面は平均で 1/ 60 秒ごとに更新されています。

    def player_update(self, task):
        self.update_direction()
        self.draw()
        return task.cont

player_updateメソッドは、プレイヤーの動きを操作します。update_directionメソッドと drawメソッドを実行します。第2引数の task は taskMgr からメソッドを呼び出すときに必要な値(タスク引数)です。メソッドの最後で return task.cont とすることで、task を継続(CONTinue)することができます。

def update_direction(self):
    if self.base.mouseWatcherNode.hasMouse():
        dt = globalClock.getDt()
        mouse_pos = self.base.mouseWatcherNode.getMouse()
        x = mouse_pos.x
        y = mouse_pos.y
        heading = self.direction.x
        pitch = self.direction.y
        print(x, y)
        if x < -0.1 or 0.1 < x:
            heading -= x * Player.heading_angular_velocity * dt
        if y < -0.1 or 0.1 < y:
            pitch += y * Player.pitch_angular_velocity * dt
        if pitch < -Player.max_pitch_angle:
            pitch = -Player.max_pitch_angle
        elif pitch > Player.max_pitch_angle:
            pitch = Player.max_pitch_angle
        self.direction = VBase3(heading, pitch, 0)

update_directionメソッドで、インスタンス変数direction(ユーザーの向き)を再計算します。
条件式 if self.base.mouseWatcherNode.hasMouse(): によりマウスが画面上にあるかを確認して、画面上にあるときは mouse_pos = self.base.mouseWatcherNode.getMouse() により画面上の座標を読み取ります。座標値 mouse_pos.x, mouse_pos.y ともに -1 から 1の間の小数値を取ります。つまり画面中央にマウスがあるときは (0, 0)、右上にあるときは (1, 1) になります。
dt = globalClock.getDt() は前回タスクの実行から経過した時間(秒)を表します。この秒数はデバイスの処理能力や描画内容によって大きく変化します。どのような条件でも、なるべく同じ動きをさせたい時に変数dt を使います。

        if x < -0.1 or 0.1 < x:
            heading -= x * Player.heading_angular_velocity * dt
        if y < -0.1 or 0.1 < y:
            pitch += y * Player.pitch_angular_velocity * dt

マウスポインターが中心より 10 % 以上離れた時に、heading角度、pitch角度が更新されます。位置ぎめを簡単にするため、中心部分は反応しないようにしました。

回転角度

heading角度の回転について詳しく見てみます。Player.heading_angular_velocity は、クラス定義文 class Player: 直下で定義されているクラス変数を読み取ります。x(マウスの位置)と Player.heading_angular_velocity(角速度)と dt(時間)とを掛け算することで回転角度が計算されます。つまりマウスの中心からの距離が大きいほど、回転角度が大きくなります。

def draw(self):
    self.base.player_node.setH(self.direction.x)
    self.base.player_node.setP(self.direction.y)
heading角度とpitch角度

drawメソッドはプレイヤーを再表示します。setHメソッドはZ軸中心の回転角度を、setPメソッドはX軸中心の回転角度を指定します。上図を参照してください。

パンダさんの回転運動

06_01_main.py を実行しすると、パンダさんをマウス操作で回転させることができます。パンダさんをクルクルさせて遊んでください。次は平行移動を実装します。

プレイヤーの位置を動かす

"""src/player.py"""
from math import *
from panda3d.core import *
from direct.showbase.ShowBaseGlobal import globalClock


class Player:
    heading_angular_velocity = 100
    pitch_angular_velocity = 50
    max_pitch_angle = 30
    speed = 10

    # コンストラクタ
    def __init__(self, base):
        self.base = base
        self.position = Point3(0, 0, 0)
        self.direction = VBase3(0, 0, 0)
        self.velocity = Vec3(0, 0, 0)

        self.base.player_node = self.base.render.attachNewNode(PandaNode('player_node'))
        self.player_model = self.base.loader.loadModel('models/panda')
        self.player_model.setH(180)
        self.player_model.reparentTo(self.base.player_node)

        # キー操作を保存
        self.key_map = {
            'w': 0,
            'a': 0,
            's': 0,
            'd': 0,
        }

        # ユーザー操作
        self.base.accept('w', self.update_key_map, ["w", 1])
        self.base.accept('a', self.update_key_map, ["a", 1])
        self.base.accept('s', self.update_key_map, ["s", 1])
        self.base.accept('d', self.update_key_map, ["d", 1])
        self.base.accept('w-up', self.update_key_map, ["w", 0])
        self.base.accept('a-up', self.update_key_map, ["a", 0])
        self.base.accept('s-up', self.update_key_map, ["s", 0])
        self.base.accept('d-up', self.update_key_map, ["d", 0])

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

    def update_direction(self):
        if self.base.mouseWatcherNode.hasMouse():
            dt = globalClock.getDt()
            mouse_pos = self.base.mouseWatcherNode.getMouse()
            x = mouse_pos.x
            y = mouse_pos.y
            heading = self.direction.x
            pitch = self.direction.y
            if x < -0.1 or 0.1 < x:
                heading -= x * Player.heading_angular_velocity * dt
            if y < -0.1 or 0.1 < y:
                pitch += y * Player.pitch_angular_velocity * dt
            if pitch < -Player.max_pitch_angle:
                pitch = -Player.max_pitch_angle
            elif pitch > Player.max_pitch_angle:
                pitch = Player.max_pitch_angle
            self.direction = VBase3(heading, pitch, 0)

    def update_key_map(self, key_name, key_state):
        self.key_map[key_name] = key_state

    def update_velocity(self):
        key_map = self.key_map

        if key_map['w'] or key_map['a'] or key_map['s'] or key_map['d']:
            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.velocity = Vec3(0, 0, 0)

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

    def draw(self):
        self.base.player_node.setH(self.direction.x)
        self.base.player_node.setP(self.direction.y)
        self.base.player_node.setPos(self.position)

    def player_update(self, task):
        self.update_direction()
        self.update_position()
        self.draw()
        return task.cont

上記のコードが Playerクラスの今回の完成形です。プレイヤーの表示、回転、移動の操作を行うことができます。追加したコードを一つずつ説明していきます。

        # キー操作を保存
        self.key_map = {
            'w': 0,
            'a': 0,
            's': 0,
            'd': 0,
        }

多くのゲームと同じように WASD キーでプレイヤーを前後左右に動かします。2つのキーを同時押しで斜め移動できるように、インスタンス変数key_map を導入しました。key_map は辞書であり、キーはキーの名前(w, a, s, d)です。は 0(False)または 1 (True) でキーが押されているかどうか、保存できます。

        # ユーザーのキー操作
        self.base.accept('w', self.update_key_map, ["w", 1])
        ....
        self.base.accept('w-up', self.update_key_map, ["w", 0])
        ....

acceptメソッドは、ユーザーのキー操作を受け付けることができるようにします。このメソッドもフレームごとに1回実行されます。
第1引数はキーの名前、第2引数が実行するメソッド名、第3引数はメソッドにわたす引数をリストで与えることができます。キーの名前に '-up' を付けるとキーが押されていない状態を表します。

    def update_key_map(self, key_name, key_state):
        self.key_map[key_name] = key_state

update_key_mapメソッドは、インスタンス変数key_map に現在にキーの状態を保存します。

    def update_velocity(self):
        key_map = self.key_map

        if key_map['w'] or key_map['a'] or key_map['s'] or key_map['d']:
            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.velocity = Vec3(0, 0, 0)

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

update_velocityメソッドは、key_mapを読み取って、プレイヤーの平行移動の速度を計算します。キーを2つ押すと、斜めに移動する速度が発生します。クラス変数 Player.speed の数値を大きく(小さく)すると、移動速度を大きく(小さく)できます。

移動距離と方向

updata_positionメソッドで、プレイヤーの現在位置を計算します。dt = globalClock.getDt() は前回タスク実行からの経過時間(秒)です。self.velocity(速度)と dt(時間)を掛け算することで移動距離と方向が得られますから、前回位置と足し算して、現在位置が得られます。

        self.position = self.position + self.velocity * dt

これらのベクトル計算が簡単に行えるのは、Panda3D の型である Vec3 や Point3 を使用しているからです。self.velocity(速度ベクトル)に時間をかけたり、self.position(位置ベクトル)と足し合わせたりする計算が、通常の四則計算と同じように実行できるのです。

    def draw(self):
        self.base.player_node.setH(self.direction.x)
        self.base.player_node.setP(self.direction.y)
        self.base.player_node.setPos(self.position)

    def player_update(self, task):
        self.update_direction()
        self.update_position()
        self.draw()
        return task.cont

drawメソッドに、self.base.player_node.setPos(self.position) を追記しました。プレイヤーをplayer_nodeノードごと、移動して表示します。
player_updateメソッドに、self.update_position() を追記して完成です。

06_01_main.py を実行すると、パンダさんを並行移動させることができます。パンダさんを自由に動かして遊んでください。

今回はプレイヤーの移動について見てきました。ユーザー操作でプレイヤーを動かすことができると、プレイヤーを動かすことができるようになるとゲームらしくなってきます。
次回は、プレイヤーのモデルを自分で作ったモデルに変更します。ブロックを6つ(頭、体、両手足)組み合わせて、動きのあるモデルを作ります。お楽しみに。


前の記事
Pythonでマイクラを作る ⑤ブロックの設置と破壊
次の記事
Pythonでマイクラを作る ⑦プレイヤーモデルを作成

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


いいなと思ったら応援しよう!