撃たれると痛い……衝突判定その2 – PICO-8ゲーム開発入門(11)
PICO-8でプログラミングを1から学ぶ連載、第11回です。前回はこちら。プログラミング経験者の方は、日本語マニュアルを読んで、すぐに創作にとりかかりましょう。作った作品は、ぜひ掲示板へ。
今回は、ビームでNPCをやっつける処理を作って、ついにシューティング・ゲームらしさを獲得します。まずは、そのために必要な下ごしらえから。
ビームを画面外で消す
今までの実装では、撃ったビームは出しっぱなしになっていました。ビームが画面の外に出て行くと表示されませんが、ビームを表す変数は存在し続けます。これは2つの点で問題があります。
1. メモリにビームが溜まっていく
ビームを何度も発射すると、beams 配列にビームがどんどん溜まっていきます。PICO-8で扱えるメモリの容量には(わりと小さめの)上限があるので、ゲームを続けていくとメモリがパンクして、エラーになります。
2. 座標値がオーバーフローして誤動作が起きる
PICO-8で扱える数値の最大値は、32767.99です。これを超えると、マイナスの値になってしまいます。これがオーバーフローです。PICO-8のコマンド画面で、以下を試してみましょう。
a=32767 a+=1 print(a)
32767+1なので、32768になりそうですが、結果は”-32768″です。つづけて、以下もどうぞ。
a-=1 print(a)
今度は”32767″に戻ります。つまり、32767.99を境に、プラスになったりマイナスになったりするのです。このような現実では起こりえない現象が、誤動作を引き起こすことがあります。
Pro Tips:
32767.99という値は、小さなゲームには十分に大きい値に思えるかもしれませんが、案外危険はすぐそばにいます。このゲームの衝突判定には2点間の距離の2乗の計算が含まれます。もし、衝突判定を行う対象同士が182ピクセル離れていたら、計算式の中で182^2=33124という32767を超えた数値が現れます。182ピクセルは、PICO-8の画面幅の2画面分もありません。毎フレーム4ピクセルずつ進むビームは、発射後1.5秒後には衝突判定のロジックを破綻させます。
上記の2つの問題を避けるために、ビームが画面外に出たら削除する処理を update_beam() に追加しましょう。
function update_beam(b)
b.x+=b.dx*beam_spd
b.y+=b.dy*beam_spd
b.life-=1
if b.life<=0 or b.x<0 or b.x>127 or b.y<0 or b.y>127 then
del(beams, b)
end
end
太字が追加した行です。
画面上に変化は見られませんが、これによって画面外に出たビームは beams 配列から削除されます。
ビームとNPCの衝突判定
次に、ビームがぶつかったNPCをやっつける処理を作ります。ビームとNPCとの衝突判定の処理を書きましょう。すでに実装している collide_with_other_npcs() と似た処理です。
function hit_npc(x, y, r) for c in all(npcs) do if collision(x, y, c.x, c.y, r) then return c end end return nil end
この関数は、(x, y)を中心とした半径の r の円とぶつかるNPCが見つかったら、そのNPCの変数を返します。見つからなければ、「空」を意味する nil を返します。
この関数を使って、NPCへのヒットの処理を update_beam() の中に追加しましょう。
function update_beam(b)
b.x+=b.dx*beam_spd
b.y+=b.dy*beam_spd
b.life-=1
if b.life<=0 or b.x<0 or b.x>127 or b.y<0 or b.y>127 then
del(beams, b)
else
local c = hit_npc(b.x, b.y, 3)
if c ~= nil then
del(npcs, c)
del(beams, b)
sfx(0)
end
end
end
太字が変更した行です。
この処理では、もしビームが衝突したNPCが見つかったら、そのNPCを npcs 配列から削除し、ビームも beams 配列から削除します。音も鳴らします。こんな感じです:
今回は、ヒット時にビームも消えるようにしました。ビームをそのまま残せば、敵を貫通するビームになります。
今回の全コードリストは以下のとおりです。
--- constants ipf=8 -- interval per frame nf=2 -- number of frames max_gun_interval=8 beam_spd=4 beam_len=4 beam_life=64 --- variables t=0 pc={} beams={} npcs={} --- common functions function distance(x1,y1,x2,y2) return sqrt((x1-x2)^2+(y1-y2)^2) end function collision(x1,y1,x2,y2,r) return distance(x1,y1,x2,y2) < r end --- characters function create_character(x, y, s0) local c={} c.x=x c.y=y c.s=s0 c.s0=s0 c.d=4 c.dx=0 c.dy=1 c.spd=2 c.walking=false return c end function update_c(c) -- sprite c.s=(c.d-1)+c.s0 if c.walking then c.s+=flr((t%(nf*ipf))/ipf+1)*16 end if c==pc and c.gun then c.s+=4 end -- position if c.walking then local nx=c.x+c.dx*c.spd local ny=c.y+c.dy*c.spd if c == pc or not collide_with_other_npcs(c, nx, ny, 6) then c.x=nx c.y=ny end end end function draw_c(c) spr(c.s,c.x-4,c.y-4) end function collide_with_other_npcs(c, x, y, r) for other in all(npcs) do if c~=other and collision(x, y, other.x, other.y, r) then return true end end return false end function hit_npc(x, y, r) for c in all(npcs) do if collision(x, y, c.x, c.y, r) then return c end end return nil end --- npcs function add_npc(x, y) add(npcs, create_character(x, y, 64)) end function update_npc(n) follow_pc(n) face_pc(n) update_c(n) end function follow_pc(n) n.walking=true n.spd=0.5 local dx=pc.x-n.x local dy=pc.y-n.y local distance=distance(pc.x, pc.y, n.x, n.y) n.dx=dx/distance n.dy=dy/distance end function face_pc(n) if abs(n.dx)>abs(n.dy) then if n.dx<0 then n.d=1 end if n.dx>0 then n.d=2 end else if n.dy<0 then n.d=3 end if n.dy>0 then n.d=4 end end end --- pc function init_pc() pc=create_character(64, 64, 1) pc.gun=false pc.gun_interval=0 end function update_pc() update_c(pc) if pc.gun_interval>0 then pc.gun_interval-=1 end end --- beams function add_beam(x, y, dx, dy) local b={} b.x=x b.y=y b.dx=dx b.dy=dy b.life=beam_life add(beams, b) end function update_beam(b) b.x+=b.dx*beam_spd b.y+=b.dy*beam_spd b.life-=1 if b.life<=0 or b.x<0 or b.x>127 or b.y<0 or b.y>127 then del(beams, b) else local c = hit_npc(b.x, b.y, 3) if c ~= nil then del(npcs, c) del(beams, b) sfx(0) end end end function draw_beam(b) line(b.x+b.dx*beam_len, b.y+b.dy*beam_len, b.x, b.y, 8) end --- function input() -- walk pc.walking=false if btn(0) then pc.dx=-1 pc.dy=0 pc.d=1 pc.walking=true end if btn(1) then pc.dx=1 pc.dy=0 pc.d=2 pc.walking=true end if btn(2) then pc.dx=0 pc.dy=-1 pc.d=3 pc.walking=true end if btn(3) then pc.dx=0 pc.dy=1 pc.d=4 pc.walking=true end -- gun pc.gun=btn(4) if pc.gun and pc.gun_interval==0 then local bx=pc.x local by=pc.y if pc.d==1 then bx-=3 by+=1 elseif pc.d==2 then bx+=2 by+=1 elseif pc.d==3 then bx+=2 by-=2 else bx-=3 end add_beam(bx, by, pc.dx, pc.dy) pc.gun_interval=max_gun_interval end end --- function _init() init_pc() for y=1,2 do for x=1,7 do add_npc(x*16,y*16) end end end function _update() t+=1 input() update_pc() foreach(npcs, update_npc) foreach(beams, update_beam) end function _draw() rectfill(0,0,127,127,13) draw_c(pc) foreach(npcs, draw_c) foreach(beams, draw_beam) end
弾を撃って敵を倒すという、2Dシューティング・ゲームの基礎ができました。次回は、ゲームっぽさを増すための画面演出を加えていきます。
PICO-8待望の新機能
PICO-8バージョン0.1.11にて、バイナリーエクスポーター機能が実装されました。これまでにも、HTMLプレイヤーの書き出し機能はありましたが、このバージョンからは Windows、Linux、Mac OS X上で単体動作するアプリケーションを書き出すことができます!これによって、PICO-8で作った制作物の配布や販売がしやすくなりました。
複数のカートリッジ(.p8ファイル)をひとまとめにして、一つのバイナリーを生成することもできます。PICO-8のカートリッジにはファイルサイズの上限があるため、作れる作品のボリュームにも上限があります。しかし、ほかのカートリッジを読み込む仕組みを使えば、この制限をある程度緩和することができます。往年のゲーム機のソフトの「◯枚組」のような仕組みです。
まだまだ進化し続けるPICO-8から、来年も目が離せません。みなさま、良いお年を。