撃たれると痛い……衝突判定その2 – PICO-8ゲーム開発入門(11)
PICO-8でプログラミングを1から学ぶ連載、第11回のテーマは「衝突判定その2」です。ビームでNPCをやっつける処理を作って、ついにシューティング・ゲームらしさを獲得します。年末年始のお休みの間に挑戦してみてはいかがでしょうか。

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から、来年も目が離せません。みなさま、良いお年を。



