WEB制作とゲーム開発のブログ

INWAN'S LABO

PICO-8で遊ぼう!【購入方法からゲームの作り方まで解説】

PICO-8でゲーム開発に挑戦してみよう!

PICO-8は1980年代に大人気だったゲーム機「ファミリーコンピューター」のようなレトロなゲームが作れるゲームエンジンです。

レトロゲーム制作に特化しているゲームエンジンなので制限されている所が多いですが、そのおかげて楽に作れる部分も多く初心者でも簡単にゲームを作ることができます。

PICO-8は有料で15ドルほどしますが私個人が使ってみた感想として間違いなく15ドル以上の価値があると断言できます。

というわけでPICO-8をゲームエンジンとして超おすすめしたいのでばっちりがっつり解説していきます。

 

※ちなみに筆者はPICO-8でこんなゲームを作ってみました。

 

レトロ感が最強のゲームエンジン

まず、何がレトロ感を出しているかというと解像度です。PICO-8の解像度は128×128ドットしかありません。これは何とあのファミコンの半分です(ファミコンは256×240)。さらに初代ゲームボーイ(160×144)よりも小さい!

そして使える色が16色しかありません。

この解像度の低さをプラスととるかマイナスと取るかは人それぞれだと思いますが、解像度が低いことの最大の利点は「お絵かきが下手でもどうにかなる」って点です。「ゲーム作りたいけどキャラとか作るの無理だし。。」みたいに思ってる人に断然優しいのが低解像度です。

そして素晴らしいことにこの低解像度の絵(ドット絵)を描く専用のツールがPICO-8に入っているので他にソフトを用意する必要がありません。

音楽もレトロ感たっぷりのピコピコ音です。そしてこのピコピコ音楽が作れる音楽作成機能もあります。

作った画像や音楽はpngファイルやwavファイルにして書き出すことも可能です。

ツールとしてだけでも15ドル分の価値はあります。

 

PCIO-8はいろんな動作環境を選べる

ゲームを作っても動作する環境が限られているとなかなか人に遊んでもらえなくて困ります。しかしPICO-8は作ったゲームをいろんな形に出力できるので様々な環境で遊ぶことができます。

パソコン(windows、Mac、Linux)の実行ファイルにすることもできますし、ブラウザゲームとしても出力できます。ブラウザゲームはスマホ上では自動でバーチャルパッドが表示されるようになっています。バーチャルパッドがデフォでついてるのはかなりポイント高いです(他のゲームエンジンでは自分で設定する必要がある)。

 

PCIO-8はいろんなゲームの中身が見れる

PICO-8にはSPLOREという機能があります。これはPICO-8用の掲示板に投稿されたゲームを遊ぶことができる機能です。

PICO-8では作ったゲームはカートリッジと呼ばれていて、SPLOREでカートリッジをPICO-8内にダウンロードして遊ぶことができます。

このカートリッジ、実は中身が全部見れます!!画像も音楽もプログラムも!!!

そして保存もできます!!

これの何がすごいって世界中のすごい開発者の技を見れるってことです。

特にゲームプログラムなんて他の人が作ったものを見る機会なんてなかなか無いと思います。これはものすごい付加価値ですよ!!!

他の人の作ったゲームを改造したりしながらじっくり勉強できるのはものすごい利点だと思います。将来プログラマーを目指してる学生にとってはすごい教材になると思います。

 

PICO-8を購入しよう!

さて、ここまで読んでPICO-8使ってみたくなったと思うので早速購入しましょう。

公式サイトが残念ながら英語しかないので自力でどうにか頑張るか翻訳を駆使するかして切り抜けてください。

とは言っても下の画像の場所でメールアドレスを入力して支払方法を決めるだけです。

PICO-8購入画面

メールアドレスにダウンロードの案内が来るので間違えないように注意してください。

 

支払方法はカード・paypal・amazonと選べます。

私は支払いをPayPalでしたんですがPayPalだとダウンロードできるようになるまで1日くらいかかりました。何か手続きに時間がかかるらしいです。

 

いくつかメールが来ますが最終的に手続きが完了すると下のようなメールが来るのでリンクをクリックしてダウンロードページに行ってダウンロードしてください。

pico-8購入完了

 

さっそくPICO-8を起動しよう!

PICO-8を起動すると下の画像の画面が表示されます。

pico-8起動

これはコマンドプロンプトと呼ばれるもので昔のパソコンみたいな画面です。ここに文字を打ち込んでいろいろ行うようになっています。プログラムのロードとか実行とかはここで行います。

この画面でESCキーを押すと以下の画面が表示されます。

pico-8開発画面

これが開発画面です。右上表示されているアイコンでツールを切り替えます。

ここでESCキーをもう一度押すとさっきのコマンドプロンプトに戻ります。

 

PCIO-8のデモゲームを遊んでみよう

コマンドプロンプトで「help」と入力してenterを押すと何やら英語が表示されます。

pico-8ヘルプ

PICO-8で使うコマンドの一覧です。大文字で表示されていますが入力は小文字で行ってください。

ここにある「install_demos」を入力するとデモカートリッジがインストールされます。

デモはdemosフォルダにインストールされているのでカレントディレクトリを移動する必要があります(ここから少し聞きなれない言葉が多く出てきますが頑張ってください)。

カレントディレクトリは現在自分がいるフォルダのことです(ディレクトリとフォルダは同じ意味)。今はルートディレクトリにいるんですがデモはdemosディレクトリにあるのでdemosディレクトリに移動する必要があります。そのコマンドが「cd ディレクトリ名」です。「cd demos」を実行するとdemosディレクトリに移動します。※cd コマンドの詳しい使い方はちょっと説明が面倒なのでググってください。

ここで今度はインストールされたものを確認するためにフォルダの中身を見る「dir」コマンドを使います。もしくは「folder」と入力するとフォルダーが開いて見れます。

この一覧の中にある拡張子が「.p8」になっているものがPICO-8のカートリッジです。

カートリッジの実行

次にこのカートリッジを読みこんで実行してみましょう。

カートリッジは「loadカートリッジ名」でロードします。この中で好きなものを一つ選んでロードしてください。おすすめはJELPIです(「load jelpi.p8」と入力してみましょう)。そして「run」と入力、もしくはCTR+Rでカードリッジを実行します。

するとゲームが起動して画面が切り替わります。

PICO-8のデモゲーム「jelpi」

終了したい場合はESCキーを押してください。また起動したい場合はCTR+Rしてください。

ゲームは矢印キーとZ、Xキーそしてenterキーでを使います。パソコンで使えるゲームコントローラーも使うことができます。

 

プログラムの編集について

ESCキーを押してゲームを止めたあともう一度ESCキーを押すとエディタ画面になります。するとゲームのプログラムが表示されます。

エディター

ここでプログラムを編集を行えるんですがこの画面では非常に見づらいです。なのでプログラミングはテキストエディターを使いましょう。

テキストエディターはプログラミングの作成などに使われる文字入力ソフトです。持っていない方はダウンロードしましょう。おすすめは「visual studio code」です

インストール後に「pico-8」のプログラムをわかりやすく表示してくれるように拡張機能をインストールしましょう。左上に並んでるアイコンの一番下のブロックみたいな四角をクリックして「pico-8」と検索すると拡張機能が出てきます。その中でとりあえず一番評価の高い「pico8vscodeeditor」を入れて有効化しておきましょう。

テキストエディタで開いた場合(ファイルをドラッグ&ドロップすると開きます)一番上に

pico-8 cartridge // http://www.pico-8.com

version 32
__lua__

と書かれた部分は絶対に消さないでください。

また下にある

__gfx__
なとと書かれた部分よりしたの数字がたくさん並んだ部分は画像データや音楽データなのでこれも絶対に消さないでください。

SPLOREでゲームを遊んでみよう

さて最後に最初の方で紹介したSPLOREで他の人が作ったゲームを遊んでみましょう。

コマンドプロンプトで「splore」と入力すると起動します。

その画面で左右キーを押すと画面がいろいろ切り替わります。その中に「FEATURED」と書かれたところがあります。ここで上下キーを押すとゲームが選べます。好きなのを選んでZまたはXキーで実行できます。

めちゃくちゃたくさんゲームがあるのでこれらのゲームが遊べるゲーム機としてだけでも15ドル以上の価値があると思います。

ここで遊べるゲームは当然PICO-8製なので勉強すれば自分でも同じようなものが作れるようになります。

 

プログラムで文字を表示する

ここからは実際にPICO-8でプログラミングしていきます。まずは文字の表示からしてみましょう。

プログラミングで最初にやる「hello world」ってやつですね!

PICO-8を起動したらESCキーを押してエディターを開きます。そこに

cls()
print("hello world", 60, 60, 7)

PICO-8エディタ画面

と入力してCTR+Rを押してください。

下の画像のように表示されると思います。

内容を簡単に説明すると、cls()は表示されているものを全部消してくれる関数です。

※関数は命令みたいなものです。また命令時に値を渡すことがありますがこれを引数と言って()内に書きます。

print()は文字を表示する関数です。引数は表示する文字列、x座標、y座標、色になっています。とりあえずこれらの引数を変えて遊んでみてください。ただしいくつかルールがあります。

  • 文字列は必ず「”」または「’」で囲むこと
  • 日本語は使わない(入力できなくなります)
  • x、y座標はどちらも0~127(これ以外は画面外になる。)
  • 色は1~15(0は黒で見えない)

変更したらまたCTR+Rで実行してください。

プログラムを保存する

では作ったプログラムを保存しましょう。

で、保存先ですが練習のプログラムをまとめて入れておくフォルダを作ってそこに保存するようにしましょう。

フォルダを作るにはコマンドプロンプトで「mkdir フォルダ名」と入力します。好きな名前を付けて作ってください。

フォルダを作ったら「cd フォルダ名」で移動します。そして「sample ファイル名」でプログラムを保存します。名前はなんでもいいですが

save sample1

とかにしておくと良いかと思います。

一度名前を付けると次回からはCTR+Sで保存できます。

コマンドプロンプトで「dir」か「folder」と入力して作ったファイルがあるか確認してください。

ファイルを作ったら今後はテキストエディタで開いて編集できます。

ゲームの更新処理

では次はもうゲームっぽいことをやっていきましょう。

PICO-8ではゲームに使われる重要な関数が3つあります。

  • _init()・・ゲーム開始時に最初に実行される
  • _update()・・更新処理。1秒間に30回行われる
  • _draw()・・描画処理。画面を書き換えるタイミングで呼ばれる(_updateの後)

この3つの関数は自動的に実行されます。

とりあえず実際に作ってみましょう

function _init()
  x = 0
  y = 0
end
function _update()
  x = x + 1
  y = y + 1
end
function _draw()
  cls()
  print("a", x, y, 7)  
end

さっき作ったプログラムの2行を消して↑のように入力して実行してみてください。Aの文字が左上から右下に移動していくはずです。

ざっくり説明するとx、yは座標を入れておく変数(入れ物)でゲーム起動時に_init()で両方とも0が入ります。そして_update()で座標が毎回1ずつ増えます。で、_draw()で毎回画面を一度奇麗にしてからAの文字をx、yの位置に表示しています。

関数の作り方

functionは関数を作るときに使う言葉です。functionの後は関数名で()を付けます。そして関数の終わりはendで閉じます。

functionとendの間が関数の中身です。この中身は中身だと分かりやすいように右に寄せます(インデント)。これはTABキーでする派とスペース2回でする派があります。ちなみにこの2派はキノコの山とたけのこの里の争いと同じくらい争われています(知らんけど)。

画面外に出ないように条件を付ける

update

今はAが画面外まで行ってしまいます。これを条件を付けて画面外に行かないように変更しましょう。

function _init()
  x = 60
  y = 0
  vx = 1 --追加
  vy = 1 --追加
end
function _update()
  x = x + vx
  y = y - vy
  if y < 0 or y > 127 then --追加 
    vy = -vy --追加 
  end --追加
  if x < 0 or x > 127 then --追加
    vx = -vx --追加
  end --追加
end
function _draw()
  cls()
  print("a", x, y, 7)  
end

「–追加」と書かれてる行が今回追加した部分です。ちなみに–はコメントでこれ以降の文字はプログラムに影響しません。※pico-8内ではひらがなとカタカナが使えますが漢字と濁点は使えません。↑をコピペすると文字化けします。

vx、vyはAの移動量です。この移動量を画面外に出そうになったときに+ーを反対にして画面外に出ていかないようにしています。

if文と条件

if文は指定された条件が正しいときにthen以下の処理を行います。条件はifとthenの間に書きます。

上の例では条件は2つあってy < 0とy > 127です。これはどちらか一つが満たされればいいのでorでつないでいます。もし両方満たす必要があればandを使います。

ifの終わりはendで閉じます。

数値を変えて遊んでね

今回はここまでです。

更新処理を使うことで動きが出てゲームっぽくなりましたね。

あとは数値をいじくって遊んでみてください。小数点以下も使えます。条件式も変えてみてください。

 

いろんな図形を描いてみよう

単純な線や円や四角は関数で簡単に書けるようになっています。

描くだけなら簡単なので一気に紹介します。

cls()
x = 10
y = 10
col = 7
pset(x, y, col)
y = 20
line(x, y, x+20, y+5  , col)
y = 30
rectfill(x, y, x+20, y+20, col+1)
rect(x, y, x+20, y+20, col)
x = 20
y = 70
r = 10 
circfill(x, y, r, col+1)
circ(x, y, r, col)

↑を入力して実行すると下のように表示されます。

PICO-8で図形を描く

さっくり説明するとまずは変数のx、yが座標です。colは色、rは半径です。

関数はpset()がピクセル(点)、line()が線、rect()は四角の線のみでrectfillが塗りつぶしあり、circ()が円の線のみでcircfill()が塗りつぶしありです。

line()は最初の2つの引数が線を引き始める場所、次の2つが線を引き終える場所です。

rect()の最初の2つは四角の左上の点、次が幅、その次が高さになります。

円と四角がそれぞれ2つの関数が重なる位置になってますが塗りつぶしの上に線が表示されています。プログラムでは後に実行されたものが上に表示されます。

とりあえず数字をいじって遊んでください(‘ω’)ノ

図形を動かそう!

描くだけなんてのはプログラムでもなんでもないしゲームっぽくもない!やっぱ動かしたいよな!!!

というわけで前回使った_update()と_draw()を使って動かして遊びましょう。

function _init()
  x = 0
  y = 0
  w = 1
  col = 7
end
function _update()
  w += 1
  y += 1
end
function _draw()
  cls()
  line(x, y, x+w, y , col)
  if y > 127 then
    y = 0
    w = 0
  end
end

これは線が下に移動しながら広がっていく感じになります。簡単ですね(*‘∀‘)

他のも同様に作っていろいろ動かしてみてください(‘ω’)ノ

もっとすごい動きをつける!!

やっぱさ、ゲームってもっと動きすごいじゃん?

なんかクネクネしたりウネウネしたりボヨンボヨンしたりなんかすごいじゃん?

ということでそれっぽいやつをちょっとやりたいと思います。

そこで三角関数ですよ

数学嫌いな人もいるとは思いますが、必要なんですよ、数学(´・ω・`)

まぁでも今回は三角関数でちょっと動きつけるくらいで大して難しくないので頑張りましょう(書いてる私もほんとは数学全然できないんです(^^;))

function _init()
  x = 64
  y = 0
  r = 15
  col = 7
  angle = 0
end
function _update()
  y += 1
  angle += 0.02
  if angle > 1 then
    angle -= 1
  end 
end
function _draw()
  cls()
  circ(x + cos(angle)*50, y, r + sin(angle)*10, col)
  if y > 127 then
    y = 0
  end
end

↑を入力して実行するとこうなります。

図形動かす

すごいっしょ?(`・ω・´)

なんか急にめちゃくちゃすごいことしちゃってる感じでしょ?

PICO-8のcon()とsin()

PICO-8のコサインとサインはcos()とsin()に角度を引数で渡すと値を返してくれます。

ただこの角度が360度の度数ではなく0~1です。0が0度で1が360度です。だから90度だと0.25です。

分かりづらいですが実際ゲーム内で「ここは絶対120度!」みたいに自分で指定することはほぼ無いです。だいたい式を書いておいてそれが計算されて表示されることがほとんどです。

なのであんま気にせずいじくり倒してみればなんとなくわかるよ!!( `ー´)b

いろいろ変えてやってみよう!!

もうここまでできればあとはx、y、幅、高さあちこちにcos()とsin()をぶち込んで遊ぶだけだぜ!!

PICO-8で図形を動かしてみる

というわけでいろいろ作ってみたんで皆さんも同じようなもの作って遊んでみてね。

 

スプライトをつくってみよう

PICO-8を起動したらESCキーを押してエディタ画面にして右上にあるアイコンの中から左から2番目を選んでください。

するとスプライト編集画面になります。

PICO-8スプライトエディターの画面構成

操作を簡単に説明すると下にある「画像エリア」の絵を描きたい場所を選んで、選んだ場所が「画像編集エリア」に表示されるのでそこでお絵かきします。編集している画像の番号が「画像番号」に表示されます。この番号をプログラムで使います。

右上にあるカラフルなのがパレットです。使える色はそれですべてです。それ以外の色は使えません。

それから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」という名前で入っています。

 

障害物との当たり判定をつけよう

実は当たり判定の基本の話は別の記事にまとめてあるのでまずそっちを一度読んでください。今回はこの記事内にある「円と円の当たり判定」を使います。

で、当たり判定をつける前に当たったときに当たったことが分かるように鳴らす効果音を先に用意しましょう。

エディタ画面で右上にあるアイコンの中の◁みたいなのを選びます。するとサウンドエディタが開くので下の画像のような感じで適当にマウスでなぞってみてください。

PICO-8サウンドエディタ画面

スペースキーを押すと再生できます。

サウンドの作り方についてはまた別の記事を書く予定なのでとりあえず今はこれだけで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で他の人のプログラムを見て研究したりして頑張ってください。

 

関連記事

tag:

◇ Posted by いんわん