[ネタ] Lua言語仕様の範囲内で書けるシナリオスクリプト言語をFungus用に拡張してみる

# 概要

[以前に作った](https://qiita.com/riafeed/items/3cca2e3b694d217a5e38)シナリオスクリプトインタプリタですが、Fungusで使えるように拡張してみた。
少なくともFungusでLuaを使う際の参考にでもなれば。
[結局ソースコードとにらめっこが必要ですが。](https://github.com/snozbot/fungus/blob/master/Assets/Fungus/Thirdparty/FungusLua/Resources/Lua/fungus.txt)


# 拡張してみる
## テキスト表示

テキスト表示はsay関数が用意されていて、引数に表示したい文字列を渡せばいいだけなので簡単です。

```Lua
    obj.TEXT = function(self, script, text)
        say(text)
    end
```

とはいえそのままでは味気ないので、スクリプトから文字色・大きさを設定できるようにする。
Fungusではテキストに```{～}```で囲まれたタグのようなものを埋め込む事ができ、テキストを装飾したりできるのでその機能を利用して実現する。
[タグ一覧はこれを見るのが一番確実そう。](https://github.com/snozbot/fungus/blob/master/Assets/Fungus/Scripts/Utils/TextTagParser.cs#L210)
このタグのようなもので実現するのが一番楽な命令がいくつかあるので、それも設定すればsayに渡して適用できるようにしておく。

```Lua
    obj._Internal.txtcolor = ""
    obj._Internal.txtsize = -1
    obj._Internal.setcommand = ""

    obj.TEXT = function(self, script, text)
        if self._Internal.txtsize >= 0 then
            text = "{size="..self._Internal.txtsize.."}"..text.."{/size}"
        end
        if self._Internal.txtcolor ~= "" then
            text = "{color="..self._Internal.txtcolor.."}"..text.."{/color}"
        end
        if self._Internal.setcommand ~= "" then
            text = self._Internal.setcommand .. text
            self._Internal.setcommand = ""
        end
        say(text)
    end

    obj.SETTEXTSIZE = function(self, script, size)
        self._Internal.txtsize = size
    end

    obj.SETTEXTCOLOR = function(self, script, colorstr)
        self._Internal.txtcolor = colorstr
    end
```
## キャラクター名

セリフウインドウ(ダイアログ)の左上にあるキャラクター名の設定はsetcharacter関数が用意されていますが、これは事前に作っておいたFungusのCharacterオブジェクトを参照してダイアログ左上に名前(Name Text)と右側に画像を表示するというもので、
単にダイアログ左上の名前だけ変えたいということはできません。
名前だけ変えたい場合はgetsaydialog関数でダイアログオブジェクトを取得してSetCharacterNameメソッドで名前と色(UnityEngine.Color型)を指定する必要があります。
ちなみにUnityEngine.Color型のオブジェクトはfactory.color関数で作れるようです。

Colorオブジェクトとテキスト表示タグの色指定の方法が全く異なっていて面倒なので色指定を統一できるようにキャラクター名の色もタグで指定する。
実は文字列に関してはsub関数(luautils.substitute)を通じて[リッチテキストのタグ](https://docs.unity3d.com/ja/2018.4/Manual/StyledText.html)に変換しているだけなので、最初からリッチテキストのタグ形式で色を指定する。

ここでは名前からCharacterオブジェクトの存在を確認してあればそれを適用し、なければその名前を表示するという仕様にしています。

```Lua
    obj._Internal.charcolor = ""
    obj._Internal.defcolor = factory.color(1.0, 1.0, 1.0, 1.0)

    obj.INIT = function(self, script)
        if self.INITSCRIPT(self, script) then
            getsaydialog().SetCharacterName("", self._Internal.defcolor)
        end
    end

    obj.SETCHARA = function(self, script, name, type)
        local gobj = luautils.find(name)
        local char = nil
        if gobj ~= nil then
            char = gobj.GetComponent("Character")
        end
        if char == nil then
            if self._Internal.charcolor ~= "" then
                name = "<color="..self._Internal.charcolor..">"..name.."</color>"
            end
            getsaydialog().SetCharacterName(name, self._Internal.defcolor)
            getsaydialog().SetCharacterImage(nil)
        else
            setcharacter(char, type)
        end
    end

    obj.SETCHARCOLOR = function(self, script, colorstr)
        self._Internal.charcolor = colorstr
    end
```
## 選択肢

選択はchoose関数があり、引数に選択肢の配列を渡すと何番目が選ばれたか返ってくるという仕様になっている。
まとめて設定すると見通しが凄まじく悪いので、1つずつ設定してもらって記憶しておくほうがよさそう。

```Lua
    obj._Internal.choosetable = {}
    obj._Internal.choosejump = {}

    obj.SETCHOOSE = function(self, script, seltext, seljump)
        table.insert(self._Internal.choosetable, seltext)
        table.insert(self._Internal.choosejump, seljump)
    end

    obj.CHOOSE = function(self, script)
        local select = choose(self._Internal.choosetable)
        self.GOTO(self, table, self._Internal.choosejump[select])
        self._Internal.choosetable = {}
        self._Internal.choosejump = {}
    end
```

## 音を鳴らす

音を鳴らすLua関数は用意されていませんが、テキストのタグで音を鳴らすことができるようなので、
タグ文字列を設定しておいてテキスト表示の時にまとめて適用されるようにするのが一番楽そうというかこれ以外なさそう。

```Lua
    obj.SOUND = function(self, script, objname)
        self._Internal.setcommand = self._Internal.setcommand .. "{audio="..objname.."}"
    end

    obj.MUSIC = function(self, script, objname)
        self._Internal.setcommand = self._Internal.setcommand .. "{audioloop="..objname.."}"
    end

    obj.PAUSE = function(self, script, objname)
        self._Internal.setcommand = self._Internal.setcommand .. "{audiopause="..objname.."}"
    end

    obj.STOP = function(self, script, objname)
        self._Internal.setcommand = self._Internal.setcommand .. "{audiostop="..objname.."}"
    end
```

## タグでできそうな機能

クリアとウエイトあたりは、あとはエフェクト関係(揺らしたりフラッシュさせたり)はもう直接指定でいいんじゃないかと。

```Lua
    obj.CLEAR = function(self, script)
        self._Internal.setcommand = self._Internal.setcommand .. "{c}"
    end

    obj.WAIT = function(self, script, time)
        self._Internal.setcommand = self._Internal.setcommand .. "{w="..time.."}"
    end

    obj.EFFECT = function(self, script, name, param)
        self._Internal.setcommand = self._Internal.setcommand .. "{"..name.."="..param.."}"
    end
```

## 画像関係

Fungus自体にはそういったコマンドはありますが、該当するLua関数はありません。
FungusとしてはLuaあくまで補助ということなんでしょうか。
Luaで無理やり頑張ろうとしても[MoonSharpの仕様](https://www.moonsharp.org/objects.html)に阻まれてC#で設定なしでは表示非表示の設定すらままならないので画像を画面外へ移動させたり画面内へ移動させたりすることでどうにか対処する。

```Lua
    obj._Internal.oldxy = {-1, -1}
    obj._Internal.thisview = nil

    obj.SETBACK = function(self, script, name)
        local gobj = luautils.find(name)
        local transform
        if gobj == nil then return end
        transform = gobj.GetComponent("Transform")
        if transform == nil then return end
        if self._Internal.thisview ~= nil then
            self._Internal.thisview.position = factory.vector3(self._Internal.oldxy[1], self._Internal.oldxy[2], 0)
        end
        self._Internal.thisview = transform
        self._Internal.oldxy[1] = transform.position.x
        self._Internal.oldxy[2] = transform.position.y
        transform.position = factory.vector3(0, 0, 0)
    end

    obj.UNSETBACK = function(self, script)
        if self._Internal.thisview == nil then return end
        self._Internal.thisview.position = factory.vector3(self._Internal.oldxy[1], self._Internal.oldxy[2], 0)
        self._Internal.thisview = nil
        self._Internal.oldxy[1] = -1
        self._Internal.oldxy[2] = -1
    end

    obj.MOVEIMAGE = function(self, script, name, x, y)
        local gobj = luautils.find(name)
        if gobj == nil then return end
        transform = gobj.GetComponent("Transform")
        if transform == nil then return end
        transform.position = factory.vector3(x, y, 0)
    end

    obj.MOVEIMAGETARGET = function(self, script, name, targetname)
        local imgobj = luautils.find(name)
        local targetobj = luautils.find(targetname)
        if imgobj == nil then return end
        local imgtransform = imgobj.GetComponent("Transform")
        local targettransform = targetobj.GetComponent("Transform")
        if imgtransform == nil then return end
        if targettransform == nil then return end
        imgtransform.position = factory.vector3(targettransform.position.x, targettransform.position.y, 0)
    end
```

## Fungusブロック呼び出し

結局のところ凝ったことをしようとするならUnityやFungusで頑張ってルーチンを作ってスクリプトから呼ぶ、もしくはその逆をすることになろうだろうからfungusブロックをスクリプトから呼べるようにしたほうがよさそう。

```Lua
    obj.RUNBLOCK = function(self, script, flowchartname, blockname, index, runwait)
        local gobj = luautils.find(flowchartname)
        local flowchart
        local wait = runwait
        if gobj == nil then return end
        flowchart = gobj.GetComponent("Flowchart")
        if flowchart == nil then return end
        if runwait == nil then wait = false end
        runblock(flowchart, blockname, index, wait)
    end
```




# 使ってみる

UnityとFungusが入っている前提で。Fungusの入れ方はググるか[ここ](https://trend-tracer.com/fungas-install/#)を参照してください。

FungusではLuaコードは```Assets/Resources/Lua```に入れることで特別な設定無しでLuaコードが参照できるようになっているようです。
他の場所に入れたい場合はC#コードでパスを追加しないといけないっぽい(多分・未確認)

[ここの](https://qiita.com/riafeed/items/3cca2e3b694d217a5e38#%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF%E6%9C%AC%E4%BD%93)SSI.luaと、以下のFungusSSI.luaを拡張子を.txtで保存して、Unityプロジェクトの```Assets/Resources/Lua```に入れてください。

メニューの[Tools]→[Fungus]→[Create]から、「Flowchart」と「LuaEnvironment」を選んでシーンに追加しておきます。

[Tools]→[Fungus]→[Flowchart Window]でフローチャート画面を開き、初期状態だと1つブロックが置かれているのでクリックし、
インスペクタ真ん中の「＋」をおして「Scripting/ExecuteLua」を追加します。
FlowchartのインスペクタのCommand欄に「Execute Lua」が追加されているはずなのでクリックし、下のLua File欄をクリックして保存したFungusSSI.txtに相当するアセットを選びます。

シナリオスクリプトを書いてScript.txtとして```Assets/Resources/Lua```に保存し、実行するとシナリオスクリプトにそって動くはずです。

```FungusSSI.Lua
local FungusSSI = {}

FungusSSI.Create = function()

    local obj = require('SSI').Create()

    --ここにカスタムコマンドを追加

    return obj
end

local ssi = FungusSSI.Create()
local success, err = ssi:Start('Script')
```