人と物のふれあい……衝突判定 – PICO-8ゲーム開発入門(7)
PICO-8でプログラミングを1から学ぶ連載、第7回です。前回はこちら。プログラミング経験者の方は、日本語マニュアルを読んで、すぐに創作にとりかかりましょう。作った作品は、ぜひ掲示板へ。
2Dプラットフォーマーを作ろう、というのが今年の目標ですが、その前に、見下ろし型の、初代『ゼルダの伝説』風のゲームを作ることにします。横スクロールの2Dプラットフォーマーは、ジャンプや地面との衝突など、難しい処理を含みます。いきなり手をつけるにはハードルが高すぎるので、ワンクッション置きたい思います。
ゼルダのような、1画面ごとにスクロールして広大な世界を冒険するゲームを目標としますが、まずは、上下左右に自由に動ける主人公を作ってみましょう。
上下左右に動けるキャラクター
第4回で作ったプログラムを流用しましょう。ただし、スプライトとコードをすこし修正してあります。コントローラーの入力を受け付ける箇所を、input()という名前の関数に切り出してあります。
x=64 y=64 s=1 -- スプライト番号 d=1 -- 方向を示す ipf=8 -- アニメーション1フレームについての時間(1ipf = 1/30秒) nf=2 -- アニメーションするフレーム数(足踏みは2フレーム) t=0 function input() local pressed=false if btn(0) then x -= 1 d=1 pressed=true end if btn(1) then x += 1 d=2 pressed=true end if btn(2) then y -= 1 d=3 pressed=true end if btn(3) then y += 1 d=4 pressed=true end if pressed then s=d+flr((t%(nf*ipf))/ipf+1)*16 else s=d end end function _update() t+=1 input() end function _draw() rectfill(0,0,127,127,13) spr(s,x-4,y-4) end
超かんたんなゲーム:アイテムを取る
今回はこれにもう一手間加えます。アイテムを登場させて、それを取るだけの超かんたんなゲームを作ってみましょう。
まずアイテムのスプライトを描きましょう。何か好きなものを描いてみてください。筆者はトマトにしました。なぜなら、AUTOMATONにはTOMATOが含まれているから……。
そして以下のとおりのコードを追加。
...略... ix=32 -- アイテムのX座標 iy=32 -- アイテムのY座標 is=5 ...略... function _draw() rectfill(0,0,127,127,13) spr(is,ix-4,iy-4) -- アイテム spr(s,x-4,y-4) -- プレイヤーキャラクター end
描画のコードは順番が大事です。後に描いたものの方が上に(画面の前面に)描かれます。アイテムにプレイヤーキャラクターがかぶさった時、キャラクターの下に隠れたほうが「取った感」が出ますから、キャラクターより先にspr()で描きます。
これだけの追加コードで、アイテムを登場させることはできました。次に実際に「取る」処理に取りかかります。
衝突判定
コンピューターゲームのお約束で、アイテムに触れたら、そのアイテムを取ったことになります。また、敵キャラクターに触れたら、ダメージを受けたりします。物体と物体が触れたかどうかを調べる「衝突判定」は、ゲームプログラミングでは重要な処理の一つです。中でも、3次元物体同士の衝突の計算は難しい分野の一つで、その話題だけの分厚い本が出ていたりもします。しかし、2Dゲームの衝突判定はさほど難しいことではありません。
今回は、点と点の距離を使う方法にします。プレイヤーやアイテムは、ある程度の面積を持った物体ですが、今回の衝突判定においては、単なる点とみなします。点と点がじゅうぶんに近ければ、それらは触れている、ということにします。すこし大雑把ですが、動かしてみて不自然でなければよいのです。
スーパー簡単なことなので説明するまでもないですが、2次元空間での2点間の距離は、三平方の定理で計算できます。プレイヤーの座標が(px, py)、アイテムの座標が(ix, iy)だとすると、距離dを求めるには:
この式の意味がわからなくても、大丈夫です。とにかく、この式に座標の数値を入れれば距離がわかります。数学の定理は使用料タダの便利ツールです。理屈は気にせずガンガン使っていきましょう!
さて、上記の数式をPICO-8のLuaコードで書くと以下のようになります。
function distance(x1,y1,x2,y2) return sqrt((x1-x2)^2+(y1-y2)^2) end
数値のn乗は、^nで表せます。xの2乗なら、x^2。そしてルート(平方根)を求めるには、PICO-8で独自に定義された関数sqrt()を使います。sqrtは”SQuare RooT”の略で、すなわち平方根の意味です。
距離がじゅうぶん近ければ、衝突したこととみなすので、衝突判定の関数は以下のようになります。
function collision(x1,y1,x2,y2) if distance(x1,y1,x2,y2) < 4 then return true else return false end end
この関数がtrueを返すとき、2点は衝突しています。「じゅうぶん近い」とみなす距離は4にしました。1つのスプライトは8×8のサイズですから、その幅の半分です。以下のような位置関係です。
アイテムを取ったら
アイテムに触れたら、リアクションが欲しいところ。どこか画面の別の場所へトマトをワープさせることにしましょう。どこかにランダムに飛ばすために、乱数を使いましょう。
rnd(x)
・返り値: 0以上x未満の乱数。
たとえばx=1のときの結果はこんな感じ:
返ってくる値は小数点を含みます。整数にしたい場合は、flr()を使いましょう。これは小数点以下を切り捨てます。flr(rnd(16))とすると、0~15までのどれかの値が出てきます。
さて、PICO-8の画面は128×128で、x座標、y座標ともに0~127の値をとります。したがって、アイテムをどこかにランダムに飛ばす関数はこのようになります:
function warp_item() ix=rnd(128) iy=rnd(128) end
さて、ここでさっき説明したflr()を使っていないので、アイテムの座標は小数点以下を含むこんな値になります:
ix=124.313 iy=65.959
ピクセルのXやYの座標は整数になりそうですが、spr()関数の引数にこの値をそのまま入れても、大丈夫です。エラーになったりせず、問題なくスプライトが表示されます。PICO-8が適切に処理してくれます。これがPICO-8の、プログラミングする人へのやさしさです。
ゲームを組み立てる
さて、いままで作ってきた関数を組み合わせて、ゲームを仕上げましょう。_update()の中でプレイヤーキャラクターとアイテムとの接触を判定するcollision()関数を呼び出して、つねにチェックします。もしそれらがぶつかっていたら、アイテムをワープさせます(warp_item())。さらに、音も鳴らしましょう。0番のSFXを用意しておいてください。
x=64 -- プレイヤーのX座標 y=64 -- プレイヤーのY座標 s=1 -- プレイヤーのスプライト番号 d=1 -- 方向を示す ipf=8 -- アニメーション1フレームについての時間(1ipf = 1/30秒) nf=2 -- アニメーションするフレーム数(足踏みは2フレーム) ix=32 -- アイテムのX座標 iy=32 -- アイテムのY座標 is=5 -- アイテムのスプライト番号 t=0 function distance(x1,y1,x2,y2) return sqrt((x1-x2)^2+(y1-y2)^2) end function collision(x1,y1,x2,y2) if distance(x1,y1,x2,y2) < 4 then return true else return false end end function warp_item() ix=rnd(128) iy=rnd(128) end function input() local pressed=false if btn(0) then x -= 1 d=1 pressed=true end if btn(1) then x += 1 d=2 pressed=true end if btn(2) then y -= 1 d=3 pressed=true end if btn(3) then y += 1 d=4 pressed=true end if pressed then s=d+flr((t%(nf*ipf))/ipf+1)*16 else s=d end end function _update() t+=1 input() if collision(x,y,ix,iy) then warp_item() sfx(0) end end function _draw() rectfill(0,0,127,127,13) spr(is,ix-4,iy-4) spr(s,x-4,y-4) end
動かしてみると、こんな感じです:
おめでとうございます!ついに、ゲームらしきものが一つ完成しました。これが、あなたのゲーム開発者人生の第一歩です。まだまだ長い道のりですが、一歩一歩着実に歩んでいきましょう。次回は、アイテムやキャラクターのデータを賢くまとめる新概念「テーブル」を使ってトマトを大量発生させたりします。お楽しみに!