1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[ネタ]Lua言語仕様の範囲内で書けるシナリオスクリプト言語を考える

Last updated at Posted at 2022-06-02

概要

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)が組み込まれているようなのでこの構想使えそうじゃないですか!
というわけでちょっとアセットを使ってみてカスタマイズしてみたい。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?