概要
Unity等のゲーム開発でノベルゲームやRPGなどテキストを多用するゲームでシナリオを記述するスクリプトとしてLuaを用いる例はいろいろありますが、
よくあるサンプルは1行1行関数呼び出しで実現していたりしていてすごく見にくそうと思ったので。
できるだけ見やすくかつLua言語の仕様に違反しない形のシナリオスクリプト言語を考えてみる。
たぶん車輪の再発明。
こんな感じ
結論から言うとテキスト中心のスクリプトならテキストや命令を下のようにLuaテーブル(配列)として記述すれば見やすいし書きやすいのでは。
Script = {
"本日は晴天なり、しらんけど", -- どこかに文字列を表示、""で囲み、最後に,か;で区切る
name.."はん、ほんまかいな~", -- Lua標準の変数埋め込みはこんな感じ(さすがにLua標準は辛いかな)
{ "Rain" }, -- スクリプト独自命令はテーブルで表記すると処理も見た目も区別しやすいのでは
[[
なんかすごい雨降りだしたやん、
どこが晴天や、いいかげんしにろ!
こんな長く喋るの面倒くさいやろ。
]], -- Luaの機能だけで複数行の文字列もOK(これもちょっと...)
"すまんやで",
"せやな",
func(abc), -- 文字列を返す関数を書くとテーブル代入時に呼び出されてプリプロセッサみたいなことができそう
{ function() Judge(0) end }, -- Luaコードも引数なしの無名関数という形なら直接書ける
"おしまい", -- Luaテーブルは最終要素の末尾にカンマつけても怒られない仕様。やったね
}
テキストと命令の区別
命令はテーブル内のテーブルにすれば処理も見た目も簡単に区別がつきそう。
script = {
"あいうえおかきくけこ",
{ "GOTO", "END" },
"ここは果たして通るのでしょうか?",
{ "LABEL", "END" } ,
"終わり",
}
コードはこんな感じ
------------------------------------------------ 中略 ---------------------------------------------------------
tn = type(t[pin])
if tn == "string" then -- 文字列
-- テキスト
elseif tn == "table" then -- テーブル
-- 命令
end
------------------------------------------------ 中略 ---------------------------------------------------------
こんな感じに書けば命令や引数はLua上ではただの文字列配列なので処理は簡単そう
{ "命令", "引数1", "引数2" ... },
コード
上記のスクリプトっていうかLuaテーブルを処理するコードは非常にシンプル。
function interpretation(t)
local tn
if type(t) ~= "table" or #t == 0 then
return
end
for i, ver in ipairs(t) do
tn = type(t[i])
if tn == "string" then -- 文字列
--画面に文字列を表示したりする処理
print(t[i])
elseif tn == "table" then -- テーブル
--スクリプト独自命令の処理
end
end
end
1行というか1要素ごとにyieldするコルーチンにもできる
interpretation = coroutine.create(
function(t)
local tn
if type(t) ~= "table" or #t == 0 then
return
end
for i, ver in ipairs(t) do
tn = type(t[i])
if tn == "string" then -- 文字列
--画面に文字列を表示したりする処理
print(t[i])
elseif tn == "table" then -- テーブル
--スクリプト独自命令の処理
end
coroutine.yield(i) -- ここでyieldするとか(UnityのときはUnity側のコルーチン使わないとまずいかも)
end
end
)
制御構造を組み込むなら?
命令に制御構造を組み込むならipairsで列挙すると好きに移動できなさそうなので、
repeat - untilバージョンに書き換えてpinに値を設定すれば飛べるようにしないといけないかも。
interpretation = coroutine.create(
function(t)
local tn
local pin
local len
if type(t) ~= "table" or #t == 0 then
return
end
len = #t
pin = 1
repeat
tn = type(t[pin])
if tn == "string" then -- 文字列
--画面に文字列を表示したりする処理
print(t[pin])
elseif tn == "table" then -- テーブル
--スクリプト独自命令の処理
end
coroutine.yield(pin) -- ここでyieldするとか(UnityのときはUnity側のコルーチン使わないとまずいかも)
pin = pin + 1
until pin > len
end
)
命令の処理どうする?
簡単なGOTOやIFとフラグ処理なんかを盛り込んでみたら機能のショボさのわりに意外と複雑なコードになっちゃった。
------------------------------------------------ 中略 ---------------------------------------------------------
local label = {}
local val = {}
local strval = {}
--ラベル位置の取得と設定
for i = 1, #t do
if type(t[i]) == "table" and t[i][1] == "LABEL" and #t[i] >= 2 then
if type(label[t[i][2]]) == "string" then
label[t[i][2]] = i
end
end
end
------------------------------------------------ 中略 ---------------------------------------------------------
elseif tn == "table" then -- テーブル
--スクリプト独自命令の処理
if #t[pin] > 0 then
-- Luaコードの実行
if type(t[pin][1]) == "function" then
t[pin][1]()
-- テキスト内で変換する変数の値が入っているテーブルを指定
elseif t[pin][1] == "TABLE" and #t[pin] >= 2 then
if type(t[pin][2]) == "table" then
strval = t[pin][2]
endif
-- フラグを管理するテーブルを指定
elseif t[pin][1] == "FLAG" and #t[pin] >= 2 then
if type(t[pin][2]) == "table" then
val = t[pin][2]
endif
-- 指定したラベルの要素にジャンプ
elseif t[pin][1] == "GOTO" and #t[pin] >= 2 then
if type(label[t[pin][2]]) == "number" then
pin = label[t[pin][2]]
end
-- フラグにtrueまたはLuaコードの戻り値をセット
elseif t[pin][1] == "SET" and #t[pin] >= 2 then
if type(t[pin][3]) == "function" then
val[t[pin][2]] = t[pin][3]()
else
val[t[pin][2]] = true
end
-- フラグにfalseをセット
elseif t[pin][1] == "UNSET" and #t[pin] >= 2 then
val[t[pin][2]] = false
-- 指定したフラグがtrueなら3つめ、falseなら4つめに指定したラベルへジャンプ
-- (4つめは省略可、その場合は次の要素へ)
elseif t[pin][1] == "IF" and #t[pin] >= 3 then
if val[t[pin][2]] then
if type(label[t[pin][3]]) == "number" then
pin = label[t[pin][3]]
end
else
if type(t[pin][4]) == "string" then
if type(label[t[pin][4]]) == "number" then
pin = label[t[pin][4]]
end
end
end
end
end
------------------------------------------------ 中略 ---------------------------------------------------------
これでこんな感じで制御処理ができるはず
script = {
{ "TABLE", abc },
{ "TABLE", {name="みかん星人", value="オレンジジュース"} },
{ "FLAG", flags },
{ "LABEL", "ABC" },
{ "GOTO", "Test" },
{ "SET", "VAL" },
{ "SET", "VALFANC", function() return true end },
{ "UNSET", "VAL" }
{ "LABEL", "Test" },
{ "IF", "VAL", "ABC", "End" }
{ "LABEL", "End" },
}
文字列の変数埋め込み
Lua言語の範囲内でも文字列結合演算子(..)を使えばなんとかなるんですが、
見やすいとはちょっと言えないので、
C#のような感じで{}内に変数を書けば反映してくれるような仕組みがいるかもしれない。
gsubっていう他の言語で言うところのreplaceみたいな関数があるのでそれを使ったらいけるようだ。
local convstr
------------------------------------------------ 中略 ---------------------------------------------------------
if tn == "string" then -- 文字列
--画面に文字列を表示したりする処理
convstr = t[pin]:gsub("(%b{})", function(w) return strval[w:sub(2, -2)] or w end)
print(convstr)
------------------------------------------------ 中略 ---------------------------------------------------------
こうするとC#のように{}内の変数を展開できるみたい。
"{name}に格納されている値は{value}", -- "みかん星人に格納されている値はオレンジジュース"
参考
制御文字の埋め込み
テキストの途中で止めたりしたい場合に手動でテキストを別要素に分けるのではなく、
特定の文字列を挟むことで制御できるようにしたほうが便利そう
"テキストの途中で止め<>たいんだよなぁ<0.5sec>止まってた?"
こういうのはsplitなんかで実現すると思うんですが、Luaには標準のsplitがないらしい。
gmatch(正規表現で文字列検索)で簡単に実現できるようですが。
むしろgmatchだと区切り文字列内のテキストを取得できて細かい指定もできそう。
local substr = {}
local wait = {}
for s, w in string.gmatch(t[pin], "(.-)<(%w-)>(.-)") do
table.insert(substr, s)
table.insert(wait, w)
end
for l in string.gmatch(txt, ".*>(.*)") do
table.insert(substr, l)
table.insert(wait, "")
end
for i = 1, #substr do
--制御文字までのテキストと制御文字内の文字列の処理
print(substr[i])
print(wait[i])
end
参考
喋ってる人の表示
RPGやビジュアルノベルなんかは通常テキストウインドウの左上に喋ってる人の名前が表示されていますが、
どうやって指定しよう?
命令の場合(こっちは設定したら再度設定されるまでずっと表示かな)
{ "ACTOR", "健太" },
{ "ACTOR", "{Player}" },
テキストでやる場合(こっちは都度クリアしたほうがいいのかな)
"健太::なにやってるんだよ!",
"{Player}::いやーわるいわるい",
コードは上の応用なんで省略
選択肢どうする?
こんな感じ?選択肢とフラグを設定しておいて選んだ選択肢のフラグをTrueにする感じ
script = {
{ "SELCLS" },
{ "SELSET", "このまま静観する", "Seikan" },
{ "SELSET", "さすがにまずい!すぐさま止める", "Tomeru" },
{ "SELECT" },
}
seldata = {}
------------------------------------------------ 中略 ---------------------------------------------------------
--選択肢データのクリア
elseif t[pin][1] == "SELCLS" then
seldata = {}
--選択肢データの追加
elseif t[pin][1] == "SELSET" and #t[pin] >= 3 then
if type(t[pin][2]) == "string" and type(t[pin][3]) == "string"
seldata.insert({t[pin][2], t[pin][3]})
--選択
elseif t[pin][1] == "SELECT"
--ランタイム側に処理をしてもらう(yieldしてもいいかも)
--selectに選択した番号が入ってる前提
val[seldata[select][2]] = true
------------------------------------------------ 中略 ---------------------------------------------------------
Unity等のランタイム側の処理
ランタイム側の処理で必要なのはこんなもの?
- テキストの表示
- (入力や時間経過等の)待ちの処理や継続の指示
- Lua側はで処理できない命令の実行
ランタイム側はLuaの処理結果を受け取って
・テキストを表示して待ちと継続の処理
・Lua側で処理は終わってるので何もせず待ちと継続の処理
・Lua側で処理できないのでランタイム側で処理した上で待ちと継続の処理
を実行する形になりそう。
任意の関数の実行、引数
テーブルの1要素目を関数名とし、2要素目以降を引数として関数を実行できないか?
Luaでは引数の展開はできないので、引数の個数を制限するしかないか・・・
------------------------------------------------ 中略 ---------------------------------------------------------
local fn = _ENV[t[pin][1]]
if type(fn) == "function" then
if #t[pin] == 1 then
fn()
elseif #t[pin] == 2 then
fn(t[pin][2])
elseif #t[pin] == 3 then
fn(t[pin][2], t[pin][3])
elseif #t[pin] == 4 then
fn(t[pin][2], t[pin][3], t[pin][4])
elseif #t[pin] == 5 then
fn(t[pin][2], t[pin][3], t[pin][4], t[pin][5])
elseif #t[pin] >= 6 then
fn(t[pin][2], t[pin][3], t[pin][4], t[pin][5], t[pin][6])
end
end
------------------------------------------------ 中略 ---------------------------------------------------------
Fungus
FungusというビジュアルノベルやRPGの会話シーン等が簡単に作れるアセットがあるらしく、
このアセットには最初からLua(MoonShsarp)が組み込まれているようなのでこの構想使えそうじゃないですか!
というわけでちょっとアセットを使ってみてカスタマイズしてみたい。