コントローラーを使おう – PICO-8ゲーム開発入門(4)
PICO-8でプログラミングを1から学ぶ連載、第4回です。前回はこちら。プログラミング経験者の方は、日本語マニュアルを読んで、すぐに創作にとりかかってください。作った作品は、ぜひ掲示板へ。
コントローラーの使い方
前回、スプライトの表示とアニメーションの方法を学びました。そろそろゲームらしく、インタラクティブにしましょう。コントローラーを使ってキャラクターを動かしてみます。
PICO-8の仮想コントローラーには、十字キーとO(マル)ボタン、X(バツ)ボタンの2ボタンの、計6ボタンあります。このファミコンのような(実際にはファミコンよりもボタンの少ない)コントローラーが、最大で8つまで使えます。PCにゲームパッドをたくさん繋げて設定すれば、8人同時プレイのゲームを作ることができます(例: zepさんが作った、『Froggleoid』。カエルがジャンプして、ハエ食べ競争するゲームです。)。
ゲームパッドが無くても大丈夫です。仮想コントローラーのボタンは、PCのキーボードに割り当てられています。以下のとおりです。
プレイヤー1
←: 矢印左
→: 矢印右
↑: 矢印上
↓: 矢印下
O: Z or C or N
X: X or V or M
プレイヤー2
←: S
→: F
↑: E
↓: D
O: 左Shift or Tab
X: A or Q
(※プレイヤー3以降は、ゲームパッドの接続など特別な設定が必要。)
PCがあれば、2人用ゲームまでは遊ぶことができるというわけです。あるいは、考え方を変えれば、6+6=12個のボタンを使用するゲームを作ることもできます。
コントローラーの入力を読み取る
コントローラーの入力は、BTN()関数で読み取ることができます。
BTN(I,P)
・I: ボタンに割り振られた番号 (0~5)
・P: プレイヤー番号 (0~7) (オプション、デフォルト0)
・返り値: ボタンが押されている : TRUE / 押されていない : FALSE
Iに入る数値:
0: 左ボタン
1: 右ボタン
2: 上ボタン
3: 下ボタン
4: Oボタン
5: Xボタン
Pに入る数値:
0: プレイヤー1
1: プレイヤー2
2: プレイヤー3
…
Pはオプションの引数で、省略することが可能です。書かなかった場合は 0(プレイヤー1)とみなされます。
この関数によって、あるボタンが押されているのか押されていないか、その状態を知ることができます。この関数は、今までに紹介してきた関数とは異なり「返り値」があります。関数の返り値は、変数に入れたり、ほかの関数の引数にしたり、IF文の条件文に使ったりすることができます。例えばこんな感じ:
-- 変数に入れる A=BTN(1) -- ほかの関数の引数にする OTHER_FUNC(BTN(1)) -- IF文の条件文で使う IF BTN(1)==TRUE THEN PRINT("RIGHT IS PRESSED") END
この関数をプレイヤー番号とボタン番号を指定して呼び出すと、そのボタンの状態をブール値で返してくれます。「ブール値」という言葉を聞いたことがない方には、難しく聞こえるかもしれませんが、これはたんに「はい」か「いいえ」を表すもので、実際には「TRUE」か「FALSE」のどちらかの値です(「ブール」の名はブール代数を築いたジョージ・ブールという数学者の名前に由来します)。
上記のコードの変数Aには、プレイヤー1の右ボタンが押されていればTRUE、そうでなければFALSEが入ります。OTHER_FUNC()にも、同じ条件でTRUEかFALSEが渡されます。また、ボタンが押されていれば、IF文のブロックの中の処理が実行され、”RIGHT IS PRESEED”と表示されます。
なお、IF文でブール値を判定する場合には、== で値を比較する書き方をせずに、下記のように省略することができます。
IF BTN(1) THEN
この書き方で、「BTN(1)がTRUEであるならば」という判定になります。逆に、「BTN(1)がFALSEであるならば」と書きたい場合には、
IF NOT BTN(1) THEN
と書けます。
キャラクターを動かす
プレイヤーの押したボタンの状態を知る方法がわかりました。次に、プレイヤーの押したボタンに応じて、画面の中のキャラクターを動かしましょう。前回、2Dプラットフォーマー的な、横向きキャラクターのスプライトを作りました。2Dプラットフォーマーは、ジャンプの表現や着地の処理など、ちょっと難易度が高いので、これから作るゲームは、ゼルダやドラクエのような見下ろし型、ということにしましょう。こんなキャラクターを描いてみました:
このキャラクターを画面に登場させます。こんなコードで:
X=60 Y=60 S=1 FUNCTION _UPDATE() END FUNCTION _DRAW() RECTFILL(0,0,127,127,3) SPR(s,x,y) END
X,Yは、キャラクターの(左上の)位置。Sはスプライトの番号です。これは単に、左向きのキャラクターを画面の真ん中あたりに置くだけです。熱心な読者のみなさんは、このようなコードまでは、ソラで書けるようになっているはず。次に、_UPDATE()の中に、入力を受け付けるコードを追加してみましょう。
X=64 Y=64 S=1 FUNCTION _UPDATE() IF BTN(0) THEN X-=1 S=1 END IF BTN(1) THEN X+=1 S=2 END IF BTN(2) THEN Y-=1 S=3 END IF BTN(3) THEN Y+=1 S=4 END END FUNCTION _DRAW() RECTFILL(0,0,127,127,3) SPR(s,x,y) END
(プレイヤー1の)左ボタンを押しているときは、X方向に-1移動させる、すなわち左に1ピクセル移動させると同時に、スプライトを左向きの絵の番号の1番にしています。右のボタンを押したときは……(以下略)。これを実行してみると:
連載4回目にして、ようやく自キャラが動かせるようになりました!
キャラクターをアニメーションさせる
さて、このままでは、硬直した人が地面をスライドしている感じです。足踏みアニメーションを付けましょう。まずはスプライトを追加です:
左右上下の各方向について、2つずつスプライトを追加しています。最初に描いたスプライトは、ただ立っている絵で、追加した2つは足を動かしているアニメーションの絵です。追加した絵はそれぞれ、最初の絵の番号から+16番目、+32番目であることを確認してください。これをプログラムで操作するときに利用できます。ちょっと複雑ですが、以下のようなプログラムをなりました:
X=64 Y=64 D=0 S=1 T=0 FUNCTION _UPDATE() T+=1 LOCAL PRESSED=FALSE IF BTN(0) THEN X-=1 S=1 PRESSED=TRUE END IF BTN(1) THEN X += 1 S = 2 PRESSED=TRUE END IF BTN(2) THEN Y -= 1 S = 3 PRESSED=TRUE END IF BTN(3) THEN Y += 1 S = 4 PRESSED=TRUE END IF PRESSED THEN LOCAL a=16 IF T%8>=4 THEN A=32 END S=d+1+A ELSE S=D+1 END END FUNCTION _DRAW() RECTFILL(0,0,127,127,3) SPR(S,X,Y) END
新たに2つの変数を追加しました。Tは前回も使用した、時間の経過をカウントする変数です。Dは、キャラクターが今向いている方向を表す変数で、方向ボタンの番号と同じ値を入れます。今回は、BTN()でボタンの状態を判定するIF文ブロックの中で、スプライトを表すSの番号を直接変更せず、Dに方向の番号を入れるだけにします。
新しい変数をもう一つ使います。PRESSEDは、_UPDATE()の中でだけ使うローカル変数です(ローカル変数についてはのちほど説明します)。PRESSEDは、方向ボタンのどれかが押されたかどうかを判定する変数です。方向ボタンがどれか押されたときはTRUE、そうでないときはFALSEになることを確認してください。この変数を使って、方向ボタンが押されているときは足踏みをさせて、どれも押されていないときは、ただ立っている絵にします。
_UPDATE()の最後のIF文のブロックが、その処理にあたります。PRESSEDがFALSEのときは、ELSE以下が実行され、SはD+1の番号になります。すなわち、立っている状態の絵です。
PRESSEDがTRUEのときの処理は、ちょっと複雑です。この状態のときは、新たに追加した2種類の絵を交互に表示して、足踏みを表現します。足踏みの絵は、それぞれの方向の立っている絵の番号の+16番目、+32番目にあります。したがって、D+1+16番目、D+1+32番目の絵を交互に表示すればよいはず。IF PRESSED THEN ~ ELSEまでの間で、その処理をやっています。そこだけを抜き出すと、以下のとおり:
IF PRESSED THEN LOCAL A=16 IF T%8>=4 THEN A=32 END S=D+1+A ELSE …
またもやローカル変数ですが、Aは、IF PRESSED THEN ~ ELSE の間だけで使う変数です。Aは、T%8>=4 のときは32、そうでないときは16になります。Tは、_UPDATE()が呼ばれるたびに、すなわち1/30秒ごとに1ずつ増えるカウンターです。前回も用いた、割り算の余りを使う方法で、時間経過に合わせて変数Aの値を、時間間隔を空けながら周期的に変化させます。図示すると以下のとおり:
今回、周期に8を選びましたが、これを変えることで、アニメーションスピードを調整できます。実行してみると:
Pro Tips: BTN()は引数を与えない場合、プレイヤー1とプレイヤー2の全ボタンの状態のビット列を返す仕様です。PRESSEDはこのビット列の判定で置き換えることができます。
ローカル変数とその寿命
Luaの変数には、グローバル変数とローカル変数の2種類があります。ローカル変数ではない変数は、すべてグローバル変数です。グローバル変数はどこからでも使える変数ですが、ローカル変数は、それが定義されたスコープの中でしか使えません。スコープとは、IF文の中身や関数の中身のことです。以下の例を見てください:
FUNCTION DEMO() LOCAL A=TRUE IF A THEN LOCAL B=TRUE END PRINT(B) END DEMO()
ローカル変数Aは、関数DEMO()の内部でのみ使用可能です。最初の宣言文“LOCAL A=TRUE”で生まれて、DEMO()の最後のENDに到達した時、消えてなくなります。ローカル変数Bの寿命はもっと短くて、“IF A THEN”で始まるIF文の内部のみです。“LOCAL B=TRUE”で宣言された直後に消えます。したがって、このコードを実行すると、以下のように出力されるはずです。
NIL
“IF A THEN”のIF文ブロックの外で“PRINT(B)”を実行していますが、このときローカル変数Bは消えてなくなっているので、値を取り出すことができません。そのため“PRINT(B)”を実行するとエラーになりそうです。しかし、実際には、ローカル変数ではないグローバス変数のBが自動的に参照されます。未使用で空っぽのグローバル変数Bを参照したため、空を意味するNILが出力されます。
ローカル変数を使用する意義はいくつかあります。使える範囲が限定されるので、意図しない参照・代入を避けてバグを減らすことができます。また、変数の名前が「かぶる」のを防ぐこともできますし(別のスコープでは、同じ名前のローカル変数を別物として扱えます)、プログラムの高速化にもつながります。
PICO-8シーン最新情報
electricgryphonさんが、PICO-8用の3Dグラフィックエンジンを公開されてます。
Gryphon3D by electricgryphon — a solid 3D engine for #pico8 in 4675 tokens. (Used to make Pico Fox) https://t.co/05hVX9DLdK pic.twitter.com/ElFa0QAey9
— zep.p8 🦣 (@lexaloffle) November 17, 2016
これはだれでも自由に使用してよいとのこと。また、3Dエンジンのプログラムは4675トークンあるそうです。PICO-8の1つのカートリッジで使える最大トークン数は8192なので、残りの3517トークンを工夫して使用する必要があります(※トークンとは、プログラムを構成する最小単位であるキーワードや記号のこと。例えば、“IF A=3 THEN”の場合、“IF”、“A”、“=”、“3”、“THEN”がそれぞれトークンで、5トークンになります)。
さて、本年最後の次回は、ビギナーコースをちょっと外れた特別編として、この3Dエンジンを使って何か作ってみよう!の回にしたいと思います。お楽しみに。