人と物のふれあい……衝突判定 – 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

動かしてみると、こんな感じです:

説明してないものも飛び散ってますが……きれいでしょコレ(上のコードでは出ません)

おめでとうございます!ついに、ゲームらしきものが一つ完成しました。これが、あなたのゲーム開発者人生の第一歩です。まだまだ長い道のりですが、一歩一歩着実に歩んでいきましょう。次回は、アイテムやキャラクターのデータを賢くまとめる新概念「テーブル」を使ってトマトを大量発生させたりします。お楽しみに!