Pyxelでゲーム制作を始めようと思ったとき、もしかすると人によっては1フレームごとにupdate()とdraw()を呼び出すという部分でまずちょっとしたつまづきを感じるかもしれません。
いやそんなことあるかいwww という方はこの記事は無用の長物です。
次のアドベントカレンダー記事に期待してページを閉じましょう!
update(), draw()って?
公式サイトの説明です。
Python スクリプト内で Pyxel モジュールをインポートし、init関数でウィンドウサイズを指定した後、run関数で Pyxel アプリケーションを開始します。
run関数の引数には、フレーム更新処理を行うupdate関数と、描画処理を行うdraw関数を指定します。
pyxel.run()が一度呼び出されると、以降「フレーム経過待ち→update関数→draw関数→フレーム経過待ち..」というループ処理が自動的に行われることになる、ということですね。
import pyxel
pyxel.init(160, 120)
def update():
if pyxel.btnp(pyxel.KEY_Q):
pyxel.quit()
def draw():
pyxel.cls(0)
pyxel.rect(10, 10, 20, 20, 11)
pyxel.run(update, draw)
update()にゲーム本体のロジック(操作受付や分岐、各種変数の更新などなど)を記述し、draw()では現在のゲームシーンに応じた画面の描画処理のみを行う、という使い方が一般的です。
別にそれが難しいわけではないのですが、もしかすると、たとえば昔BASICで初歩的なプログラムを書いていたようなおっさん(もしくはおねえさま)が恐る恐るPyxelに手を出そうとしたときに、「シンプルに上から実行していてって、プログラム実行中も『今はコードのこの部分だけでループしている』とイメージしやすい構造がよかったなぁ」と思うかもしれません。
というのは、実は数年前久しぶりにゲーム開発やってみようと思ったときの私の最初の感想なんですけどね!
んで、実はそういう組み方も可能、というお話です。
pyxel.flip()を使う
pyxelにはflip()という関数があり、公式では「画面を 1 フレーム更新します。Escを押すとアプリケーションは終了します。この関数は Web 版では動作しません。
」と説明されています。
これをwhileループ等に組み込むことで1フレーム(デフォルトでは1/30秒)の同期処理を行なってくれます。
flip()を使った短いプログラムを見てみましょう。たった10行のミニプログラムです。
import pyxel as px
px.init(240, 240, title="2Dフィールド移動デモ")
px.load("assets.pyxres")
(x, y, e, s) = (39 * 16, 39 * 16, 113 * 16, 8)
while True:
y += s * (px.btn(px.KEY_DOWN) - px.btn(px.KEY_UP))
x += s * (px.btn(px.KEY_RIGHT) - px.btn(px.KEY_LEFT))
y, x = max(0, min(y, e)), max(0, min(x, e))
px.bltm(0, 0, 0, x, y, 240, 240)
px.flip()
1行目:ライブラリ呼び出し
2行目:pyxel初期化
3行目:アセットファイルの読み込み
4行目:変数の設定
5行目〜:メインループ
6,7行目:カーソルキーに応じてx, y座標を変数sの値分動かす
8行目:xとyが画面の端を超えないように制御(eは画面の右端および下端)
9行目:pyxelのタイルマップ描画
10行目:フレーム同期
コードを短くするため、6~8行目だけちょっとテクニカルな書き方になっているかもしれません。
動作イメージは以下のようになります。
前提としてアセットファイルに以下のようにpyxelのルールに沿ったスプライトとタイルマップを用意しておく必要があります。
これによって、9行目だけでこの画面全体描画ができてしまうわけですね。
標準的な書き方を使う場合
ちなみにupdate()やdraw()を使い、かつ公式で推奨されているようにpyxel全体をクラスでラッピングした場合のソースコードはこんな感じです。
コードは10行から18行になりました。
import pyxel as px
class App:
def __init__(self):
px.init(240, 240, title="2Dフィールド移動デモ")
px.load("assets.pyxres")
(self.x, self.y, self.e, self.s) = (39 * 16, 39 * 16, 113 * 16, 8)
px.run(self.update, self.draw)
def update(self):
self.y += self.s * (px.btn(px.KEY_DOWN) - px.btn(px.KEY_UP))
self.x += self.s * (px.btn(px.KEY_RIGHT) - px.btn(px.KEY_LEFT))
self.y, self.x = max(0, min(self.y, self.e)), max(0, min(self.x, self.e))
def draw(self):
px.bltm(0, 0, 0, self.x, self.y, 240, 240)
App()
発展例:キャラクタを移動させる
せっかくなのでもう少し発展させて、キャラクタを歩かせてみます。
行数はコメント除くと19行ですが、古典的2DフィールドRPGの移動の基礎的なところは実現できています。
処理内容はコメントを参考にしてください。
ただあまりPyxel入門的な趣旨ではないので細かくは書いていないのと、先ほどのものと同様、短く収めることに主眼を置いているのでかなり汚いです(変数名が1〜2文字とか今日的ではないですしね)。
import pyxel as px
px.init(240, 240, title="2Dフィールド移動デモ改")
px.load("assets.pyxres")
(x, y, dx, dy, e, s) = (39 * 16, 39 * 16, 0, 0, 113 * 16, 4)
tm = px.tilemaps[0]
while True:
if x % 16 == 0 and y % 16 == 0:
dy = px.btn(px.KEY_DOWN) - px.btn(px.KEY_UP)
dx = px.btn(px.KEY_RIGHT) - px.btn(px.KEY_LEFT) if not dy else 0
# 移動先のタイルマップのタイルを(image_tx, image_ty)のタプルとして取得する
p = tm.pget((x + dx * 16 + 112) // 8, (y + dy * 16 + 112) // 8)
if p[1] < 4: # このアセットでは海のタイルがp[1]=0~3になっている
dx, dy = (0, 0)
y += s * dy
x += s * dx
# u, vは主人公のイメージマップ上の座標、フレームカウントに応じてアニメーションする
(u, v) = ((px.frame_count % 12) // 6 * 16, 5 * 16)
y, x = max(0, min(y, e)), max(0, min(x, e)) # 境界処理
px.bltm(0, 0, 0, x, y, 240, 240) # マップ
px.blt(112, 112, 0, u, v, 16, 16, 2) # 主人公
px.flip()
メリット・デメリット
このflip()方式が比較的適していると思われるケースは、プレイヤーの入力待ちが多いボードゲームや古のRPG、アドベンチャー、シミュレーション系のゲームです。
入力待ちをするときは入力待ちだけのループを作ることで、update()やdraw()のコード全体を意識せずに済みます。
一方でこのflip()方式には大きな欠点があります。
公式ドキュメントにある 「この関数は Web 版では動作しません
」 がそれです。
Pyxelでプログラムを作ったらSNSなどで公開して人に見て(遊んで)もらいたくなると思いますが、最もお手軽な手段であるWebでの共有がこのスタイルだと封じられてしまうんですよね。
なので、結局のとこと、使うとしてもあくまで最初の一歩の練習段階、もしくは明らかにWebでの公開は行わない(pyxappや実行可能形式でのみ配布する)と決めているケースに限定されると思いますが、今回は「とにかく短いコードでPyxelの魅力の一面を紹介したい!」という考えもあってこのテーマにしてみました。
それでは、よいPyxelライフを〜!