見出し画像

pyxelゲーム制作TIPS_見下ろし形RPG風

はじめに

このnoteはpythonのレトロゲームエンジン「pyxel」でのゲーム制作に役立つかもしれない小さなサンプルプログラムのTIPSです。
以下の環境で作成・動作させています。
・OS:Windows11
・開発環境:Visual Studio Code(Ver.1.88.1)
・pyxelのバージョン:2.0.12

記事内のコードはご自由にお使いください。イメージ等もファイルは添付していませんが、模写(?)して使っていただいて構いません。
また、pyxelの動作環境や各関数の詳細な説明はpyxel公式GitHubをご参照ください。

見下ろし形RPGって?

昔懐かしい見下ろし形RPG。ゼルダの伝説の初期や携帯機ではこの形態の作品が多かった気がします。ちょっと毛色は違うけど、風来のシレンとか。ドラゴンクエスト・ファイナルファンタジーなんかもバトルシーン以外は「見下ろし形」と言っていいんじゃないでしょうか。
マップを描画し、キャラクターを動かす。今回の記事ではここまでを目標としますが、それでも達成感はなかなかのもの。この骨組みさえできれば、あとは戦闘だったりイベントだったり、思うがままに付け足していけばいいのです。

作っていきましょう

それでは始めましょう。
まずはpyxelアプリケーションの基本的な型を書いてしまいます。これだけ書けばとりあえず画面は表示されますから、おいおい足していきましょう。

import pyxel


class App:
    def __init__(self):
        #ここで起動時の処理をします
        pyxel.init(128, 128)        
        pyxel.run(self.update, self.draw)

    def update(self):
        #ここで毎フレームの更新作業をします
        pass

    def draw(self):
        #ここで毎フレームの描画作業をします
        pyxel.cls(0)
        pyxel.text(40, 60, "Hello, Pyxel!", pyxel.frame_count % 16)        


App()

今回は128*128の画面サイズで作ります。実行するとビカビカ光る文字が表示されるはずです。処理の順番としては毎フレーム「update→draw」の関数が実行されます。

さて、ここまで出来たら早速要素を追加していきます。
まずは「マップの表示」です。
Pyxel Editorを起動してタイルマップを作成しましょう。

▲イメージバンク2にタイルマップ用の画像を作成。
簡単な感じで土と草、岩です。
▲タイルマップエディタに切り替えてマップ作製。
周りは岩で囲って、なんか建物っぽいのも配置。草は気分で。

さあ、マップができました。pyxelのタイルマップは1マスが8*8でできています。これが上の画面だと16*16マスあります。という事はこのマップを表示するのには16*8=128、縦横128ドット必要になります。なので今回は128*128の画面サイズにしたわけですね。

このマップを表示させるためにコードに以下の記述を付け足します。

    def __init__(self):
        #ここで起動時の処理をします                                
        pyxel.init(128, 128)        
        pyxel.load('./sample01.pyxres')             
        pyxel.run(self.update, self.draw)

まずはpyxel.loadで作成したpyxresを読み込みます。ファイルパスやファイル名は適宜変更してください。

    def draw(self):
        #ここで毎フレームの描画作業をします
        pyxel.cls(0)
        pyxel.bltm(0, 0, 0, 0, 0, 128, 128)       

draw関数にpyxel.bltmを追加してタイルマップを描画させます。実行してみましょう。

▲タイルマップが表示されました。

良い感じですね。次はここにキャラクターを配置してみましょう。まずはPyxel Editorで画像を作ります。1マス8*8なので同じサイズで作りましょう。
サイズがあっていればどんなキャラクターでも大丈夫です。

▲猫だか犬だかわからない何かを作成。

では画面上に表示させてみます。今回のマップは1マスが8*8、つまり8ドット区切りになっています。キャラクターを配置する際は座標が8の倍数になるようにするときれいに収まるはずです。

    def draw(self):
        #ここで毎フレームの描画作業をします
        pyxel.cls(0)        
        pyxel.bltm(0, 0, 0, 0, 0, 128, 128)       
        pyxel.blt(16, 16, 0, 0, 0, 8, 8, 14)

draw関数にpyxel.bltを追加します。今回最後の引数に「14」を指定しましたが、これは透過させる色の指定です。上の画像で背景色にしているピンク?っぽい色を透過させています。
また、書かれた命令は上から実行されていくので、マップの描画の前にキャラクターの描画命令を書いてしまうと、上からマップで塗りつぶされてしまいます。順番には注意しましょう。
では実行してみましょう。

▲左上あたりにキャラクターが描画されました。

ゲームっぽさがでてきました。ついでだからキーボードで動かせるようにしちゃいましょう。
動かせるようにするという事は、キャラクターの座標を可変にするという事。まずはそのための変数を用意します。

    def __init__(self):
        #ここで起動時の処理をします                                
        pyxel.init(128, 128)        
        pyxel.load('./sample01.pyxres')    
        self.player_pos = [16, 16]         
        pyxel.run(self.update, self.draw)

self.player_posという変数を作成しました。リスト型でキャラクターのx座標・y座標を格納します。
では次にキーボードを押した際にこの値が変わるようにしましょう。

    def update(self):
        #ここで毎フレームの更新作業をします
        #コントロール部分###################
        if pyxel.btnp(pyxel.KEY_RIGHT):
            self.player_pos[0] += 8
            
        elif pyxel.btnp(pyxel.KEY_LEFT):
            self.player_pos[0] -= 8

        elif pyxel.btnp(pyxel.KEY_UP):
            self.player_pos[1] -= 8

        elif pyxel.btnp(pyxel.KEY_DOWN):
            self.player_pos[1] += 8

if pyxel.btnp(pyxel.キー名):でキーボードの押下判定ができます。今回は上下左右のキーで判定します。それぞれ「右ボタンが押されたらx座標を+8」「左ボタンが押されたらx座標を-8」という風に作っていきます。
上下のy座標は注意が必要で、pyxelでは下がに行くほどy座標が大きくなります。そのため「上ボタンが押されたらy座標を-8」「下ボタンが押されたらy座標を+8」というようにキー操作と逆になります。

最後に現在は決め打ちになっているキャラクターの描画位置を変数を使う形に改良しましょう。

    def draw(self):
        #ここで毎フレームの描画作業をします
        pyxel.cls(0)        
        pyxel.bltm(0, 0, 0, 0, 0, 128, 128)       
        pyxel.blt(self.player_pos[0], self.player_pos[1], 0, 0, 0, 8, 8, 14)

pyxel.bltで指定している座標をself.player_posから取得するようにしました。これでupdate関数での変更を描画時に反映させることができます。

▲ぐりぐり動いています。

ただ、現状では障害物の当たり判定がないので岩のマスも通過してしまいます。これではマップというよりただの画像なので、当たり判定を追加していきましょう。

各キーのコントロール部分に当たり判定のチェックロジックを付けてもいいのですが、こういったいろいろなところで使いそうなものは関数化すると便利です。

    def move_check(self, x, y):
        if pyxel.tilemaps[0].pget(x//8, y//8) == (0, 1):
            return False
        else:
            return True

move_check関数を作りました。呼び出す際の引数としてxとyがあり、その座標が岩であればFalse、それ以外はTrueを返します。タイルマップの種類の取得にはpyxel.tilemaps[t].pgetの命令を使いますが、タイルマップは8ドットで1マスなので実際の座標を8で割ることでタイルマップのどのマスかを特定できます。先ほどの例だと座標は[16, 16]なので8で割って[2, 2]のマスという事になります。

それではこの関数をキーコントロールの部分で呼び出してみましょう。

    def update(self):
        #ここで毎フレームの更新作業をします
        #コントロール部分###################
        if pyxel.btnp(pyxel.KEY_RIGHT):
            if self.move_check(self.player_pos[0]+8, self.player_pos[1]):
                self.player_pos[0] += 8
            
        elif pyxel.btnp(pyxel.KEY_LEFT):
            if self.move_check(self.player_pos[0]-8, self.player_pos[1]):
                self.player_pos[0] -= 8

        elif pyxel.btnp(pyxel.KEY_UP):
            if self.move_check(self.player_pos[0], self.player_pos[1]-8):
                self.player_pos[1] -= 8

        elif pyxel.btnp(pyxel.KEY_DOWN):
            if self.move_check(self.player_pos[0], self.player_pos[1]+8):
                self.player_pos[1] += 8

if self.move_check(self.player_pos[0]+8, self.player_pos[1]):のような形で呼び出します。これでmove_check関数がTrueを返したときのみ、移行の処理が流れます。チェックするマスは「キャラクターが今いるマス」ではなく「これから移動しようとしているマス」である点を注意してください。

▲きちんと岩のマスは進めなくなりました!

さあこれで「マップの描画」「キャラクターの描画」「キャラクターの移動」ができました。これをベースに何を作りましょうか?

・キャラクターが1つというのは寂しいのでNPCを増やす。
→移動の判定とかは今回作ったものが参考になりそうです。
・もっと大きなマップを作ってみる。
→マップを大きくするとスクロールの問題が出てきます。pyxel.cameraの命令が役に立つかもしれません。
・移動先のマスによって挙動を変えてみる。
→pyxel.tilemaps[t].pgetでマスの種類が分かるのは上記の通りです。マスによってイベントを設定したりするのも良いですね。

ぜひ色々と試していただければと思います。個人的には広いマップというのはやはりロマンですね。

最後にすべてつなげたソースを載せて終わりにしようと思います。

import pyxel


class App:
    def __init__(self):
        #ここで起動時の処理をします                                
        pyxel.init(128, 128)        
        pyxel.load('./sample01.pyxres')    
        self.player_pos = [16, 16]         
        pyxel.run(self.update, self.draw)

    def update(self):
        #ここで毎フレームの更新作業をします
        #コントロール部分###################
        if pyxel.btnp(pyxel.KEY_RIGHT):
            if self.move_check(self.player_pos[0]+8, self.player_pos[1]):
                self.player_pos[0] += 8
            
        elif pyxel.btnp(pyxel.KEY_LEFT):
            if self.move_check(self.player_pos[0]-8, self.player_pos[1]):
                self.player_pos[0] -= 8

        elif pyxel.btnp(pyxel.KEY_UP):
            if self.move_check(self.player_pos[0], self.player_pos[1]-8):
                self.player_pos[1] -= 8

        elif pyxel.btnp(pyxel.KEY_DOWN):
            if self.move_check(self.player_pos[0], self.player_pos[1]+8):
                self.player_pos[1] += 8

    def move_check(self, x, y):
        if pyxel.tilemaps[0].pget(x//8, y//8) == (0, 1):
            return False
        else:
            return True

    def draw(self):
        #ここで毎フレームの描画作業をします
        pyxel.cls(0)        
        pyxel.bltm(0, 0, 0, 0, 0, 128, 128)       
        pyxel.blt(self.player_pos[0], self.player_pos[1], 0, 0, 0, 8, 8, 14)


App()

ここまで読んでいただきありがとうございました。

※有料エリアですが、特に何もありません。設定しているだけですので、お気に召しましたら購入いただけると嬉しいです。

ここから先は

28字

¥ 100

ここまで読んでいただきありがとうございます!