PICO-8はレトロ風なゲームが簡単に作れるゲームエンジンです。
今回は実際にPICO-8でゲームを作っていきます。
※PICO-8についてはこちらの記事で紹介しています。
スプライトをつくってみよう
PICO-8を起動したらESCキーを押してエディタ画面にして右上にあるアイコンの中から左から2番目を選んでください。
するとスプライト編集画面になります。
操作を簡単に説明すると下にある「画像エリア」の絵を描きたい場所を選んで、選んだ場所が「画像編集エリア」に表示されるのでそこでお絵かきします。編集している画像の番号が「画像番号」に表示されます。この番号をプログラムで使います。
右上にあるカラフルなのがパレットです。使える色はそれですべてです。それ以外の色は使えません。
それからPICO-8のお絵かきする絵の大きさの単位は8の倍数で固定されています。最小が8×8です。とりあえずこの最小の8×8でキャラクターを作りましょう。アニメーションもさせたいので4つほど横一列に作ってください。
あと画像エリアの左上端(0番)は空けておいてください。
アニメーションさせよう
適当に画像を描いたら次は表示してみましょう。
表示は簡単で「spr(画像番号, x, y,)」です。
で、これの画像番号を切り替えてアニメーションしているようにしてみましょう。
function _init() x = 60 y = 60 num = 1 fcount = 0 end function _update() fcount += 1 if fcount > 32767 then fcount = 0 end if fcount % 5 == 0 then num += 1 if num > 4 then num = 1 end end end function _draw() cls() spr(num, x, y) end
そうすると↓のようにアニメーションします。
プログラムをさっくり説明すると、numという変数に画像番号が入っていてその数字を入れ替えることで画像が変わってアニメーションしているように見せています。
ただ、このnum変数の値が毎フレーム(update処理のタイミングね)数字を入れ替えているとアニメーションが速すぎるので(1秒間に30回変わるので)、fcountという変数を作ってフレームを数えさせて5回に一回だけにしています。
で、if文で「fcount > 32767」という条件がありますが、これはPICO-8での数値の最大値が32767.99だからです(それを超えるとマイナスの値になります)。なのでこの条件式がついています。(ただ、32767を超えたら0にするのは良くないね。0に戻るときに%5で0になるタイミングがずれるね。もうちょっと切りのいい数字でやるべき。記事読み返してて気づいた(^^;))
あと、「fcount % 5 == 0」という式は「fcountを5で割った余り == 0」という意味で「%」は「数字を割ったときの余りの値」を出します。今回の式では余りが0になるのは5の倍数の時だけなので5回に1回ということになるわけです。
あと、今回は使いませんがspr()関数はあと2つ引数を追加できるようになっていて、spr(番号, x, y, 幅, 高さ)という感じで幅・高さを追加できます。この幅高さは8×8の画像が何枚分かという意味です。16×16で作った画像の場合はspr(番号, x, y, 2, 2)となります。
キャラクターを操作しよう
では次にキャラクターを操作できるようにしましょう。
PICO-8は入力できるものが限定されている分非常に簡単に入力を扱うことができます。
ではさっそく作ってみましょう。
function _init() x = 60 y = 60 num = 1 fcount = 0 end function _update() fcount += 1 if fcount > 32767 then fcount = 0 end if fcount % 5 == 0 then num += 1 if num > 4 then num = 1 end end if btn(0) then x -= 0.3 end if btn(1) then x += 0.3 end if btn(4) then y -= 0.3 end if btn(5) then y += 0.3 end end function _draw() cls() spr(num, x, y) end
上のプログラムを入力して実行するとこんな感じになります。
PICO-8での入力判定はbtn()関数で簡単に行うことができます。
入力には0~5の番号が割り当てられて
[0: 左][1: 右][2: 上][3: 下][4: z][5: x]
という風に各ボタンが割り当てられています。そしてbtn()関数の引数にこの数字を渡すと押されているときは「true」、押されていないときは「false」が返ってきます。これは真偽値と呼ばれるもので数字とは違う特殊な値です。
実はif文に使われている式(num > 4みたいなやつ)なんかもこの真偽値を返しています。で、それがtrueの時にif文は条件を満たしているとしてその中の処理を行うようになっています。なのでif(btn(0))はtrueかfalseが返ってくるので「== true」のような式が無くても条件判定がされるわけです。
※※※BOOTHでこのサンプルプログラムをダウンロードできます。
このプログラムが「sample4.p8」という名前で入っています。
さて、次は簡単な障害物を作ってよりゲームっぽくしていこうかと思います。
ゲームっぽくなってくる分、開発難易度も上がってくるので頑張っておくれ(‘ω’)ノ
テーブルを使ってみよう
PICO-8には複数の値を入れられるテーブルというものがあります。
このテーブルはなんでも詰め込める便利な入れ物で変数も関数も入れられます。
で、このまとめて入れられるテーブルをどう使うかというと、例えば「プレイヤーキャラ」関係のものだけまとめたり、敵キャラだけのものをまとめたりして分けて管理してプログラムをわかりやすく作ることができます。
まぁ説明だけではわかりづらいと思うので前回の記事のプログラムをちょっと変更してみます。
player = {} player.init = function() player.x = 60 player.y = 60 player.num = 1 end player.update = function() if fcount % 5 == 0 then player.num += 1 if player.num > 4 then player.num = 1 end end if btn(0) then player.x -= 0.3 end if btn(1) then player.x += 0.3 end if btn(4) then player.y -= 0.3 end if btn(5) then player.y += 0.3 end end player.draw = function() spr(player.num, player.x, player.y) end function _init() fcount = 0 player.init() end function _update() fcount += 1 if fcount > 32767 then fcount = 0 end player.update() end function _draw() cls() player.draw() end
さっくり説明するとまず「player = {}」でplayerというテーブルを作っています。
で、「player.init = function()」というところでinitという名前の関数を作って_uinit()にあったプレイヤーキャラに関係するものこっちに移しています。update、drawも同様です。で、作っただけでは実行されないので下の_init()、_update()、_draw()内に記述して実行しています。
プレイヤーキャラに関する変数にはすべて「player」をつけてplayerテーブルに入れます。「.」は「の」みたいな感じで「player.x」だったら「playerのx」みたいなもんだととりあえず思ってください。
fcountはプレイヤーキャラと直接は関係ないので_update()に残しています。
と、まぁこんな感じでまとめて整理して見やすくするわけです。
障害物のテーブルを作る
プレイヤーキャラと同じような感じで障害物を作りましょう。
ただテーブル名は面倒なのでenemyにしました(^^;)
enemy = {} enemy.init = function() enemy.r = 10 enemy.x = enemy.r + rnd(128 - enemy.r*2) enemy.y = -enemy.r end enemy.update = function() enemy.y += 5 if enemy.y > 128 + enemy.r then enemy.y = -enemy.r enemy.x = enemy.r + rnd(128 - enemy.r*2) end end enemy.draw = function() circ(enemy.x, enemy.y, enemy.r, 7) end
こんな感じになります↓
スプライトを作るのが面倒だったので障害物は円にしました。
ここまでちゃんとやってきていれば内容はわかると思います。ここで新しいのはrnd()くらいです。rnd()は0から引数で渡した数までの間でランダムで数字を返してくれる関数です。前後にenemy.rが入っているのは円の座標は中心の座標なので半径分ずらしておかないと画面外にはみ出るからです。
あと、上のプログラムでは省略してるけどinit()などの関数は_init()にちゃんと記述してね。
クラスを作って障害物を増やそう
障害物一個じゃゲームになんねぇぜ!!ってことで増やしましょう。こっからけっこうややこしくなってくるぞ!!
enemy_class = function(r, i) local obj = {} obj.init = function() obj.x = r + rnd(128 - r*2) obj.y = -(r * i * 2 + r) obj.r = r end obj.update = function() obj.y += 1 if obj.y > 127 + obj.r then obj.y = -obj.r obj.x = obj.r + rnd(128 - obj.r*2) end end obj.draw = function() circ(obj.x, obj.y, obj.r, 7) end return obj end enemies = {} for i = 1, 9, 1 do enemies[i] = enemy_class(8, i) end function _init() fcount = 0 player.init() for i = 1, #enemies, 1 do enemies[i].init() end end function _update() fcount += 1 if fcount > 32767 then fcount = 0 end player.update() for i = 1, #enemies, 1 do enemies[i].update() end end function _draw() cls() player.draw() for i = 1, #enemies, 1 do enemies[i].draw() end end
がんばって入力するとこんな感じに動きます。
ではさっくり解説していきます。
まず今回はクラスというものを作ります(正確にはプロトタイプというらしくてクラスとはちょっと違うみたい)。enemy_classの中身を見ると一つ前に作ったenemyテーブルとほぼ同じものがあります。ただobjという名前のテーブルになっています。
で、このobjテーブルには最初に「local」とついてます。これは「ここでしか使えない変数」の時に付けます。そして「ここ」はこの変数が入っている関数の中です。この関数の外に出ると消えます。だから関数の外から中身を見ることができません。逆にこのlocalがついていないものはグローバル変数と言ってどこからでも中身が見えます。
なのでこのenemy_class()の中で作られたobjはここから出たら消えます。ただ消える前にこの内容をreturnで送っています。送る先がenemies[i]になります。そして次の時はまた新しいobjが作られます。
もしobjにlocalがついていないとenemiesの中身が全部同じになっておかしくなります(試してみてね)。「毎回objの中身は新しくなってるはずなのになんで?」という疑問が湧くと思いますがこれはobjがグローバルでずっと存在していてenemiesの各要素と繋がっているからです。詳しく知りたい方は「アドレス渡し」とかで調べてください(‘ω’)ノ
ここで新しくfor文というものが出てきます。これは繰り返しを行う特殊な命令です。でこのfor文は「for 初期値, 終了値, 加算値 do」となっています。初期値から毎回加算値を足して終了値になるまで繰り返します。つまりここではenemies[1]からenemies[9]まで9個enemy_classを作っています。※注:PICO-8で使われているLua言語のテーブルの要素は[0]からではなく[1]からなんだそうです。
後半のfor文に出てくる「#enemies」はenemiesテーブルの要素数を意味します。つまり9になるわけです。
※※※BOOTHでこのサンプルプログラムをダウンロードできます。
このプログラムが「sample5.p8」という名前で入っています。
障害物との当たり判定をつけよう
実は当たり判定の基本の話は別の記事にまとめてあるのでまずそっちを一度読んでください。今回はこの記事内にある「円と円の当たり判定」を使います。
で、当たり判定をつける前に当たったときに当たったことが分かるように鳴らす効果音を先に用意しましょう。
エディタ画面で右上にあるアイコンの中の◁みたいなのを選びます。するとサウンドエディタが開くので下の画像のような感じで適当にマウスでなぞってみてください。
スペースキーを押すと再生できます。
サウンドの作り方についてはまた別の記事を書く予定なのでとりあえず今はこれだけでOKです。保存してプログラムに戻ってください。
で、当たり判定を付けたプログラムはこんな感じになります。
enemy_class = function(r, i) local obj = {} obj.init = function() obj.x = r + rnd(128 - r*2) obj.y = -(r * i * 2 + r) obj.r = r end obj.update = function() obj.y += 1 if obj.y > 127 + obj.r then obj.y = -obj.r obj.x = obj.r + rnd(128 - obj.r*2) end if obj.y > -obj.r then if hitcheck(obj.x, obj.y, obj.r, player.x+4, player.y+4, player.r) then sfx(0) end end end obj.draw = function() circ(obj.x, obj.y, obj.r, 7) end return obj end function hitcheck(x1, y1, r1, x2, y2, r2) if (x1 - x2) ^ 2 + (y1 - y2) ^ 2 < (r1 + r2) ^ 2 then return true end return false end
あとplayerテーブルのinit()に「player.r = 3」を追加してください。
これで実行すると障害物に当たったときにさっき作った効果音が鳴ります。
さっくり説明
まずfunction hitcheck()が当たり判定用の関数です。この当たり判定の説明は上で紹介している当たり判定記事を読んでください。
でこのhitcheck()をenemy_class内で使っています。hitcheck()は別にどこで実行してもいいんですが今回の判定は障害物とプレイヤーキャラだけなのでenemy_classに書くのが一番書くことが少なくて済むのでここにしました(他の場所だとfor文が必要になってくる)。
hitcheck()の前にあるif文は画面内に障害物が見える範囲だけに絞るために付けています。理由としては前の記事でも書いたようにPICO-8で使える数字の最大値が32767.99のためです。今回の判定では2乗(^2と書いているのが2乗という意味です)を使うので画面外にある場合に32767を超える可能性があります(ゲーム開始時に超えてるものがあります)。なのでこの条件を付けています。
sfx(0)は効果音を鳴らす命令です。引数は作ったサウンドの番号です。めちゃくちゃ簡単ですね。
そういえばプレイヤーキャラに画面外に出ないようにする処理を書いていなかったので追加しておいてください(‘ω’)ノ
あと当たり判定で使うplayer.rが画像サイズの半分の4ではなく3にしているのは4で作ると当たる範囲が広すぎるためです。試しに大きさを変えて遊んでみてください(circ()で表示してみるといいよ)。
あ、あとスプライトの座標は画像の左上の角を指しています。なので円の当たり判定で中心を取りたい場合は座標に画像の半分の幅・高さを足してください。
キャラクターを非表示にする
障害物に当たったら演出を加えたいところですが、まずキャラクターを非表示にしましょう。もいらないんでね。
非表示にするのは単純にdraw()しなければ表示されることが無くなるんですが、そのためにフラグというものを作ります。フラグというのはtrue/falseで状態を管理するもので、今回はプレイヤーキャラクターの生死を管理するのに使います。
まず、player.init()内に「player.isdead = fasle」というのを作ります。ゲーム開始時はまだ死んでないのでfalseです。
で、当たり判定の所に「player.isdead = true」と追加します。これで死んだことが分かります。
次にplayer.draw()内のspr()の前に「if player.isdead then return end」を追加します。これでキャラクターが表示されなくなります。
やられた感を出す演出を作る
最近のゲームでは「ブシャーッ!」とか「プチュッ!」みたいな感じを出すために木っ端みじんに飛ばしたり血しぶきを飛び散らしてやられちゃった感を演出してるものが多いです。
その飛び散らしてるものをパーティクルと呼ぶらしいです。
というわけでプレイヤーが障害物に当たったときにパーティクルを飛び散らして「ミスった感」を盛り上げましょう。
出来上がりイメージはこんな感じです↓
パーティクルクラスを作る
particle_class = function(x, y) local obj = {} obj.x = x obj.y = y obj.r = rnd(4)+1 obj.angle = rnd() obj.speed = rnd(3)+2 obj.update = function() obj.speed *= 0.9 obj.x += obj.speed * cos(obj.angle) obj.y += obj.speed * sin(obj.angle) obj.r *= 0.9 end obj.draw = function() if obj.r < 0.3 then return end circfill(obj.x, obj.y, obj.r, 8) end return obj end particles = {}
パーティクルクラスはこんな感じにしてみました。今回はinit()はつくってません。
angleは飛んでいく角度、speedは飛んでいく速さです。どちらもランダムにしています。rnd()は引数無しだと0~1未満の範囲(小数点以下の数字)になるようなのです。
update()ではパーティクルの移動を行っています。またspeedとrは毎回ちょっとずつ小さくなるようにしています。
draw()のif文ではrがo.3より小さくなったら表示しないで終わりにします。これはcirc()が半径0でも1ドット表示するのでこうしました。
最後にパーティクル用のテーブル「particles = {}」を作っていますがここではまだパーティクルは作りません。
ミスったらパーティクルを作る
プレイヤーキャラが障害物に当たったら実際にパーティクルを作ります。
if hitcheck(obj.x, obj.y, obj.r, player.x+4, player.y+4, player.r) then sfx(0) player.isdead = true for i = 1, 20, 1 do particles[i] = particle_class(player.x+4, player.y+4) end end
enemy_class内の当たり判定のif文内でパーティクルを作成するようにします。
ここでは20個のパーティクルをプレイヤーキャラクターの位置に作っています。
パーティクルの更新処理
作ったパーティクルは_update()と_draw()で更新して描画します。
function _update() fcount += 1 if fcount > 32767 then fcount = 0 end player.update() for enm in all(enemies) do enm.update() end if #particles then for prt in all(particles) do prt.update() end end end function _draw() cls() player.draw() for enm in all(enemies) do enm.draw() end if #particles then for prt in all(particles) do prt.draw() end end end
ここで大事なのはゲーム開始時はparticlesテーブルは空っぽなことです。空っぽの状態(まだクラスが無い)ではクラス内のupdate()やdraw()は実行できないのでif文で中身があるか判定しています。
テーブルに「#」をつけるとテーブルの要素の数が分かるのでそれで判定します。判定の時「0」はfalseとして処理されます。そして「0以外」はtrueになります。なので比較式が無しでOKです。
今回for文がまたちょっと違う書き方になっています。↑の例で言うとparticlesの要素を一つ取り出してprtという名前でローカル変数を作っています。それをpartilclesの要素の数だけ順番にやってくれます。要素から取り出す場合はこの方が書く量が少なくて便利です。
※※※BOOTHでこのサンプルプログラムをダウンロードできます。
このプログラムが「sample6.p8」という名前で入っています。
加速と減速を作る
ここまでの内容ではキャラクターが画面内を自由に動けましたがそれではいまいちなのでボタンをアクセルとブレーキのような感じに使うように変更します。つまり画面がスクロールするようなイメージになります。なので障害物はそのスピードに合わせて迫ってくるようになります。
if btn(4) then speed -= 0.1 if speed < 0.3 then speed = 0.3 end end if btn(5) then speed += 0.02 if speed > 3 then speed = 3 end end
こんな感じになります(少し色変えました)。加速しているんが分かりますか?
プログラムの解説
まずspeedという変数を_init()に作ります。初期値は0.3にしておきます。
で、playerクラス内の操作をしている所の4番と5番の所でspeed変数の加算減算をしています。
あとはenemyクラスのupdate()内にあるy座標を加算している数字をspeedに変更します。
簡単ですね。
あとは効果音を入れるともっとそれっぽくなりますよ(自分でやってね)。
それと速くなるのに合わせてプレイヤーの画像も速く切り替わるとより雰囲気がでますね。
スコア的なものを表示する
これだけではゲームとしてやっぱり物足りないので移動した距離と時間を表示してみましょう。
-- _init() distance = 0 start_time = time() elapsed_time = 0 -- update() if not player.isdead then distance += speed elapsed_time = time() - start_time end -- _draw() rectfill(0, 0, 128, 8, 9) print("distance: "..flr(distance)/10, 2, 2, 8) print("time: "..elapsed_time, 70, 2, 8)
上記をそれぞれの関数に追加します。
distanceは移動距離を入れる変数です。
time()はPICO-8を起動してからの時間を取得できる関数です。とりあえずゲーム開始時に一度その時間を取って(start_time)、後は毎回現在との差(elapsed_time)を出して経過時間として表示します。
死んだときは更新する必要が無いのでif文で条件を付けています。
flr()は小数点以下を切り落とす関数です。10で割っているのは10ドットで1メートルということにするためです(単位表示してないけど)。
画面遷移を作る
死んだらゲームオーバー画面が欲しいですよね。そしてできればタイトル画面も欲しいですよね。なので作りましょう。
-- game function game_init() particles = {} speed = 0.3 fcount = 0 goal = 200 distance = 0 start_time = time() elapsed_time = 0 player.init() for enm in all(enemies) do enm.init() end music(0) end function game_update() if player.isdead then if time() - dead_time > 2 then mode = 2 end else distance += speed elapsed_time = time() - start_time end player.update() for enm in all(enemies) do enm.update() end if #particles then for prt in all(particles) do prt.update() end end end function game_draw() rectfill(0, 0, 128, 128, 1) player.draw() for enm in all(enemies) do enm.draw() end if #particles then for prt in all(particles) do prt.draw() end end rectfill(0, 0, 128, 8, 9) print("distance: "..flr(distance)/10, 2, 2, 8) print("time: "..elapsed_time, 70, 2, 8) end -- title function title_init() fcount = 0 end function title_update() if btn(4) then mode = 1 _init() end end function title_draw() cls() if fcount % 20 > 9 then print("press z to start", 30, 60, 4) end end -- result function result_update() if btn(4) then mode = 1 _init() end end function result_draw() rectfill(25, 55, 103, 80, 0) print("game over", 47, 60, 8) if fcount % 20 > 9 then print("press z to start", 33, 70, 4) end end -- main mode = 0 function _init() if mode == 0 then title_init() elseif mode == 1 then game_init() elseif mode == 2 then end end function _update() fcount += 1 if fcount > 32767 then fcount = 0 end if mode == 0 then title_update() elseif mode == 1 then game_update() elseif mode == 2 then result_update() end end function _draw() if mode == 0 then title_draw() elseif mode == 1 then game_draw() elseif mode == 2 then result_draw() end end
かなり長いプログラムになっていますが簡単に説明します。
まず、_init()・_update()・_draw()・の中身を切り取ってgame_init()・game_update()・game_draw()に移します。
で元の関数にはif文を追加します。このif文はmode変数で切り替わるようになっています。このmode変数はゲームが何の画面かを入れておく変数です。このmode変数の数字を変えると画面が切り替わります。
あとはtitle_○○()とresult_○○()関数を作って_init()・_update()・_draw()の各モードの場所に記述します。
ちょっとプログラムが見づらいですが頑張ってください。
_update()内にあるfcountはどの画面でも使われるのであそこで処理するようにしています。
game_update()内にあるプレイヤーの生死の条件の所に少し変更があります。
if player.isdead then if time() - dead_time > 2 then mode = 2 end else distance += speed elapsed_time = time() - start_time end
プレイヤーが死んですぐにゲームオーバー画面が出てしまうとパーティクルの演出が見れないのでちょっと時間をおいて切り替えるようにしています(dead_timeは死んだときに時間を入れておく)。
またparticlesはゲーム開始のたびに空にしたいのでgame_init()内に移動しています。
ゴールを作る
進むだけだといまいちな感じなのでゴールを作って記録を取るようにします。
-- game_init() player.init() goal = player.y - 1000 -- game_update() goal += speed if goal > player.y + 4 then music(-1) mode = 2 sfx(7) if elapsed_time < record then record = elapsed_time end end
ゴールの座標を入れるgoal変数を作ってgame_init()内のplayer.init()の後にplayer.yからゴールまでの距離(ここでは1000)を引いた位置を入れます。
あとはgame_update()内でspeedを足していってplayer.yより大きくなればゴールです。
ゴールはline()でgoalの位置に表示するように作ってください。
ゴールの際に新記録ならrecordに記録(elapsed_time)を保存します。recordはgame_init()で初期化すると記録が消えてしまうのでゲーム開始時に初期値を入れておいてください。
これでゲームは一応完成しました。
★私のサイトで遊べます
今回作ったゲームは私のゲームサイトで公開しています。
出来上がりがどんなものになるのか気になる方はぜひ遊んでみてください(‘ω’)ノ
おしまい
PICO-8のゲームの作り方について一通り説明しました。
作り方に関してはいろいろなやり方があるので私の紹介した作り方も一つの例に過ぎないです。
もっと効率のいい作り方やプログラムの記述方法もあると思うのでSPLOREで他の人のプログラムを見て研究したりして頑張ってください。