ビーム、撃っちゃうね。……繰り返しとテーブルその2 – PICO-8ゲーム開発入門(9)
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を移動させます。
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 などの値を変えることで、動きを自分好みに調整してみてください。ビーム銃が完成しましたので、次回はケチャップを浴びせられる、かわいそうな敵キャラクターたちを作りたいと思います。