WCE blog

早稲田大学公認 総合デジタル創作サークル 早稲田コンピュータエンタテインメント

PICO-8で遊ぼう!(前編)【2021年WCE新歓ブログ 第三回】

どうも、WCE8期のNYLONです。今回は私がPICO-8を用いたゲーム作りについて解説します。ゲームを作るのは凄く楽しいのでこの記事を読んで少しでもゲームの作り方がわかってもらえると嬉しいです。春から大学に入るプログラミングは全くの初心者 (自分も大学からプログラミングを学びました。) でも読めるように、いろいろ書いてしまったので二回に分けて解説する予定です。

我々のサークルについてもっと知りたい方はこちらの記事をご覧ください。▼

WCEってどんなサークルなの?【2021年WCE新歓ブログ 第一回】 - WCE blog

 

1.PICO-8とは

前時代的なゲーム現代風な手法で作成できるゲームエンジンです。昔のファミコンじみたゲームを手軽に簡単に作ることが出来ます。またコミュニティも充実していて作ったゲームを公式サイトから公開したり、誰かが作ったゲームを遊んだり、プロジェクトファイルを見たりできます。詳しくはPICO-8の公式サイトをご覧ください。▼

www.lexaloffle.com

 

2.仕様

ゲームに必要なプログラムイラストSE (効果音)BGMは全てPICO-8上で作れます。プログラミング言語Luaを用い、Lua初心者でもわかりやすい素直な言語です。イラストは8*8、16色のドット絵を256枚使うことが出来ます。SEやBGMは同時に4音鳴らせる仕様だと思われます。波形は8種類あり簡単にそれっぽい音が作れます。コードを書くのも簡単で実行も早いので、どんなゲームエンジンより快適にスムーズに制作が進められる点は優秀です。

デメリットとして制約がかなり多いことであり、それはちょうどいい手軽さや諦めを与えるメリットにもなり得ますが、扱える数値の上限が32768 (=2^15) だったり、制限の多さから頭を悩まされることも多いです。

PICO-8の制作物はさまざまな形にできますが、基本的にはHTMLJavaScriptで出力して自分のウェブサイトなどに張り付けることが多いです。もちろん各OSに合わせた形式の実行ファイルにすることもできます。

 

3.今回紹介するゲーム

今回は3月中に我々のサークルのゲームジャム (短期間でゲームを作るイベント) で制作した"SHIP n' GRAVITY" (https://www.lexaloffle.com/bbs/?pid=shipandgravity) というゲームを紹介しようと思います。このゲームはだいたい2日くらいで作りました。簡単な割にいろいろと工夫したので面白いゲームだと感じてもらえれば幸いです。

 

4.ゲームデザイン

まず今回のゲームジャムはテーマが「宇宙」と決まっていて初めから宇宙を題材にすることは決まっていました。しかし、宇宙ネタだと安直にSTGを作ってしまいがちですが、ネタ被りを避けるため、重力で引っ張られるという要素を加えることにしました。

ここで、PICO-8の計算力だと二乗計算などはすぐ数値の上限に達してしまうため、万有引力のような挙動はかなり簡略化して実装しています。実際、ゲームとしてユーザーが感じるアクションが直感的で爽快であれば、どういう実装が裏でなされてようがあまり関係ないです。

あとは宇宙の表現として多重スクロールに見える星の配置やロケットのアニメーションにこだわり、UIやロゴは見やすくてかっこいいものになるよう心掛けてデザインしました。このあたりの配慮が突貫的で簡素に作られたゲームの割に、丁寧に作られているように感じさせていると思います。ゲームの見た目は面白さや爽快感を与えるのに大事ですが、作品全体のクオリティの印象もユーザーに与えるので、演出などはかなり細かいところまでこだわったりすることが重要です。

その他ゲーム自体のデザインについてですが、耐久っぽいゲームにするとユーザーがパッシブに避けるだけのゲームになりそうだったので、時間制限を設けて積極的に敵を倒すようにしました。ただし、それだけでもゲームの奥行があまりないように感じたので、アイテムで時間制限を伸ばせるようにしました。これによってユーザーに重力がかかる中、アイテムを取りに行く動作を狙わせることができ、「制限時間を長くすれば敵をたくさん倒せるが、そこまでで被弾してゲームオーバーになってしまうとスコアが伸びない」というリスクとリターンを課すことができました。ゲームにおいてリスクとリターンはユーザーに考えて行動する余地を与え、単純な動作から積極的に遊ぶゲームへと昇華させる重量なキーポイントだと思っていて、ゲームに奥行きが与えられると私は感じています。

レベルデザインとして惑星の発生や敵の発生タイミングはそこまで練り切ってはいないですが、難しいけど慣れれば対処できるくらいの量や位置になっていると思います。自機のファイアレートは音としての聞こえの良さで決めたのでそれに合わせて敵の量を調整しました。アイテムは5秒に一回発生し5秒間制限時間を延ばせる仕様にしていましたが、それだと少しプレイ時間が長引くときがあったので発生頻度を6秒に調整しました。

 

5.プログラミングについて

自分はプログラミングについて詳しいというほどでもないですが、実際にコードを紹介する前にざっくりと解説します。

プログラムとは人間がコンピュータにやらせたいことを書いた指示書で、それを書く作業をプログラミングと言います。

しかしコンピュータはなんでもできるわけではなく、おおまかには計算しかできないのでプログラムには計算するものとして変数と計算の方法として関数を使うことが多いです。

関数は一般的に数学で用いる関数とは若干違いますが、PICO-8で用いるLuaではだいたい以下のような作りです。

function aaaaa(x,y,z,・・・)
 やりたいこと
end

ここで aaaaaを関数、 x, y, zなどやりたいことに必要な変数をを引数 (ひきすう) と言います。PICO-8ではfunction~endなど定型文はピンクPICO-8に存在する関数は黄緑、数値などは水色でハイライトします。引数は無くても関数は成立します。また関数の実行結果に値を返したいとき

function aaaaa(x,y,z,・・・)
 計算
 return 結果
end

 みたいになっているとき、結果のところにある変数を戻り値 (もどりち) と言います。例えば以下の関数は二つの値を足し算します。

function sum(x,y)
 return x+y
end

その他の詳しいことは実際にコードを見ながら適宜解説します。

 

6.実際のコード

先にコードを紹介する前にスプライト (イラスト素材) を見せます。

f:id:WCE:20210318121529p:plain

作成したスプライト (ゲーム用イラスト素材)

これは自分で手打ちしたドットイラストですがコード内でたびたび呼び出されます。基本的にはspr関数で呼ぶことができ、

spr(n,x,y,w,h)

で n番目のスプライトを位置 (x,y) に幅 w 高さ h で描画することが出来ます。

ちなみにゲーム画面は幅128ピクセル、高さ128ピクセルで y座標は下向きに進みます。スプライト番号は左上から右に8*8のマス目ごとに0から15を表し、下の段へと進みます。上の画像では0~2にロケット (これは縦に長く16~18の領域も使っています) 、8番目にアイテム、32~33に敵のスプライトが描かれてます。残りはロゴなど大きい塊で用いていますが、左上の数字だけ説明すると64にタイトルロゴ、96に数字、128にゲームオーバーが描かれてます。

ここからは実際に書いたコードを紹介していきますが、私が 10時間程度で 強引に 書ききったコードであるため、かなり汚くて読みにくいところもあると思いますが、参考程度になればいいかと思います。それではある程度のブロックに分けて説明していきます。

 

6.1全体的なこと

--ship n gravity
version=2.0

function _init()
 cartdata("shipngravity")
 game=false
 init_star()
 t=0
 ready=0
 if version==dget(1) then
  hscore=dget(0)
 else
  hscore=0
 end
 dset(1,version)
end

function _update60()
 if ready>0 then
  ready-=1
  if (ready==8) readyf()
 else
  if game then
   if pl.life>0 and timelimit>0 then
   	update_game()
   else
    if not gameover then
     gameover=true
     if (timelimit==0) bonus=5*pl.life score+=bonus
      music(32)
      for s in all(shot) do
      add_ptcl8(s,12)
      del(shot,s)
     end
     newscore()
    end
    if btn(🅾️) or btn(❎) then
     ready=16
     readyf=init_title
     sfx(0)
    end
    if abs(pl.y-64)<128 then
     pl.y+=pl.vy
     pl.vy=1
     pl_mot()
     add_smoke()
     add_smoke()
    end
    update_move_only(enemy)
    update_move_only(ast)
    update_move_only(item)
    foreach(ptcl,update_ptcl)
   end
  else
   if btn(🅾️) or btn(❎) then
    ready=16
    readyf=init_game
    sfx(0)
   end
   t%=120
   t+=1
  end
 end
end

function _draw()
 cls()
 foreach(star,draw_star)
 if game then
  draw_game()
 else
  draw_title()
 end
 if ready>0 then
  local y=(8-abs(ready-8))^2
  rectfill(0,64-y,128,64+y,1)
  rectfill(0,64-y+4,128,64+y-4,7)
 end
end

長いですが順に解説していきます。まずPICO-8には3つの特別な関数があります。

  1. _init関数
  2. _update関数(_update60関数)
  3. _draw関数

まず_init関数はゲームが起動したときのみ呼び出されます。_update関数は1/30秒に一回 (_update60は1/60秒に一回) 呼び出される関数でゲームの更新をします。_draw関数は_update関数が終わるたびに呼び出され結果を描画します。 (一応どちらかだけでも成立しなくもないですが、_updateと_drawはどちらも使う人が多いです。)

上から見ていきます。Luaでは "--" でコメントアウト (計算に使わない文の記述) ができるらしいです。 (自分はPICO-8以外でLuaを使う機会が無いのでLua自体の仕様にはあまり詳しくありません。) version=2.0はversionという変数の値を2.0にしています。プログラミングでは「これをあれにする」とき「これ=あれ」という形で書き、この作業を代入と呼んでいます。version=2.0については_init関数内でもいいのですが、見やすいので一番上に書いてます。この記事の執筆現在では2.0バージョンを公開しています。今後ゲームをアップデートするたびにこの数値は変えていきます。

以下、_init関数を見ていきます。PICO-8にはデータを記録する機能があります。これによってハイスコアなどを保存できます。データを書き込んだり、読み込んだりする際にまずcartdata関数を呼び出す必要があります。cartdata関数の引数は文字列で今回の場合、名前がshipngravityであるデータが呼び出されるか、作られるかします。

次にブール型変数のgameがfalseという値になっています。ブール型というのは変数の型の一つでtruefalseの二値のみを取ります。今回のゲームではgame値がtrueのとき、ゲームが動き、falseのときはタイトル画面が動きます。起動直後はタイトル画面を映してほしいのでfalseにしています。

次にinit_star関数を呼び出していますが、これは背景に星を描くため星の準備をしています。この関数の中身についてはまた後で紹介します。その後 t (だいたい時間を管理する変数の名前は t) を0にして、readyという変数も0にしています。readyは画面が切り替わるときに使ってます。

その次にif文が使われています。if文とは「もしこの条件が成立していればこのことをする」という処理に使う表現です。if文の条件にはブール型が使えて、trueならif文内の処理が行われます。if~elseでそうじゃないときの処理も行えます。今回の

 if version==dget(1) then
  hscore=dget(0)
 else
  hscore=0
 end

はversionとdget(1)が等しければ上の処理をします。ここで等式は"=="で表します。"="だと代入だったので二つ並べることで条件式を表しています。dget関数はデータを読み込む関数で今回は1番のデータを読み込ませています。1番のデータはバージョン情報で今のバージョンが記録されているバージョンと等しければハイスコアのデータも読み込ませています。 (hscoreがハイスコアを表す変数で0番目のデータにハイスコアのデータは保存されています。) もしバージョンが違う場合はハイスコアは0にしています。これによって今後バージョンが変わったときにいくら難易度を上げても昔のデータが残っているせいでハイスコアの更新が狙えない状況を避けています。

このif文のあとでバージョンの情報を記録しています。 dset関数はデータを記録する関数で

 dset(n,value)

でn番目のデータの値をvalueに上書きします。これで長いですが_init関数の処理は終わりです。

 

次に_update60関数を見ていきましょう。

これも長いですがおおまかには三層のif文になっています。

f:id:WCE:20210321111739p:plain

上から順にみていくとまず画面遷移処理の部分がありますが、これはreadyの値を1ずつ減らして8になったとき画面移行処理(readyf)が行われています。ここでは省略形を使っていますが ready=ready-を ready-=で表せます。変数自身からの更新について代入だと長くなってしまうのでこのような省略形が使えると短くて便利です。画面が切り替わるときに四角で画面を覆っていますがこれはready値をうまく使っているだけです。

f:id:WCE:20210321111225g:plain

readyが0のときはgameがtrueならゲーム画面、falseならタイトル画面を映しています。ゲーム画面中はプレイヤーのライフが正、もしくは時間制限が来ていないとき、update_game関数が動いてゲームが更新されます。これは中身については後で紹介します。

ゲームオーバーになってしまったら、gameover値をtrueにします。他にも方法はいろいろありますが、これによってゲームオーバーになった瞬間に一回だけ行う処理を行っています。その処理としては時間切れでのゲームオーバーならボーナスを付け加えたり、ゲームオーバー音を鳴らしたり (music関数で指定した番号の音楽が流れます。)、自機が出した弾を消したりしています。

ここでfor文が使われています。for文とは繰り返し処理を行うときに使う表現で簡単にはfor、i、iの初期値、iの最後が書かれることが多いです。イメージとしては Σ の計算みたいな感じでk=1からnまでみたいな公式になじみがあると思います。それをより抽象化したのがfor文です。for文は初めから終わりまで指定された処理を繰り返してくれます。しかし今回のfor文はもっと特殊で k みたいな変数無しに shot 全てにパーティクル (粒のエフェクト) を出したあと shot から消す、という処理をしています。この shot というのはテーブルと呼ばれるもので詳しくは後で解説します。

そのあとはボタン入力の部分です。PICO-8ではbtn関数があり、引数のボタンが押されているとtrueを返します。前までは引数は数字のみ (左:0, 右:1, 上:2, 下:3, z/〇:4, x/×:5) でしたが今は記号も入れらるので見やすさが増しました。〇ボタンか×ボタンが押されると ready の値を16にして画面遷移処理 (readyf) に init_title関数を代入して (関数に関数を代入することもできます。) タイトル画面に戻る準備をしています。このとき sfx関数が使われていますが、これは効果音を鳴らす関数です。

そのあとのif文は自機が画面内に見えているならという条件です。abs絶対値を表し、画面の縦幅は128なので中心64から十分近い間は更新を続けるという処理になっています。(128も取る必要は全くないですね...) その間プレイヤーのy座標を1ずつ下げています。 (画面は下向きが正なので1ずつ足しています。) ここで vy (y成分の速度) を使って下げているのはその下の pl_mot関数のためです。pl_mot関数は後で紹介しますが、自機のアニメーションの関数で速度に応じてアニメーションを変えているため、一応速度を使っています。あとは add_smoke関数で煙を出しながらプレイヤーは落下しています。これも中身については後で紹介します。二回しているのでゲーム画面の二倍の煙が出てます。故障感を出したかったから二回にしています。

最後に update_move_only関数が enemy (敵) と ast (惑星) と item (アイテム) に行われています。これも中身はあとで紹介しますが見ての通り動くだけの処理をしています。(ゲーム中は当たり判定があったりします。)

あとは ptcl (パーティクル) が更新されています。ここで foreachLua特有の関数で

 foreach(テーブル,処理)

でテーブル全部に同じ処理をさせます。これによってパーティクルは動いてます。

最後にタイトル画面ではゲームオーバーのときと同じでボタン入力が行われています。t (時間) も更新されているのはタイトルロゴをたまに点滅させるためです。

 

最後に_draw関数を見ていきましょう。

最初のcls関数はclear of screenの略で画面を全部黒にしてくれます。このあとforeach関数で星を描いていてどの画面でも星が瞬いてます。あとは状況に合わせてゲーム画面かタイトルを描いていてreadyが正のとき四角で画面を覆っています。

if文の中を見るとlocal yと書かれています。これはローカル変数という変数の使い方でプログラム内で部分的に使いたいときに使います。for文のiとかも同じ役割です。今回このyは四角で覆うのにしか使わないのでローカル変数に設定しています。yに変な値を代入していますが絶対値を取ったり、8から引いたり、二乗したりして、あの四角の挙動はできてます。その下のrectfill関数は長方形を描画する関数で

 rectfill(x1,y1,x2,y2,c)

 で画面の (x1,y1), (x2,y2) を対角線に持つ辺がx軸、y軸に平行な長方形を描けます。最後のcは色を表していてPICO-8では色は16色で0~15が色と対応しています。

これで今回紹介する分は終わりです。非常に長くなってしまいそうなのでこのあとは次回に回したいと思います。

 

7.最後に

自分は実は春から修士になる現在4年生ですが、4年間このサークルに在籍していて非常にいろいろな体験ができたと思います。当初はDTMがやりたくてサークルに所属したのですがいろいろなことがあって今は何でもやってます。うちのサークルは人も少ないし、サークル員の交流する機会も少なくなってしまうときもありますが、人が少ない分いろいろなことをやっているとありがたがられます。もちろん一つのことを極めているようなサークル員もいるし、自分のように好きなことをいろいろやっているサークル員もいます。サークルに所属しなくても創作活動はできますが、情報交換などサークルに所属するメリットもあります。うちのサークルはそうした最低限度のサークルの役割を持たせた個人主義的な団体だと考えていて外部から見てすっげーつまんなそうって思われてたりするらしいですが、自分は楽しい4年間を過ごせました。興味があったらぜひ見学に来てください。