2020/06/24

LuaチョットワカルけどステマニのLua意味わからんユーザーのためのLua講座

こんにちは、A.Cです。
今回はStepMania用のLuaを書くにあたって知っておいた方が良い小ネタ的なことを書いていこうかと思います。

この記事はLuaについて少しは勉強していることが前提です。
変数、数値・文字列・テーブル、関数定義が理解できるくらいが基準です。

あとプログラミング系の記事書くとプログラミング警察の方が来られそうなので一応書いておきますと、
僕自身完全に理解しているわけでもないし、なんとなく雰囲気がわかる程度の書き方をしているので、
書かれている内容が厳密には間違っている可能性があります。
そこはご了承ください。


StepMania用のLuaを書くんだから同じStepManiaで使われているテーマのLuaや先人のコードを見て勉強するのが一番なんですが、正直な話、StepManiaでよく使われている書き方のコードってかなり独特でわかりにくいんです。

あれなんですよ、Lua使えると面白そうと思っている人は多いと思うんです。
で、ちょっとは調べて雰囲気を学んだ人もいると思います。
そして先人のコードはどうなっているのか覗いてみて…オーゥイミワカンネー

僕がLua触り始めたのはStepMania5がまだsm-sscだった頃なので割とマジで資料が公式テーマしかありませんでした
なので「なんかよくわからんけどこれがStepManiaの書き方なんだ」でテーマやら何やら作ってきましたけど、正直ちゃんと理解して作った方が覚えるのもかなり楽です。
※StepMania5.0.5の前がbeta4、beta1の前がAlpha3、Alpha1の前がpreview4、preview1の前がsm-ssc

例えば、曲フォルダの中にさらにフォルダを作って、
その中にdefault.luaを作成してからBGCHANGE(FGCHANGE)で呼び出せば
それが動作する
ってのは何となくわかるんじゃないでしょうか。

じゃあdefault.luaには何を書けばいいんでしょうか。
正解はActor Classを返却するコードを書けばいいわけです。

はい、よくわかりません。
じゃあ一旦StepManiaから離れましょう。

数値を返却するコードを書く
これならどうでしょう。
正解は下のようになるのですが、なんとなく意味がわかるんじゃないでしょうか。


return 334


ちなみにLuaは他の言語でよくある行末のセミコロン(;)は必要ありません。(書いても大丈夫です)
では次です。

文字列を返却するコードを書く
これもわかりやすいですね。
全く同じ回答なのもアレなんでちょっと変えてみます。

local function Moji(moji)
return moji..moji
end

return Moji('文字')

今回は関数を定義しています。
Moji()の引数に与えられたテキストを2回続けて返します。
..は結合を意味します。
文字列の場合は単純につなげるものだと思ってください。

ちなみにfunction Moji()の前についているlocalはローカル関数として定義するためのものです。
詳しくはスコープとかで調べてもらうとして、上のコードの場合は定義したファイル内でのみ有効になる関数となります。
localを付けない関数はグローバル関数となり、一度読み込めば他のファイルからでも呼び出せるようになります。

グローバル関数はとても便利に思いますが、他の人が作成した関数や、StepManiaがもともと用意している関数と名前が被ってしまった時、後から呼び出された方で上書きされてしまい、予期せぬ動作を引き起こすことになります。

BG/FGではよほどのことがない限り(というか原則絶対)local定義をしましょう。
これは関数に限らず、変数でも同じことが言えます。
もう一つ、行きましょう。

キーAとBを持つテーブルを返却する
これもただテーブルを返却すればいいだけです。
テーブルの中身は指示されていないのでなんか適当に返しましょうか。

local function Kansu()
return 0
end

local hensu = 1
return {
A = hensu,
B = Kansu(),
}

実質A=1とB=0を返しているだけですね。
テーブルの要素は「,」区切りで書きますが、最後の要素の後は書いても書かなくても問題ありません。
ただ、書いていた方がコピペしたり順番を入れ替えやすいです。
ちなみにLuaのテーブルはキーを指定する方法と指定しない方法があります。


-- ari['A']は1、ari['B']は2
local ari = { A = 1, B = 2 }

-- nashi[1]は'A'、nashi[2]は'B'
local nashi = { 'A', 'B' }

CやPHPといった言語ではキー指定のない配列は0から採番されますが、Luaのテーブルは1からになります。

さて、返却の方法はわかったので、StepManiaで使うdefault.luaですね。
先ほど言ったとおり、「Actor Class」を返却すればいいわけです。

Actor Classって何よ

簡単に言えばStepManiaで扱うための型だと思ってください。
その実体は特殊な情報を格納したテーブルデータです。

画像であればDef.Sprite()、サウンドであればDef.Sound()、luaが格納されたフォルダであればDef.BGAnimation()と「Def.」から始まる名前で色々定義されていますが、グローバル関数のLoadActor()を使用すれば内部で判断して良い感じに返却してくれます。

例えば単純に画像ファイルを表示させたい場合のサンプルです。

フォルダ構造

📂 sugoi_kyoku
┣📂 bga
┃┣🎨 gazou.png
┃┗🌙 default.lua
┣🎵 song.ogg
┗📄 humen.ssc

default.luaの中身

return LoadActor('gazou')

LoadActor()はluaと同じフォルダ内にあるファイルを読み込みActor Classで返却します。
引数に拡張子(.png)は必要ありません。
この場合、「gazou」から始まるファイルを検索します。
そのため、「gazou2」や「gazou_sugoi」というような「gazou」から始まるファイルが同じフォルダに存在する場合、警告が出ますので注意してください。

せっかくLuaを使うわけですし、単純に表示させるだけではなく複数画像を重ねたり動かしたりしたいですね!
では、先人のコードを参考に作ってみましょう!


local t = Def.ActorFrame{};
t[#t+1] = Def.ActorFrame{
InitCommand = cmd(Center);
LoadActor('shita');
LoadActor('ue')..{
InitCommand = function(self)
self:addy(100);
end;
OnCommand = function(self)
self:linear(0.5):addy(-200);
end;
};
};
return t;


なにこれ

普通のLuaならわかるのにこれを見て意味が分からなくなる人が多いと思います。
セミコロンは書く必要ないって上で言ってたのに消すと動かなくなるんですよこれ。

ここで「これがStepManiaの書き方なんだー」で済ませてしまうと覚えるのが大変です。
ということで、ひとつずつ説明していきます。

まず、最終的にreturnしているのは「t」です。「t」は何でしょうか。

local t = Def.ActorFrame{};

これですね。
Def.ActorFrame()は複数のActor Classを一つにまとめてActor Classで返却する関数です。
はい、これ関数なんです。

実はLuaは引数が一つしかなく、テーブルまたは文字列の場合は()の省略ができます
たとえば文字列返却の例で見せた関数ですが、

local function Moji(moji)
return moji..moji
end

これを呼び出すときは通常Moji('文字')となるところをMoji'文字'と書いても動作します。
つまり、Def.ActorFrame{}とは空のテーブルを引数に持つ関数Def.ActorFrame({})なんですね。
なのでもちろん、テーブルのようにDef.ActorFrame({1,2,3})と書いてもエラーになりません。

あ、もう一つ言い忘れてました。
Def.なんちゃらって書き方してるけどこれはどういう意味でしょうか。
実はLuaはキーを持つテーブルの場合、変数名.キー名という書き方ができます。
※ドット記述で書けるのは1次元のみ
つまり、Def.ActorFrameとはDef['ActorFrame']なんです。

もうわかった人もいると思いますが、Actor Classとは関数が格納されたグローバル変数(テーブル)Defの各要素のことです。
_fallbackテーマのScriptsフォルダ内にある02 ActorDef.luaで定義されています。

そのため、 tの代入は次のように書いても動作します。

local t = Def['ActorFrame']({})


でもその後なんかよくわからない代入をしていますね。

t[#t+1] = Def.ActorFrame{
(省略)

t[1]と書けばtがテーブルということがわかると思います。
テーブルの現在の要素数は「#変数名」で取得できます。
つまり、t[#t+1]とはテーブルの最後に要素を一つ追加ということになります。

ところで先ほどActor Class(= Def系関数)の実体はテーブルと書きましたが、このテーブルは中に格納されているActor Classの数が要素数となります。
つまり、最初のtに代入した時点での要素数は0であり、t[#t+1]=Def.ActorFrameをしてはじめて1になります。
t[#t+1]で要素追加後は次のコードと同じ状態になります。

local t = Def.ActorFrame{
Def.ActorFrame{
(省略)
}
}


さて、Defはテーブルを引数に持つ関数ということですが、
実は、テーブルの区切り文字は「,」の他に「;」も使うことができます。
これで察しがついたと思いますが、InitCommandやOnCommandは実はテーブルのキーです。

だからセミコロンを消すとテーブルが壊れるので動作しなくなるんです。
行末(本来不要)とテーブル要素の区切り(本来「,」)で両方同じ「;」を使ってるんです。
これが非常に紛らわしく、そしてStepManiaのLuaをわかりにくくしているんだと思います。

つまり、先の例のActorFrameは次のようなテーブルを引数として渡していることになります。

local hikisu = {
InitCommand = cmd(Center),
LoadActor('shita'),
LoadActor('ue')..{
(省略)
},
}
Def.ActorFrame(hikisu)

こう書いた方がわかりやすいですかね。

local hikisu = { InitCommand = cmd(), LoadActor(), LoadActor() }
Def.ActorFrame(hikisu)


はい。見ての通り単純にテーブルを引数として渡しているだけなので、

local hensu = 100
local t = Def.ActorFrame{
Def.ActorFrame{
InitCommand = function(self)
self:addx(hensu)
end,
}
}

という書き方は出来ても

local t = Def.ActorFrame{
local hensu = 100
Def.ActorFrame{
InitCommand = function(self)
self:addx(hensu)
end,
}
}

という書き方はできません。

さらに、テーブルは変数名.キー名で定義できるので、次のように書くこともできます。

local hikisu = {
LoadActor('shita'),
LoadActor('ue')..{
(省略)
},
}
local t = Def.ActorFrame(hikisu)
t.InitCommand = cmd(Center)


ナチュラルにLoadActor()のうしろに「..」という謎の記述が出ています。

Def.ActorFrame{
(省略)
LoadActor('ue')..{
InitCommand=function(self)
self:addy(100);
end;
OnCommand=function(self)
self:linear(0.5):addy(-200);
end;
};
};

先ほど文字列の時に説明しましたが「..」は結合です。
つまり、LoadActor('ue')の戻り値にテーブルを合成しています。
この書き方は通常のテーブル同士の結合には使えません。
Actor Classに対して行える特殊な書き方です。
次のコードと同等です。

local ue = LoadActor('ue')
ue['InitCommand'] = function(self)
self:addy(100)
end
ue['OnCommand'] = function(self)
self:linear(0.5):addy(-200)
end
Def.ActorFrame{
(省略)
ue,
}

そういえば変数にfunction(self)~endを代入するのもいきなり出てましたね。
これは名前を定義していない関数を代入しています。
やっていることは下とほぼ同じです。

local function KansuInitCommand(self)
self:addy(100)
end
ue['InitCommand'] = KansuInitCommand(self)


local hensuInitCommand = function(self)
self:addy(100)
end
ue['InitCommand'] = hensuInitCommand


ところで、cmd()を使った書き方をすれば3.9のBGAnimationに近い書き方ができます。
しかし、これは非推奨な記述方法で、将来的には削除されるので極力使わないようにしましょう。
次のように置き換えるべきです。

InitCommand = cmd(Center)

 ↓

InitCommand = function(self)
self:Center()
end


OnCommand = cmd(diffuse,1,0,0,1;linear,0.2;diffusealpha,0)

 ↓

OnCommand = function(self)
self:diffuse(1,0,0,1):linear(0.2):diffusealpha(0)
end


以上を踏まえて、極力わかりやすく書き直したのが次のコードになります。


return Def.ActorFrame({
InitCommand = function(self)
self:Center()
end,
LoadActor('shita'),
LoadActor('ue')..{
InitCommand = function(self)
self:addy(100)
end,
OnCommand = function(self)
self:linear(0.5):addy(-200)
end,
},
})

※全く同じにするならホントはもう1段階Def.ActorFrameで囲んでいる必要があるんですけど、実質同じ動作なので省略してます。




といった感じでなんとなくですがStepManiaのLuaっておかしくね?を解決できるかもしれない内容を書いてみました。
まあ結局は書いて慣れろってところなんで、とりあえず作ってみるのが一番じゃないですかね。

次回書く気力があれば簡単なデバッグ方法とかそもそもどこから始めるかとかその辺書いてみようかなと思います。

ではでは。