コントローラーを使おう – 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プラットフォーマーは、ジャンプの表現や着地の処理など、ちょっと難易度が高いので、これから作るゲームは、ゼルダやドラクエのような見下ろし型、ということにしましょう。こんなキャラクターを描いてみました:

20161124-34441-4sprites

このキャラクターを画面に登場させます。こんなコードで:

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番にしています。右のボタンを押したときは……(以下略)。これを実行してみると:

20161124-34441-moving01

連載4回目にして、ようやく自キャラが動かせるようになりました!

 

キャラクターをアニメーションさせる

さて、このままでは、硬直した人が地面をスライドしている感じです。足踏みアニメーションを付けましょう。まずはスプライトを追加です:

20161124-34441-12sprites

左右上下の各方向について、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の値を、時間間隔を空けながら周期的に変化させます。図示すると以下のとおり:

20161124-34441-mod

今回、周期に8を選びましたが、これを変えることで、アニメーションスピードを調整できます。実行してみると:

20161124-34441-moving02

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グラフィックエンジンを公開されてます。

これはだれでも自由に使用してよいとのこと。また、3Dエンジンのプログラムは4675トークンあるそうです。PICO-8の1つのカートリッジで使える最大トークン数は8192なので、残りの3517トークンを工夫して使用する必要があります(※トークンとは、プログラムを構成する最小単位であるキーワードや記号のこと。例えば、“IF A=3 THEN”の場合、“IF”、“A”、“=”、“3”、“THEN”がそれぞれトークンで、5トークンになります)。
さて、本年最後の次回は、ビギナーコースをちょっと外れた特別編として、この3Dエンジンを使って何か作ってみよう!の回にしたいと思います。お楽しみに。