Pyxel 迷路ゲームの作成2.キャラクターの歩き動作

Pyxel 迷路ゲームの作成(キャラクターのアニメーション)

 Python向けレトロゲームエンジン「Pyxel(ピクセル)」を使ってのゲーム作成を通してプログラミングを学習します。

 次の2点を大きな目標にして,迷路ゲームを作成する例を紹介します。
 ・キャラクターが上下左右に歩くアニメーションを作る
 ・タイルマップの迷路を自動生成する

 
 迷路ゲームの作成1の記事(クラスを使わないソースコードです)
 Pyxel パソコン実習用資料(迷路ゲームの作成) - 勉強ボックス管理者ブログ

 

作りたいゲームの内容

 上から見たタイプの迷路で,敵キャラクターを避けてゴールを目指すゲームを作ります。
 

キャラクターのアニメーションの実験

 歩き動作のアニメーションを,パラパラアニメで表現する例です。ドット絵を用意して,プログラムで表示を切り替えます。

 動作を2枚の絵で表すものと3枚の絵で表すものの2種類のパターン
 
 

歩きアニメーション確認用のコード

import pyxel
pyxel.init(80, 48,display_scale=5,capture_scale=5)
pyxel.load("walk.pyxres")

ANIMA_WALK_2 = [
    [(32,0),(48,0)],
    [(0,0),(16,0)],
    [(0,16),(16,16)],
    [(32,16),(48,16)]
    ]

v = 48
ANIMA_WALK_4 = [
    [(0,v+0),(16,v+0),(0,v+0),(32,v+0)],
    [(0,v+16),(16,v+16),(0,v+16),(32,v+16)],
    [(0,v+32),(16,v+32),(0,v+32),(32,v+32)],
    [(0,v+48),(16,v+48),(0,v+48),(32,v+48)]
    ]

def update():
    pass

def draw():
    pyxel.cls(1)

    x = y = 2
    for i in range(4):
        u,v = ANIMA_WALK_2[i][pyxel.frame_count//8 % 2]
        pyxel.blt(x,y, 0, u,v, 16,16,2)
        x += 20

    x = 2
    y = 24
    for i in range(4):
        u,v = ANIMA_WALK_4[i][pyxel.frame_count//6 % 4]
        pyxel.blt(x,y, 0, u,v, 16,16,2)
        x += 20    

pyxel.run(update,draw)

実行結果

・ANIMA_WALK_xのリストに各方向の1サイクルで表示する絵の座標を設定しています。
 [0] 上方向で表示する絵の座標リスト
 [1] 下方向で表示する絵の座標リスト
 [2] 左方向で表示する絵の座標リスト
 [3] 右方向で表示する絵の座標リスト

・絵が3枚のパターンでは,0→1→0→2 のように足が真下にある絵をはさんで1サイクルにしています。

 

迷路ゲームの作成

 本記事で作成するゲームのソースファイルとリソースファイルは下記を参照してください。
 Googleドライブ 迷路ゲーム02 - Google ドライブ
 最終版ソース game/pyxel/maze_random at main · benkyoubox/game · GitHub

 

リソースファイルの内容

 今回のゲームでは,歩き動作に3枚の絵を使うパターンで作成します。
 イメージバンク(0)
 
 イメージバンク(1)・・・壁タイルに使用します
 
 拡大図・・・左上から3列4行のドット絵で歩き動作にします。
 

 

01 移動するときにだけ歩く動作を行う

 Playerクラスを作成し,上下左右にキャラクターを移動させてみましょう。
 先ほどの歩く例ではPyxelのフレームカウントでアニメーションパターンを進めましたが,方向キーが押されたときにだけカウントアップするようにしてみます。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

player.py

import pyxel

DIR_UP = 0
DIR_DOWN = 1
DIR_LEFT = 2
DIR_RIGHT = 3

MOT_WAIT = 0
MOT_WALK = 1

ANIMA_WALK = [
    [(0,0),(16,0),(0,0),(32,0)],
    [(0,16),(16,16),(0,16),(32,16)],
    [(0,32),(16,32),(0,32),(32,32)],
    [(0,48),(16,48),(0,48),(32,48)]
    ]
SPEED = 3

class Player:
    ''' プレイヤーキャラクター '''

    def __init__(self,x,y):
        self.setpos(x,y)
        self.w = 16
        self.h = 16
        return

    def setpos(self,x,y):
        ''' スタート位置設定 '''
        self.x = x
        self.y = y        
        self.dir = DIR_DOWN
        self.flmcnt = 0
        self.motion = MOT_WAIT
        
    def setwalk(self,dir):
        self.dir = dir
        self.flmcnt += 1
        self.motion = MOT_WALK
        
    def move(self):
        ''' 座標移動 '''
        dx = dy = 0
        
        if pyxel.btn(pyxel.KEY_UP):
            self.setwalk(DIR_UP)
            dy = -SPEED
        if pyxel.btn(pyxel.KEY_DOWN):
            self.setwalk(DIR_DOWN)
            dy = SPEED
        if pyxel.btn(pyxel.KEY_LEFT):
            self.setwalk(DIR_LEFT)
            dx = -SPEED
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.setwalk(DIR_RIGHT)
            dx = SPEED
            
        # キャラクターの移動 暫定版
        self.x += dx
        self.y += dy

        return

    def update(self):
        if MOT_WAIT == self.motion or MOT_WALK ==  self.motion:
            self.move()
        
        return

    def getImg(self):
        u,v = (0,0)
        if MOT_WAIT == self.motion :
            u,v = (0,16)
        elif MOT_WALK ==  self.motion :
            u,v = ANIMA_WALK[self.dir][self.flmcnt//6 % 4]

        return u,v

    def draw(self):
        u,v = self.getImg()
        pyxel.blt(self.x,self.y, 0, u,v, self.w,self.h,2)

        return

# テストコード
if __name__ == '__main__':
    pyxel.init(128, 128)
    pyxel.load("alice.pyxres")
    pl = Player(10,10)

    def update():
        pl.update()

    def draw():
        pyxel.cls(1)
        pyxel.text(1,1,"Player Test",7)
        pl.draw()

    pyxel.run(update,draw)

・Playerクラス
 xy座標の他に,向きと動作,アニメーション用のカウンタを保持します。インスタンス作成後はupdate()メソッドとdraw()メソッドが繰り返し呼び出されるようにします。

・self.dir と DIR_UP ~ DIR_RIGHT
 現在のキャラクターの向きを self.dir に設定して,リストのインデックスに使用できるようにしています。

・self.motion と MOT_WAIT,MOT_WALK
 現在のキャラクターの動作を self.motion に設定して,処理を分岐させます。

・dx = dy = 0 多重代入式
 move()メソッド内で,代入演算子「=」で変数dxとdyと値を1行で記述しています。dyに0を代入した式の値がdxに代入され,dxも0になります。この式で複数の変数を同じ値で初期化することができます。

・テストコード
 Playerクラスの動作確認用のコードです。「if __name__ == '__main__':」のブロックを利用して,player.py を実行したときに動くコードを記述しています。

 
実行結果
 

 

02 ゴール時の動作追加

 迷路のゴール到達時に,キャラクターが喜ぶ動作を追加します。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

player.py 抜粋

・・・
MOT_WAIT = 0
MOT_WALK = 1
MOT_GOAL = 2
・・・

ANIMA_GOAL = [(0,80),(0,16)]
・・・

・ゴール用のモーションの番号とアニメーションパターンの座標を追加します。(両手上げ,両手下げ)

class Player:
・・・
    def goal(self):
        ''' ゴール到達 '''
        self.motion = MOT_GOAL
        self.flmcnt = 0
        return

    def update(self):
        if MOT_WAIT == self.motion or MOT_WALK ==  self.motion:
            self.move()
        elif MOT_GOAL == self.motion:
            self.flmcnt += 1        
        return

    def getImg(self):
        u,v = (0,0)
        if MOT_WAIT == self.motion :
            u,v = (0,16)
        elif MOT_WALK ==  self.motion :
            u,v = ANIMA_WALK[self.dir][self.flmcnt//6 % 4]
        elif MOT_GOAL == self.motion:
            u,v = ANIMA_GOAL[self.flmcnt//8 % 2]
        return u,v

    def draw(self):
        u,v = self.getImg()
        if MOT_GOAL == self.motion:
            y = self.y - 1 + self.flmcnt//8 % 2
            pyxel.blt(self.x,y, 0, u,v, self.w,self.h,2)
        else:
            pyxel.blt(self.x,self.y, 0, u,v, self.w,self.h,2)

        return

・goal()メソッド呼び出し時にモーションを変更します。setpos()メソッドが呼び出されるまで動作が継続するつくりになっていますが,時間で終了させる場合はupdate()メソッド内でカウンタを判定してモーションをMOT_WALKに戻せばよいでしょう。
・draw()メソッドでゴールモーションの場合はy座標の値を変化させています。(両手上げの絵のとき上に1ドットずらして,ジャンプしているように見せるため)

# テストコード
if __name__ == '__main__':
    pyxel.init(128, 128)
    pyxel.load("alice.pyxres")
    pl = Player(10,10)

    def update():
        if pyxel.btnp(pyxel.KEY_G):
            if MOT_GOAL != pl.motion :
                pl.goal()
            else:
                pl.setpos(pl.x,pl.y)

        pl.update()
・・・

・テストコードにGキー押下で確認できるようにしました。もう一度押されたらsetpos()メソッドでモーションを止めます。
 
実行結果
 

 

03 壁判定追加

 キャラクターが壁タイルを通らないように判定処理を追加します。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

player.py 抜粋

・・・
TILE_WALL = (1,0)
chkpoint = [(0,5),(7,5),(15,5),
            (0,7),      (15,7),
            (0,15),(7,15),(15,15)]
def chkwall(cx,cy):
    c = 0
    for cpx,cpy in chkpoint:
        xi = (cx + cpx)//8
        yi = (cy + cpy)//8
        if TILE_WALL == pyxel.tilemap(0).pget(xi,yi):
            c += 1
    return c

・キャラクター(16ドット)がタイル(8ドット)より大きいので,四隅の他に中間点も判定しています。(上部の判定は内側になるようにしました。ドット絵の内容に合わせて座標を変更してください)

class Player:
・・・
    def move(self):
 ・・・
        # キャラクターの移動
        lr = pyxel.sgn(dx)
        loop = abs(dx)
        while 0 < loop:
            if 0 != chkwall(self.x+lr,self.y):
                dx = 0
                break
            self.x += lr
            loop -= 1

        ud = pyxel.sgn(dy)
        loop = abs(dy)
        while 0 < loop:
            if 0 != chkwall(self.x,self.y+ud):
                dy = 0
                break
            self.y += ud
            loop -= 1

        return
・・・

・横方向と縦方向それぞれ,移動量分1ドットずつ壁がないか判定しながら進めます。(変数のlrはleftとright,udはupとdownの頭文字です。pyxelのsgn()関数で-1か1が設定されます)

# テストコード
if __name__ == '__main__':
    pyxel.init(128, 128)
    pyxel.load("alice.pyxres")
    pyxel.tilemap(0).refimg = 1
    pyxel.tilemap(0).pset(5,5,TILE_WALL)
    pl = Player(10,10)

    def update():
        if pyxel.btnp(pyxel.KEY_G):
            if MOT_GOAL != pl.motion :
                pl.goal()
            else:
                pl.setpos(pl.x,pl.y)

        pl.update()

    def draw():
        pyxel.cls(1)
        pyxel.bltm(0,0, 0, 0,0, pyxel.width,pyxel.height, 0)
        pyxel.text(1,1,"Player Test",7)
        pl.draw()

    pyxel.run(update,draw)

・テストコードでタイルマップの参照イメージバンク指定と,壁タイル配置を行っていますが,リソースファイルをエディタで編集している場合は不要なコードです。
 
実行結果
 

 

04 【参考】攻撃モーション

 歩き動作,ゴール到達動作の他にキャラクターの攻撃の動作を追加してみます。複数の絵を組み合わせて動作を作る参考にしてください。
 ※あくまでも見た目だけの実験です。実際のアクションゲームにする場合は,剣を(弾クラスのような)当たり判定を持つオブジェクトにして作ることになると思います。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

player.py 抜粋

・・・
MOT_ATTACK = 3
・・・
ANIMA_ATTACK = [
    [(48,0),(0,0)],
    [(48,16),(0,16)],
    [(48,32),(0,32)],
    [(48,48),(0,48)]
    ]
・・・
class Player:
・・・
    def move(self):
        ・・・
        if pyxel.btn(pyxel.KEY_RIGHT):
            self.setwalk(DIR_RIGHT)
            dx = SPEED

        if pyxel.btnp(pyxel.KEY_A):
            dx = dy = 0
            self.motion = MOT_ATTACK
            self.flmcnt = 0
・・・
    def update(self):
        if MOT_WAIT == self.motion or MOT_WALK ==  self.motion:
            self.move()
        elif MOT_GOAL == self.motion:
            self.flmcnt += 1
        elif MOT_ATTACK == self.motion:
            self.flmcnt += 1
            if 19 < self.flmcnt:
                self.motion = MOT_WALK
                self.flmcnt = 0
        return

    def getImg(self):
        u,v = (0,0)
        if MOT_WAIT == self.motion :
            u,v = (0,16)
        elif MOT_WALK ==  self.motion :
            u,v = ANIMA_WALK[self.dir][self.flmcnt//6 % 4]
        elif MOT_GOAL == self.motion:
            u,v = ANIMA_GOAL[self.flmcnt//8 % 2]
        elif MOT_ATTACK == self.motion:
            u,v = ANIMA_ATTACK[self.dir][self.flmcnt//10 % 2]
        return u,v

    def test_attack(self,u,v):
        if 0 < self.flmcnt < 10:
            x = self.x
            y = self.y
            
            if DIR_UP == self.dir:
                x -= 2
                y -= 14
            elif DIR_DOWN == self.dir:
                x += 2
                y += 13
            elif DIR_LEFT == self.dir:
                x -= 13
            elif DIR_RIGHT == self.dir:
                x += 13
            
            pyxel.blt(x,y, 0, u+self.w,v, 16,16,2)
        return

    def draw(self):
        u,v = self.getImg()
        if MOT_GOAL == self.motion:
            y = self.y - 1 + self.flmcnt//8 % 2
            pyxel.blt(self.x,y, 0, u,v, self.w,self.h,2)
        else:
            pyxel.blt(self.x,self.y, 0, u,v, self.w,self.h,2)

        if MOT_ATTACK == self.motion:
            self.test_attack(u,v)

        return
・・・

実行結果
 
・キャラクター位置とずらしてフラミンゴの絵を表示させています。作成するゲームに合わせて剣や軌跡の絵にしてみましょう。


 次の記事
kinutani.hateblo.jp

関連記事

kinutani.hateblo.jp