前回の三日月爆発の改変です。
TIC君(?)にうらみはないです。処理がわかりやすいように配置しました。ごめんよ。
主な変更点は以下の通り
- 円描画のアルゴリズムを変更
- 爆発の大きさをランダムに
- 爆発の色をいくつかの中からランダム選択
- 爆発描画(の入れ物)を速いものと遅いものを用意
MIN_X,MAX_X=40,200
MIN_Y,MAX_Y=40,100
MIN_R,MAX_R=10,20
BOMBTIME=10
BGCOLOR,BOMBCOLS=0,{6,8,11,14,15}
ran,min=math.random,math.min
function bomb(x,y,br,bc)
local cnt=br*2
return function()
if cnt<0 then return true end
local r=min(cnt,br*2-cnt)
local c=(cnt>br) and bc or BGCOLOR
circb(x,y,r,c,"andres")
cnt=cnt-1 end end
function bombug(uptime)
local bb={}
bb.ut=uptime
bb.update=function(self)
self.ut=self.ut-1
if self.ut<1 then
self.ut=uptime
for i=#self,1,-1 do
if self[i]() then
table.remove(self,i) end end end end
return bb end
function testinit()
cnt=BOMBTIME
slowbb=bombug(6)
fastbb=bombug(2)
cls(BGCOLOR)
spr(1,50,4,-1,8,0,0,2,2)
end
testinit()
function TIC()
cnt=cnt-1
if cnt<1 then
cnt=BOMBTIME
newbomb=bomb(ran(MIN_X,MAX_X),ran(MIN_Y,MAX_Y),ran(MIN_R,MAX_R),BOMBCOLS[ran(1,#BOMBCOLS)])
if ran(1,3)==1 then
table.insert(slowbb,1,newbomb)
else
table.insert(fastbb,1,newbomb) end end
slowbb:update()
fastbb:update()
end
##円描画のアルゴリズム変更
これは自分で作った訳ではありません。というか円描画にアルゴリズムが複数あることを知らなかったです。以前に調べた「スプライトシートの任意の場所から色番号を取得」という関数のすぐ下に円描画が紹介されていました。
https://github.com/nesbox/TIC-80/wiki/code-examples-and-snippets
前回の三日月爆発では塗り残しがあったので、そういやあそこに円を描く奴があったなーと思い出して使用してみました。
TIC-80デフォルトの円描画命令(API)
circb(x,y,r,c)
x,y 中心座標、r 半径、c 色番号
今回使用したもの
circb(x,y,r,c,"andres")
アンドレスさんのアルゴリズム?
上記のコードには定義は載せていません。さっぱり分からなかったです。
ちょっと調べてみたのですが、円描画アルゴリズムは本当に奥が深そうですね。
これは軽い気分で触れちゃいけないやつだ‥‥深淵を覗いた気分です。
真円だけに。 ‥‥真円だけにな! し...
これにより塗り残しがなくなりました。
また、半径1で描画すると中心に穴が開くので、開始と終了に半径0を含めました。
##我輩は爆発描画命令(関数)である、名前はまだ無い。
あ、副作用が主目的の function call を個人的に「命令」と呼んでいます。
個人的にごっちゃにならないように書き分けているだけですので、適宜読み替えて下さい。
(返り値は使わないか補助的使用。Luaリファレンスマニュアルの3.3.6 に近い使い方の場合)
circb(x,y,r,c)
は中心座標x,y、半径r、色c の円描画命令
今回作ったbomb(x,y,br,bc)
は中心座標x,y、半径br、色bc、の爆発描画命令を返す関数
爆発描画命令はcircb
を1回実行するが必要なパラメータは作成時に与えられ、それを元に
計算するので ( ) を付けて実行するだけ。
例えば変数 b に代入して実行する場合の例。(背景色は0とする)
b = bomb(100,70,4,11)
b() -- circb(100,70,0,11)
b() -- circb(100,70,1,11)
b() -- circb(100,70,2,11)
b() -- circb(100,70,3,11)
b() -- circb(100,70,4,0)
b() -- circb(100,70,3,0)
b() -- circb(100,70,2,0)
b() -- circb(100,70,1,0)
b() -- circb(100,70,0,0)
b() -- true
前回では描画が終わっても単に return するだけでしたが、今回は打ち止めを示すtrueを返すようにしました。管理側でこれを受け取ると操作対象から外す(tableからremove)ように変更。
##何この・・・何?
前回は爆発描画命令は一つのテーブル bombs{}
に入れてまとめて処理していました。
for i,f in ipairs(bombs) do f() end
怠慢して描画が終わっている命令もそのままにして最大数を超えたら古いものから remove という‥。
今回は複数の更新頻度の異なる「爆発命令実行機能付きの入れ物」を用意することにしました。
そう、table に 操作(function)を持たせました、なにげに初めてです。
Lua初心者なら憧れる [変数名]:[操作] の表記が可能ですね。(メソッド? Luaリファレンス3.4.10)
文字列でなら [変数名]:gsub(pattern, repl)
みたいな感じで使ったことはあるのですが。
爆発描画の入れ物だからボム袋、すなわち bombug でいいな。
あ、今気づきましたが 重なるbを一つ省いたのは意図的だったけど bug じゃなくて bag じゃね?
‥‥再走(書き直し)要求には応えられない、本当に申し訳ない。
###table 走査時 remove の罠
今回は、爆発描画の半径をランダムにしたので、先に入れた命令が先に終了するとは限りません。
終了した命令は table.remove で消去するのですが、ここで ipairs で走査していると問題があります。
i | f | i | f | |
---|---|---|---|---|
1 | 赤爆発 | 1 | 赤爆発 | |
2 | 青爆発 | remove | ||
3 | 黄爆発 | 2 | 黄爆発(飛ばされる) | |
4 | 白爆発 | 3 | 白爆発 |
みたいな感じでリストに入っている場合、 ipairs で回して[2]青爆発が終了していたので remove すると
その時点で添字の割り当てが変更されて、次には黄爆発は飛ばされて、[3]白爆発が処理されます。
調べたのですが日本語の情報は見つけられず(低検索力)、英語は読解力不足で良くわかりませんでしたが
それっぽい情報が。色々解決策はあるようですが最も記述が簡潔そうなのは、ずばり
添字の大きい方から処理する、ようです。おーなるほどー。
for i=#self,1,-1 do -- 添字は命令数から1まで-1刻みで、
if self[i]() then -- 爆発命令を実行。返り値が true なら
table.remove(self,i) end end -- ボム袋から取り除く
微妙に美しくないけどしょうがないですかね。後、新しい爆発命令を追加する場合、
リストの最初に挿入することになるので少し遅くなるかも? ままえあろ
他の言語だとどのような処理になるのでしょうか。それ用のデータ構造があったりするのでしょうか。
爆発命令を追加する処理は今回は1種類しかないためメインループ(1フレームに1回呼ばれる TIC( ) )
内に記述しましたがこれも bombgenerator みたいな感じで別途定義した方がいいのかなあとは
思いました(記述するとは言ってな‥)
##まとめ
bomb(x,y,br,bc)
は対応先がcircb(x,y,r,c)
で 「命令(パラメータ)」なので
同様のもの、すなわち 命令withパラメータ を返すクロージャ(?)っぽい書き方が
相性がよく、自然に記述できた気がします。
ただ、bombug(uptime)
はデータ構造に操作(function)を持たせたら、
オブジェクトっぽくなるかなと思いましたが、これは全然別物な気がします。
(カプセル化(?)とか継承(?)とかもうそれ以前の段階の問題)
まあいいか、bug ってるし。
最後にメインループ内で(背景色ではない)灰色でのリフレッシュを追加した場合の結果を。
円しか描画していないことが分かりやすいかなと思います。
あ、例のゲームの移植は、当初の目標の1面プレイの再現がほぼ終わって、
音関係に手を出したらさっぱり訳わからなかったので、デモ(アーケード落ちゲーみたいな)や
別モードを追加を見据えて、また最初から書き直しています。