キャラクターの移動処理
Python向けレトロゲームエンジン「Pyxel(ピクセル)」の公式サンプルのプレイヤーキャラクターの移動処理を見てみましょう。前回の記事「Pyxel 背景の処理(タイルマップ) - 勉強ボックス管理者ブログ」と同じく 10_platformer.py の内容です。
・マップのある横スクロールアクションゲーム
10_platformer.py
assets/platformer.pyxres
【ソースコード】pyxel/10_platformer.py at main · kitao/pyxel · GitHub
【デモ】https://kitao.github.io/pyxel/wasm/examples/10_platformer.html
操作と移動の概要
左右の方向キー操作で横に移動し,スペースキーを押すとジャンプ(連続で押すと連続でジャンプ)します。
Playerクラス
ソースコードでどのように移動を実現しているか確認します。
01 変化量を管理する変数
キャラクターの座標の他に横方向と縦方向の「1フレームごとの変化量(以下,移動量と記載)」を管理する変数が使われています。
10_platformer.py抜粋
class Player: def __init__(self, x, y): self.x = x self.y = y self.dx = 0 self.dy = 0 self.direction = 1 self.is_falling = False
・キャラクターのxy座標と,横方向の移動量 dx,縦方向の移動量 dy,キャラクターの向き,落下状態を持っています。(変化量の変数にdが使われるのは物理や数学のデルタからだと思われます。変数名にpを付けるコード例もあります)
・移動量を管理する変数があると,前のフレームでの動きの続きが行えます。具体的には一度動いたらその方向に少し動き続ける慣性の制御や,放物線になるようなジャンプを表現できます。
02 移動先の判定
操作によりキャラクターを動かしますが,動いた先の地形が壁や床の場合はキャラクターが入れないようします。現在の座標から移動量に合わせて1ドットずつタイルを調べ,壁にぶつかった場合は入り込む直前の座標がキャラクターの座標になります。
class Player: ・・・ def update(self): global scroll_x last_y = self.y if pyxel.btn(pyxel.KEY_LEFT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_LEFT): self.dx = -2 self.direction = -1 if pyxel.btn(pyxel.KEY_RIGHT) or pyxel.btn(pyxel.GAMEPAD1_BUTTON_DPAD_RIGHT): self.dx = 2 self.direction = 1 self.dy = min(self.dy + 1, 3) if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.GAMEPAD1_BUTTON_A): self.dy = -6 pyxel.play(3, 8) self.x, self.y, self.dx, self.dy = push_back(self.x, self.y, self.dx, self.dy) if self.x < scroll_x: self.x = scroll_x if self.y < 0: self.y = 0 self.dx = int(self.dx * 0.8) self.is_falling = self.y > last_y if self.x > scroll_x + SCROLL_BORDER_X: last_scroll_x = scroll_x scroll_x = min(self.x - SCROLL_BORDER_X, 240 * 8) spawn_enemy(last_scroll_x + 128, scroll_x + 127) if self.y >= pyxel.height: game_over()
push_back()関数
def push_back(x, y, dx, dy): abs_dx = abs(dx) abs_dy = abs(dy) if abs_dx > abs_dy: sign = 1 if dx > 0 else -1 for _ in range(abs_dx): if detect_collision(x + sign, y, dy): break x += sign sign = 1 if dy > 0 else -1 for _ in range(abs_dy): if detect_collision(x, y + sign, dy): break y += sign else: sign = 1 if dy > 0 else -1 for _ in range(abs_dy): if detect_collision(x, y + sign, dy): break y += sign sign = 1 if dx > 0 else -1 for _ in range(abs_dx): if detect_collision(x + sign, y, dy): break x += sign return x, y, dx, dy
・push_back()内で detect_collision() を呼び出し,タイル判定を行っています。
detect_collision()関数
def detect_collision(x, y, dy): x1 = x // 8 y1 = y // 8 x2 = (x + 8 - 1) // 8 y2 = (y + 8 - 1) // 8 for yi in range(y1, y2 + 1): for xi in range(x1, x2 + 1): if get_tile(xi, yi)[0] >= WALL_TILE_X: return True if dy > 0 and y % 8 == 1: for xi in range(x1, x2 + 1): if get_tile(xi, y1 + 1) == TILE_FLOOR: return True return False
・キャラクターの大きさが8×8ドットなので,移動先の四隅(※)で壁タイル WALL_TILE_X を調べています。
※ The guide to implementing 2D platformers | Higher-Order Fun では移動先にキャラクターのコピーを作って重なりを調べるとあります。大きなキャラではキャラクターの大きさに合わせて調べる場所を追加しましょう。
・タイル情報は (tile_x,tile_y) のタプルですが,get_tile(xi, yi)[0] の [0] で要素 tile_x を指定して WALL_TILE_X と比較しています。
・床タイル TILE_FLOOR は下方向への移動時かつタイルの境界のときだけ,左下と右下の2カ所を調べています。
縦方向の移動は上昇と落下がありますが,縦方向の移動量 dy の値を変化させていくことでジャンプの動きを実現しています。
スペースキーを押して上方向への移動量を大きく設定した後は,徐々に移動量が小さくなってやがて下向きに逆転します。
・update()内の「self.is_falling = self.y > last_y」は一見奇妙な一行ですが,右辺の式を評価した結果(True/False)が左辺に代入されます。
スペースキーが押されなければ,常に下方向に3ドット移動の判定が行われて足元のタイルが壁や床ではない場合に落下が始まります。
03 キャラクター描画
キャラクターの状態(通常/落下状態)により,イメージバンクの参照座標を変更して動作アニメーションを行っています。またキャラクターの向きにより表示の左右反転も行い,操作と一致するようになっています。
class Player: ・・・ def draw(self): u = (2 if self.is_falling else pyxel.frame_count // 3 % 2) * 8 w = 8 if self.direction > 0 else -8 pyxel.blt(self.x, self.y, 0, u, 16, w, 8, TRANSPARENT_COLOR)
キャラクターの移動についての紹介は以上です。サンプルコードを参考にして,横スクロールアクションゲームを作ってみましょう。