撃たれると痛い……衝突判定その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で作った制作物の配布や販売がしやすくなりました。

どの環境で書き出しても、Windows、Linux、Mac OS X用すべてのバイナリーが生成されます。

複数のカートリッジ(.p8ファイル)をひとまとめにして、一つのバイナリーを生成することもできます。PICO-8のカートリッジにはファイルサイズの上限があるため、作れる作品のボリュームにも上限があります。しかし、ほかのカートリッジを読み込む仕組みを使えば、この制限をある程度緩和することができます。往年のゲーム機のソフトの「◯枚組」のような仕組みです。

まだまだ進化し続けるPICO-8から、来年も目が離せません。みなさま、良いお年を。