1、2、3…無限大……繰り返しとテーブル – PICO-8ゲーム開発入門(8)


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

 

アイテムをたくさん置く

前回は、アイテムを一個取るたびに、別の場所にまたアイテムが出るというという簡単なゲームを作りました。ゲームとしては、これは退屈ですよね。もっとアイテムを一度にいっぱい出したいところです。スーパーマリオのように、画面上にいっぱい出ているコインをどんどん取っていったりする方が気持ちいいですよね?

今回は、こんなゲームを作ってみます:

たくさんのトマトがあらかじめ画面に敷き詰められています。これをオートマ坊やを動かしてどんどん取っていき、全部取るのにかかったタイムを競うゲームです。
前回のゲームでは、プレイヤーの位置を(x, y)アイテムの位置を(ix,iy)で表しました。アイテムをたくさん出すためには、アイテムの位置情報のための変数がもっと必要です。今回のゲームでは、横に16列、縦に16行で、全部で256個のトマトを画面に表示します。256個のトマトそれぞれに、変数を用意していったら大変です。

こんな感じでしょうか……?

i1x=0
i1y=0
i2x=8
i2y=0
i3x=16
i3y=0
…
i255x=112
i255y=120
i256x=120
i256y=120

省略しましたが、こんなふうに書き連ねていくなんて、やってられません。そこで「配列」を使って、同じような役目をする変数をまとめて扱います。しかし、Luaには他のプログラミング言語のような「配列」がありません。代わりに「テーブル」を使います。

 

テーブルとは

テーブルは、複数の値をまとめて入れられる変数です。それぞれの値には「キー」を割り当てて、キーで呼び出せます。ほかのプログラミング言語の経験者の方は、配列と辞書が一緒になったものと考えればわかりやすいと思います。

a={} -- テーブルを初期化
a[1] = 4 -- テーブルの1番に値4を入れる
a[2] = 3 -- テーブルの2番に値3を入れる
a[3] = "aiueo" -- テーブルの3番に値"aiueo"を入れる
a["fruit"] = "pineapple" -- テーブルのfruit要素に値"pineapple"を入れる
a.animal = "panda" -- テーブルのanimal要素に値"panda"を入れる
print(a[1]) -- テーブルaの1番の要素を表示
print(a[2]) -- テーブルaの2番の要素を表示
print(a[3]) -- テーブルaの3番の要素を表示
print(a["fruit"]) -- テーブルaのfruit要素のを表示
print(a.animal) -- テーブルaのanimal要素を表示

これを実行すると、

4
3
aiueo
pineapple
panda

a[“fruit”]とa.animalは書き方が違うだけで、同様にテーブルのキーに対応した値を指しています。ただし、数値のキーを使ってa.1というような書き方はできません。また、Luaのテーブルを配列のように使う場合の添字(数値のキー)は、ほかの多くの言語とは異なり、1から始まります。

テーブルのイメージ

テーブルの初期化と同時に、キー・値を複数設定しておく書き方もあります:

a={x=3, y=4, fruit=“banana”}

ところで、テーブルにはテーブルを入れることもできます。これを利用すると、アイテムの座標の管理がとてもしやすくなります。座標が(0,10)、(10, 20)、(20, 30)の位置にあるアイテムがあるとすると、下記のようにテーブルで座標データを表せます。

items={}
items[1] = {x=0, y=10}
items[2] = {x=10, y=20}
items[3] = {x=20, y=30}

これらのアイテムを画面に描くとしたら、こんな感じでしょうか:

spr(0, item[1].x, item[1].y)
spr(0, item[2].x, item[2].y)
spr(0, item[3].x, item[3].y)

テーブルのテーブルを使えば、たくさんあるアイテムの座標情報をきれいにまとめられそうです。次に紹介するループと組み合わせると、もっとコードを短くできます。

 

ループとは

ループとは繰り返しの処理です。以下のコードを見てください。

for i=1,5 do
 print(i)
end

これは、forから始まる行とendの行の間を、5回繰り返します。iはfor〜endの間でのみ有効なローカル変数で、その値は1から始まって、5になるまで繰り返しのたびに1ずつ増えます。これを実行すると:

1
2
3
4
5

繰り返しのたびに増える数値を、1以外にすることもできます。8ずつ増やすとしたら:

for i=0,120,8 do
 print(i)
end

iは最初は0で、毎回8ずつ増え、120以上になるまで繰り返されます。これを実行すると:

0
8
16
...
120

テーブル+ループ

さきほど、アイテムの座標のデータをテーブルのテーブルで表現しました。

items={}
items[1] = {x=0, y=10}
items[2] = {x=10, y=20}
items[3] = {x=20, y=30}

このアイテムの座標データ作りを、ループを使ってやってみましょう。PICO-8の画面いっぱいに、8×8のスプライトで描かれたアイテムを敷き詰めたいと思います。こんな感じ:

これらの座標データを作るコードは:

items={}
local i=0
for y in 0,120,8 do
 for x in 0,120,8 do
  items[i]={x=x, y=y}
  i += 1
 end
end

ここでは2重のループを使っています。16行×16列を1行ずつなめていく動きです。このループの中の流れは、以下のGIFアニメで理解できると思います。

これで16×16=256個のアイテム座標データを作る処理を、8行で書けました。次に、この256個のアイテムを画面に描画します。この処理はたった3行で済みます。

for i=1,#items do
 spr(0, items[i].x, items[i].y)
end

テーブルの変数名の頭に#を付けると、テーブルの中身の要素の個数を得られます。ここでは64です。さて、ここで、テーブルの中身の要素について繰り返す、forループの便利な書き方も紹介しておきます。

for item in all(items) do
 spr(0, item.x, item.y)
end

この書き方では、テーブル変数itemsの要素の個数分繰り返し処理が行われ、毎回1個ずつitemsの要素がローカル変数itemに入ります。

 

トマト収穫ゲーム

以上を踏まえ、前回のゲームに手を加えて、画面いっぱいのトマトを取りまくるゲームにしてみます。コードは以下のとおりです。スプライトは、前回のものと同じです。


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

-- timer

function update_timer()
 if #items>0 then
  time+=1
 end
end

function draw_timer()
 local centi=flr(time*10/3)%100
 if centi<10 then centi="0"..centi end
 local sec=flr(time/30)
 local min=flr(sec/60)
 sec=sec%60
 if sec<10 then sec="0"..sec end
 local col=7
 if #items==0 then col=rnd(16) end
 print(min..":"..sec.."."..centi, 2, 120, col)
end

-- collision

function distance(x1,y1,x2,y2)
 return sqrt((x1-x2)^2+(y1-y2)^2)
end

function collision(x1,y1,x2,y2)
 if distance(x1,y1,x2,y2) < 6 then
  return true
 else
  return false
 end
end

-- items

function place_items()
 local i=1
 for y=0,120,8 do
  for x=0,120,8 do
   items[i]={x=x+4,y=y+4}
   i+=1
  end
 end
end

function draw_items()
 for item in all(items) do
  spr(5, item.x-4, item.y-4)
 end
end

-- input

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 _init()
 place_items()
end

function _update()
 t+=1
 input()
 for item in all(items) do
  if collision(x, y, item.x, item.y) then
   del(items, item)
   sfx(0)
  end
 end
 update_timer()
end

function _draw()
 rectfill(0,0,127,127,13)
 draw_items()
 spr(s,x-4,y-4)
 draw_timer()
end

_update()の中で、del()という関数を使用しています。これはPICO-8独自の組み込み関数で、テーブルの要素を一つ削除します。

del(t, v)
・t: テーブル
・v: 削除するテーブルの要素

ほかのプログラミング言語の経験者の方は、心配に思うかもしれませんが、del()はテーブルの要素についてのループの中においても、安全に使用することができます。

トマトを全部取ったタイムを競うゲームにするため、タイマーの表示も組み込まれています。このコードはちょっと難しいかもしれません。1/30秒おきに1ずつ増えるカウンターを「分:秒.1/100秒」に分解する処理が行われています。また、前回同様、アイテムを取ったときに飛び散る粒子のコードはここには含まれていません。

テーブルとループを使って、たくさんのアイテムを短いコードで扱うことができました。次回は、敵キャラクターを登場させてみましょう。