ビーム、撃っちゃうね。……繰り返しとテーブルその2 – PICO-8ゲーム開発入門(9)

PICO-8でプログラミングを1から学ぶ連載、第9回は「プレイヤーキャラクターが使う銃」を作ってみます。オートマ坊やに銃を持たせ、ビームを撃つ処理を紹介します。コードをコピーして実際に動作させ、そこから改良を加えながら学ぶというのもPICO-8なら簡単です。

PICO-8でプログラミングを1から学ぶ連載、第9回です。前回はこちら。プログラミング経験者の方は、日本語マニュアルを読んで、すぐに創作にとりかかりましょう。作った作品は、ぜひ掲示板へ。

前回、「次回は、敵キャラクターを登場させてみましょう」と書きましたが、訂正します。その前に、今回はプレイヤーキャラクターが使う銃を作ってみます。銃といっても、このゲームの主人公・オートマ坊やが持っているのは、トマトケチャップのビームを放つケチャップ銃です。アメリカ北西部の田舎街を舞台に、野菜ペーストを充填した銃を持つ少年・オートマ坊やが、悪の皇帝に闘いを挑むのです(暫定設定)。

まずは、坊やが歩けるだけのコードをベースとします。前回作ったアイテムを表示するコードはいったん抜き去ります。

x=64
y=64
s=1 -- sprite 
d=1 -- direction 
ipf=8 -- interval per frame 
nf=2 -- number of frames 
spd=2
t=0

function input()
 local pressed=false
 if btn(0) then
  x -= spd
  d=1
  pressed=true
 end
 if btn(1) then
  x += spd
  d=2
  pressed=true
 end
 if btn(2) then
  y -= spd
  d=3
  pressed=true
 end
 if btn(3) then
  y += spd
  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

スプライトはこのとおりです。

 

銃、持っちゃうね。

まずは坊やに銃を持たせましょう。銃を持った状態のスプライトを追加します。トマトは今回使用しませんが、16番の位置に退避させておきます。

オートマ坊やは右利きです。

銃を持ったスプライトは、銃を持っていない同じポーズのスプライトからちょうど4つ番号が増えた位置にあります。これを利用して、コードでスプライトを切り替えます。

関数 input() を以下のように書き換えましょう。

function input()
 local pressed=false
 local shoot=0
 if btn(0) then
  x -= spd
  d=1
  pressed=true
 end
 if btn(1) then
  x += spd
  d=2
  pressed=true
 end
 if btn(2) then
  y -= spd
  d=3
  pressed=true
 end
 if btn(3) then
  y += spd
  d=4
  pressed=true
 end
 if btn(4) then
  shoot=4
 end
 if pressed then
  s=d+flr((t%(nf*ipf))/ipf+1)*16
 else
  s=d
 end
 s+=shoot
end

btn(4)はZキーを押したときtrueになります。Zキーを押しているときだけ、通常のスプライトの番号に4足しているので、坊やが銃を構えた絵になります。

 

プレイヤーの情報をまとめる

次に、銃からビームが出るようにしたいところですが、その前に、プレイヤーキャラクター(PC)の情報をテーブルにまとめます。いままでグローバル変数x, y, sなどにPCの情報をバラバラに入れていましたが、前回のアイテムと同様に、一つのテーブルにまとめます。

pc={}
pc.x=64
pc.y=64
pc.s=1 -- sprite
pc.d=1 -- direction
pc.dx=0 -- direction x 
pc.dy=0 -- direction y 
pc.spd=2
pc.walking=false
pc.gun=false

ipf=8 -- interval per frame 
nf=2 -- number of frames 
t=0

function input()
 pc.dx=0
 pc.dy=0
 pc.walking=false
 if btn(0) then
  pc.dx-=1
  pc.d=1
  pc.walking=true
 end
 if btn(1) then
  pc.dx+=1
  pc.d=2
  pc.walking=true
 end
 if btn(2) then
  pc.dy-=1
  pc.d=3
  pc.walking=true
 end
 if btn(3) then
  pc.dy+=1
  pc.d=4
  pc.walking=true
 end
 pc.gun=btn(4)
end

function draw_pc()
  pc.s=pc.d
  if pc.walking then
   pc.s+=flr((t%(nf*ipf))/ipf+1)*16
  end
  if pc.gun then
   pc.s+=4
  end
  spr(pc.s,pc.x-4,pc.y-4)
end

function update_pc()
  pc.x+=pc.dx*pc.spd
  pc.y+=pc.dy*pc.spd
end

function _update()
 t+=1
 input()
 update_pc()
end

function _draw()
 rectfill(0,0,127,127,13)
 draw_pc()
end

このコードから、input() の中身が大幅に変わっています。今まで、PCの移動とスプライト番号を決定する処理までこの関数の中に書いていましたが、それぞれ、update_pc()、draw_pc() という関数に切り出しました。こうやって機能・処理ごとに関数に分けていくと、コードが読みやすくなり、のちに変更を加えやすくなります。

今回から pc.dx, pc.dy という変数を導入しています。これは、X座標 / Y座標のどの方向に進むか、ということを表しています(数学の言葉で言えば、単位方向ベクトルです)。input() では押された方向キーに応じて、pc.dx / pc.dyの値を設定します。update_pc()では、pc.dx/pc.dyの値に速さ(spd)を掛け合わせ、PCを移動させます。

方向とpc.dx/pc.dy

draw_pc() には、歩きアニメーションのスプライトの番号の決定と、スプライトの描画までが含まれ、_draw() から呼ばれます。

処理の流れはこんな感じ:

このフローが、毎フレーム(1/30秒おき)実行されます。

 

ビーム、撃っちゃうね。

ビームを撃つ処理にとりかかります。Zキーを押している間に、連射できるようにします。連続できるということは、複数のビームを扱う必要があるので、前回のアイテムと同様に、テーブルによる配列を用意します。こんな感じの動きを目指します:

これを作るために、以下のような処理を追加します。

ビームの追加:
Zキーを押したとき、ビームの配列にビームを追加する。

ビームの更新:
ビームの配列に入っているビーム一つ一つを、少しずつ移動させる。

ビームの描画:
ビームを画面に描く。

さっきのフロー図に追加するとこんな感じになります。

 

ビームの追加

ビームは自動的に飛んでいく物体です。ひとつひとつのビームもプレイヤーと同様に、位置と方向の情報が必要です。ビームを追加する関数はこんな感じになります。

beams={}
beam_life=64

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

ローカル変数 b に、ビーム一つぶんの情報を持つテーブルを作り、ビームの配列 beams に追加します。ビーム一つぶんのテーブルが持つ情報は、以下のとおりです:

●b.x / b.y : ビームの位置
●b.dx / b.dy : ビームの向いている方向
●b.life : ビームの寿命

これを呼び出す側のコードは、以下のようになります(関数input()の一部)。

function input()
 ...略...
 pc.gun=btn(4)
 if pc.gun 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)
 end
end

基本的には、Zボタンが押されたときのPCの位置、向きをadd_beam()の引数に渡して、ビームの初期位置と向きを決定します。ただし、pc.x / pc.y をそのままビームの初期位置とはせずに、いったん bx / by の変数に入れてから、向いている方向によって、数ピクセルずつずらしています。これは、坊やの持っている銃の先端からビームが出ているように見えるようにするためです。

life はビームの寿命です。毎フレーム減らしていって、0 になったらビームの配列から取り除きます。この処理を入れないと、銃を撃つたびに beams 配列の中身が増えていき、処理は重くなり、いずれメモリがパンクします。寿命を扱う処理はのちほど加えます。

メモリがパンクすると、プログラムがエラーで止まります。

 

ビームの描画

次に、配列の中のひとつひとつのビームを画面に表示します。ビームひとつぶんを表示する関数は、以下のとおりになります。引数 b は、ビームひとつぶんのテーブルです。

function draw_beam(b)
 pset(b.x, b.y, 8) -- ケチャップの赤色は8番
end

この関数を、配列の中のすべてのビームについて呼び出します。前回学んだfor ループを使えば、以下のように書けます。

function _draw()
 ...略...
 for i=1,#beams do
  draw_beam(beams[i])
 end
end

ビームの個数分繰り返すループの中で、ビームひとつひとつをdraw_beam()関数に引数として渡します。今回は、もっとスマートな書き方をご紹介します。

function _draw()
 ...略...
 foreach(beams, draw_beam)
end

この foreach は、PICO-8独自の関数です。

foreach(t, f)
●t: テーブル
●f: 関数

テーブル(配列)の t のすべての要素を、関数 f に引数として渡して呼び出します。for を使った場合とくらべると、配列の中身の個数を調べる必要もなく、ループ処理がたったの1行で書けます。このforeachは便利なので、今後多用していきます。

 

ビームの更新

ビームが飛んで行くように、毎フレームごとに移動させる関数 update_beam() を作ります。この関数の中で、ビームの寿命を扱う処理も行います。_update() からの呼び出しには、描画と同様に foreach を使います。

beam_spd=2

function update_beam(b)
 b.x+=b.dx*beam_spd
 b.y+=b.dy*beam_spd
 b.life-=1
 if b.life<1 then del(beams, b) end
end

function _update()
 ...略...
 foreach(beams, update_beam)
end

b.life を毎フレームに1ずつ減らしていき、0になったら beams 配列から取り除きます。これがビームの寿命です。ビームのスピード(beam_spd)はちょうど良い値になるように調整しましょう。

 

発射間隔の調整

ここまでのコードで、ビームが撃てるようになっています。ここからは、気持ち良い動き・見た目になるようにする調整作業です。いまのままでは、ボタンを押したままのとき毎フレーム発射し続けるので、ビームが数珠つなぎになってしまいます。

これを防ぐために、一発撃ったあとは、間をあけるようにしましょう。PCのテーブルに、発射間隔をカウントするための変数を追加します。

pc.gun_interval=0

そして、input() に下記を追加します。

max_gun_interval=8

function input()
 ...略...
 if pc.gun and pc.gun_interval==0 then
  ...略...
  add_beam(bx, by, pc.dx, pc.dy)
  pc.gun_interval=max_gun_interval
 end
end

pc.gun_interval の値が0のときのみ銃を撃てるようにします。また、銃を一発撃った直後は、pc.gun_interval に値を設定します。これは、次に発射できるようになるまで何フレーム待つか、という値です。ここでは8にしました。最後に、この pc.gun_interval を毎フレーム減らす処理を update_pc() に加えます。

function update_pc()
 ...略...
 if pc.gun_interval>0 then
  pc.gun_interval-=1
 end
end

これによって、Zキーを押し続けていたとしても、8フレームに1回しか発射されなくなります。

 

ビームをのばす

最後に、ビームをもっとスターウォーズのようにしましょう。いまのままでは、ただの点ですので(ケチャップの弾を発射する銃、という解釈にしてもいいかもしれませんが・・・)。draw_beam() の中身を書き換えます。

beam_len=4

function draw_beam(b)
 line(b.x+b.dx*beam_len, b.y+b.dy*beam_len, b.x, b.y,8)
end

pset() ではなく line() を使って、線を引いています。最初の引数2つは、ビームの位置よりビームの進行方向に少しだけ移動した位置の座標になります。その「少し」の長さが、ビームの長さであり、これは変数 beam_len に設定してあります(“length(長さ)”を短くして”len”です)。beam_lenの値を変えるとビームの長さが変わるので、好きな長さに調節してみましょう。

今回の全コードは以下のとおりです。

pc={}
pc.x=64
pc.y=64
pc.s=4 -- sprite
pc.d=4 -- direction
pc.dx=0 -- direction x
pc.dy=1 -- direction y
pc.spd=2
pc.walking=false
pc.gun=false
pc.gun_interval=0
max_gun_interval=8

ipf=8 -- interval per frame
nf=2 -- number of frames
t=0

beams={}
beam_spd=4
beam_len=4
beam_life=64

--- 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 then del(beam, b) 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() 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 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 draw_pc() pc.s=pc.d if pc.walking then pc.s+=flr((t%(nf*ipf))/ipf+1)*16 end if pc.gun then pc.s+=4 end spr(pc.s,pc.x-4,pc.y-4) end function update_pc() if pc.walking then pc.x+=pc.dx*pc.spd pc.y+=pc.dy*pc.spd end if pc.gun_interval>0 then
  pc.gun_interval-=1
 end
end

function _update()
 t+=1
 input()
 update_pc()
 foreach(beams, update_beam)
end

function _draw()
 rectfill(0,0,127,127,13)
 draw_pc()
 foreach(beams, draw_beam)
end

max_gun_interval、beam_spd、beam_len などの値を変えることで、動きを自分好みに調整してみてください。ビーム銃が完成しましたので、次回はケチャップを浴びせられる、かわいそうな敵キャラクターたちを作りたいと思います。

Ryosuke Mihara
Ryosuke Mihara

なかなか完成しないローグライクゲーム『Gesuido』を開発し続けている30代後半。また、レトロでモダンなファンタジー・コンソール『PICO-8』に魅了され、勝手に普及活動に励んでいる。

記事本文: 12