Python Pyxel でプログラミング練習 第3回(アクションゲーム 前編)

Pyxelでアクションゲームを作る

 Python向けレトロゲームエンジン「Pyxel(ピクセル)」を使ったゲーム制作を通してプログラミングを学習します。
 今回作成するゲームで,ゲーム画面を構成するタイルマップ,キャラクターの左右移動とジャンプの例を紹介します。また,Pythonで「タプル型」と呼ばれるデータの扱い方も学習します。

 (他のPyxel記事:Pyxelゲーム作成の記事一覧 - 勉強ボックス管理者ブログ

 

ゲームの内容を考える

 忍者を主人公にして,コイン(小判)を集めるゲームにします。キーボードで操作し,スペースキーが押されたらジャンプします。空中のブロック(床)に飛び乗る仕組みとして,足元に床がないと落下するつくりにしましょう。
 ゲーム画面はPyxelのタイルマップを使って作成します。
 

タイルマップ用画像の作成

 リソースファイルの名前を ninja.pyxres とします。
 PowerShellウィンドウで「pyxel edit ninja」を入力し,Pyxelのドット絵のエディタを起動してください。

 イメージバンクの左上(0,0)から16×16ドットの範囲に,ドット絵を作成します。
 

 
 16×16ドットを4つ(8×8ずつ)に分けた1区画が今回のゲームで使うタイルマップの単位になります。左上の空欄もキャラクターが自由に移動できる空間を表すタイルになります。ドット絵は好きなものに変更して問題ありませんが,説明の都合上「左上:空欄,右上:床,左下:右方向を向いているキャラクター,右下:得点アイテム」としてください。

 

タイルマップの作成

 エディタ上部のモード変更ボタンでタイルマップエディタに切り替えます。
 
 エディタ右側に,先ほど描いたドット絵のタイルが表示されるので,タイルを選んで左側の編集エリアに配置していきます。
 
 ※保存を忘れずに 

 タイルマップ作成後でも,イメージバンクのドット絵を修正するとタイルマップの表示にも変更が反映されます。プログラム作成後に見た目を変更することも簡単にできます。

 

プログラムの作成

 ソースコードを記述していきます。ファイル名は任意です(最後まで同じファイル名で問題ありません)。

 ※本記事では「ソースコードを書く」→「実行して確認」→「ソースコードに処理を追加」→「実行して確認」→・・・を繰り返します。各ステップでのソースコード変更部分は,下記ファイルに黄色ハイライトで示しましたので参考にしてください。
 Pyxel_アクションゲーム - Google ドライブ
 

01 タイルマップを表示する

 画面にタイルマップとプレーヤーキャラクターを表示してみましょう。
 list05_01.py

import pyxel
pyxel.init(128,128,title="NINJA")
pyxel.load("ninja.pyxres")

x = 8
y = 100
pldir = 1

def update():
    return

def draw():
    pyxel.cls(0)
    pyxel.bltm(0,0, 0, 0,0, pyxel.width,pyxel.height, 0)
    pyxel.blt( x, y, 0,  0, 8, pldir*8,8, 0)
    return

pyxel.run(update,draw)

 実行結果
 

・ pyxel.bltm(x, y, tm, u, v, w, h, [colkey])
 ゲーム画面の座標(x,y)の位置にタイルマップを指定したサイズで表示する命令です。
 tm 今回は0で指定しましたが,エディタで0-7のタイルマップを作成しておくことができます。
 u,v タイルマップの開始座標です。今回は0,0で左上を指定しています。
 w,h 表示するタイルマップの範囲です。今回は画面サイズと同じにしました。
   pyxel.width 画面の幅(init()命令で指定した幅です)
   pyxel.height 画面の高さ(init()命令で指定した高さです)
 colkey 透明色として扱う色をパレット番号で指定します。

・pyxel.blt( x, y, 0, 0, 8, pldir*8,8, 0)
 pldirの値が1のときは,イメージバンクの絵の向きで表示
 pldirの値が-1のときは,左右を反転して表示
 リソースは右向きのキャラクターの絵1枚にして,プログラムで左向きの絵にもできるようにする工夫です。

  

02 キャラクターを左右に動かす

 キーボード操作を受け付け,キャラクターを移動させる処理を作成します。基本は「左方向キーが押されたらx座標の値を減らす。右方向キーが押されたらx座標の値を増やす。」なので,まずはその形で追加してみましょう。

list05_02.py
前のプログラムからの変更箇所は 確認用ファイル を参照してください。

import pyxel
pyxel.init(128,128,title="NINJA")
pyxel.load("ninja.pyxres")

x = 8
y = 100
dx = 0
dy = 0
pldir = 1

def update():
    global x,y,dx,dy,pldir

    # 操作判定
    if pyxel.btn(pyxel.KEY_LEFT):
        dx = -2
        pldir = -1
    elif pyxel.btn(pyxel.KEY_RIGHT):
        dx = 2
        pldir = 1
    else:
        dx = 0

    # 横方向の移動
    x = x + dx
    
    return

def draw():
    pyxel.cls(0)
    pyxel.bltm(0,0, 0, 0,0, pyxel.width,pyxel.height, 0)
    pyxel.blt( x, y, 0,  0, 8, pldir*8,8, 0)
    return

pyxel.run(update,draw)

 実行結果
 
 x座標の変化量として変数dxを追加して,update()関数で操作に応じて値を変更しています。また,キャラクターの向きも変わるようにpldirの値も切り替えています。pldirには0を設定しないよう注意してください。

<2024-04-01追記>
 左右方向キー入力で直接x座標の値を変化させずに,変化量の変数dxを追加したことについての補足です。
 変数dxはこのフレームでの移動する向き(左が負,右が正)と大きさの値で,グローバル変数なので値を保持しておくことができます。
 list05_02.pyのソースコードでは,キー操作なし(操作判定のif文のelse節)の場合にはdxを0に設定していますが,後の改造でdxの値が徐々に小さくなるようにelse節の処理を変更します。else節の処理変更後は方向キーの入力が無くなったフレームでは少し小さくなったdxの値でx座標が更新されることになります。(キーを離した瞬間に止まるのではなく,少し移動が続く)
 変化量の変数に現在の移動する向きと大きさを保持しておくことで,操作がなくても移動し続けるキャラクターをつくることができるようになります。この考え方はピンポンゲームのボールの動きシューティングゲームの弾や敵キャラクターを動かすときにも利用できます。

 

03 タイルマップの判定

 キャラクターが入ることができない場所をタイルマップから読み取り,移動に制限をかけてみましょう。

 <Pyxelのタイルマップ>
 タイル(イメージバンクの絵を8×8ドットの単位で区切っている)は位置で区別され,タプル型の情報(tile_x, tile_y)を持っています。
 
 タイルマップに配置されたイメージ
 pyxelのpget()命令を使うとプログラムから指定したマップ位置のタイルが何か判別することができます。

 <タプル型>
 Pythonのオブジェクトで複数のデータを組み合わせて管理できるものです。
 例えば,緯度と経度を1つのタプル型変数に代入したり,名前と数学と英語の点数を組み合わせたタプル型からデータを取り出したりすることができます。

>>> pos = (35.63366931113252, 139.87988297941195)
>>> print(pos)
(35.63366931113252, 139.87988297941195)
>>> result = ("yamada", 80, 90)
>>> name,eng,math = result
>>> print(name,eng,math)
yamada 80 90

 今回のプログラムでは,タイルマップの情報とキャラクターの四隅の座標をタプル型で扱ってみましょう。

 <キャラクター移動時の判定>
 プレーヤーキャラクターが移動する先の四隅の位置のタイルを読み取って,通れる場所かどうか判定し,床や壁に入りこんでしまう場合は移動できないようにします。



list05_03.py
前のプログラムからの変更箇所は 確認用ファイル を参照してください。

import pyxel
pyxel.init(128,128,title="NINJA")
pyxel.load("ninja.pyxres")

x = 8
y = 85    # 動作確認のために位置を変更
dx = 0
dy = 0
pldir = 1

chkpoint = [(2,0),(6,0),(2,7),(6,7)]
def chkwall(cx,cy):
    c = 0
    if cx < 0 or pyxel.width -8 < cx:
        c = c + 1
    for cpx,cpy in chkpoint:
        xi = (cx + cpx)//8
        yi = (cy + cpy)//8
        if (1,0) == pyxel.tilemap(0).pget(xi,yi):
            c = c + 1
    return c

def update():
    global x,y,dx,dy,pldir

    # 操作判定
    if pyxel.btn(pyxel.KEY_LEFT):
        dx = -2
        pldir = -1
    elif pyxel.btn(pyxel.KEY_RIGHT):
        dx = 2
        pldir = 1
    else:
        dx = 0

    # 横方向の移動
    lr = pyxel.sgn(dx)
    loop = abs(dx)
    while 0 < loop :
        if chkwall( x + lr, y) != 0:
            dx = 0
            break
        x = x + lr
        loop = loop -1
    
    return

def draw():
    pyxel.cls(0)
    pyxel.bltm(0,0, 0, 0,0, pyxel.width,pyxel.height, 0)
    pyxel.blt( x, y, 0,  0, 8, pldir*8,8, 0)
    return

pyxel.run(update,draw)

 実行結果
 

・chkpoint = [(2,0),(6,0),(2,7),(6,7)]
 キャラクター四隅の座標をタプル型の配列で管理します。8×8ドットの四隅は「(0,0),(7,0),(07),(7,7)」ですが見た目と合わせるために少し内側の座標にしています。

・chkwall()関数
 引数で調べたい座標(移動先)を指定すると,移動可能の場合は0,移動不可の場合は1以上を返す関数です。
 戻り値は0で初期化して,移動先が画面外になる場合は戻り値を加算。
 移動先にキャラクターを描画したときの四隅の座標分判定を繰り返しています。
 配列(リスト)の長さ分処理を行いたいときはforループで記述すると便利です。whileループで記述すると下記のようになります。

    i = 0
    while i < 4:
        cpx,cpy = chkpoint[i]
        xi = (cx + cpx)//8
        yi = (cy + cpy)//8
        if (1,0) == pyxel.tilemap(0).pget(xi,yi):
            c = c + 1
        i = i + 1

・pyxel.tilemap(0).pget(xi,yi)
 pget()命令でマップの位置(1マスが8×8ドットなのでxy座標を8で切り捨て除算して算出)のタイルを取得します。タプル型同士の比較ができるので,ブロックのタイル(1,0)かどうか判定をしています。
 tile = pyxel.tilemap(0).pget(xi,yi) # 戻り値を変数に代入してから比較
 if (1,0) == tile :
  c = c + 1
 このように書くこともできます。壁や床のタイルの種類が増えたときは変数に入れてから比較した方が記述しやすいかと思います。

・横方向の移動
 pyxel.sgn()命令は,引数が正なら1,0なら0,負のとき-1を返します。
 x座標の移動量分,1ドットずつ移動先に進めるかどうか判定してからx座標の値を更新しています。進めない場合はbreak文で処理を打ち切ります。(break前の行のdxの初期化はここでは不要な処理なのですが,後の慣性制御のために入れてあります)
 abs()関数は,絶対値を取得するPythonの組み込み関数です。dxが-2のときもループ回数が2になります。


<2024-04-04追記>
複雑な処理なのでソースコードと説明を並べたスライドを作成しました。参考にしてください。

list05_03変更箇所の説明 - Google スライド

 
<慣性制御の追加>
 方向キーを押し続けたときに移動量が増えて,キーを離してもすぐには止まらないようにします。操作が若干難しくなりますが,アクションゲームらしくなります。

update()関数の変更内容 list05_03r.py
前のプログラムからの変更箇所は 確認用ファイル を参照してください。

    # 操作判定
    if pyxel.btn(pyxel.KEY_LEFT):
        # -3まで徐々に変化
        if -3 < dx:
            dx = dx - 1
        pldir = -1
    elif pyxel.btn(pyxel.KEY_RIGHT):
        # 3まで徐々に変化
        if dx < 3:
            dx = dx + 1
        pldir = 1
    else:
        dx = int(dx*0.7)    # 急には止まれない


 前編で横方向の動きを作りました。後編では縦方向(ジャンプ)の処理を作成します。



【参考文献】
 廣瀬 豪(2022)『7大ゲームの作り方を完全マスター! ゲームアルゴリズムまるごと図鑑』 技術評論社

 本記事は上記書籍の「第5章 横スクロールアクション」の内容を参考に作成しました。JavaScriptでのゲーム制作方法について解説された本ですが,他の言語やツールでの開発においてもとても役立つと思います。


【後編の記事】
 本記事(アクションゲームを作る)の続きです。kinutani.hateblo.jp