Lua
Unity3D
Unity 2Day 10

unityでuniluaを使ってADV機能を実装する

More than 1 year has passed since last update.

動機

ADVみたいな2Dの寸劇を制御するスクリプトが欲しかったのです。
最初はexcelで入力するcsv形式の独自スクリプトを作りました。
RPGツクールを参考にif elseもサポートしました。
しかしexcelでの入力は不便でした。
if else をサポートするなら普通のエディタで記述したいものです。
そこで誰でも一度は聞いたことあるスクリプトluaに目をつけました。

色んなLuaパーサー

unity luaでググると色々出てきます

  • unity Lua Interface 2011年から更新無し dllなのでwindows用か
  • uLua 有料 ios androidでも動作するがプラグイン形式
  • BBlua 有料 ios androidでも動作するプラグイン形式
  • NLua 色んなplatform向けがある、unity3Dと.net Pure C#何が違うんだ…
  • unilua C#実装
  • MoonSharp おそらく一番モダンか? C#実装で.net mono xamarin unityとの互換性

僕が求めていたのは、ios android端末でも動作するものでした。
純粋なC#で記述したのが望ましく、その要求を満たしてたのがuniluaでした(2014年の時点では。今ならmoonsharpが良さそう
ともあれluaを組み込んでADVパートを実装してみましょう。

Luaスクリプト

僕が実装したLuaドラマスクリプトはどういうものか見てみましょう。
以下はios android で遊べるはらぺこウィッチーズで実際に使われたスクリプトの抜粋です。

event0001.lua
require("common/common")
function event0100()
    c_2dcharShow(kwd_ChEila,kwd_FileCp01_Stand,kwd_ShowInstant,kwd_Pos_OutR);
    c_2dcharShow(kwd_ChMinna,kwd_FileCp03_Stand,kwd_ShowInstant,kwd_Pos_OutL);
    c_2dcharReverse(kwd_ChEila,false);
    c_2dcharReverse(kwd_ChMinna,true);
    c_Wait(0.5);

    c_toggleWindow(true);

    c_2dcharMove(kwd_ChEila,kwd_Pos_R,kwd_SpeedFast, true);
    c_2dcharMove(kwd_ChMinna,kwd_Pos_L,kwd_SpeedFast, true);
    c_showmessage(kwd_NameAuto,kwd_ChMinna,kwd_FukiNormal,"aaa","こちらミーナ<br>エイラ、聞こえてる?");
    --省略
    c_showmessage(kwd_NameAuto,kwd_ChMinna,kwd_FukiNormal,"aaa","一人でも戦えるように装備を一新しておいたけれど<br>説明はいる?");
    c_switch("頼むぞ","ムリダナ",nil,nil);
    idx = c_getvariable(1);--とあるC#global配列のidx 1には直前に選ばれた選択肢番号が入るというルール
    if idx == 0 then
        c_showmessage(kwd_NameAuto,kwd_ChMinna,kwd_FukiNormal,"aaa","ふふ、わかったわ");
    else
        c_showmessage(kwd_NameAuto,kwd_ChMinna,kwd_FukiNormal,"aaa","えっ?$|あぁ必要ないってことね<br>了解よ");

    end
    c_toggleWindow(false);

    c_changeScreenTone(0,0,0,0,0.5,true);
    c_opvariable(va_ScenarioProcess,kwd_OpMov,kwd_ParamConst,ev_ScStreetED,0,0);

    c_clearAllPic();
end
common.lua
function c_2dcharShow(kwd_CharID, szFilename, kwd_Show, kwd_Pos)
    libc.c_Command(237, kwd_CharID, szFilename, kwd_Show, kwd_Pos);
end
function c_showmessage(name, kwd_charid, kwd_fuki, voicefile, message)
    libc.c_Command(101, name, kwd_charid, kwd_fuki, voicefile, message);
    coroutine.yield(0);
end
function c_getvariable(id)
    val = libc.c_GetVariable(id);
    return val;
end
--他にも色々な関数が定義

event0100.luaはドラマスクリプトです。好きなだけ量産します。
common.luaはドラマスクリプトで使う関数や定数を定義します。

libc.c_Command(cmdID,パラメータ);
libc.c_GetVariable(id);
基本この2つに集約されます。どちらもC#で定義されてる関数と思ってください。
coroutine.yield(0)は、luaからC#に処理を戻すために使います。
メッセージ表示はカタカタ表示だし、ユーザーの入力を受けるので戻す必要があるわけです。

このluaファイルをunityで読み込みドラマを再生するわけです。

C#側

luaの仕組みについてはこちらの解説が一番わかりやすいです。
c++組み込みの説明ですがC#でも考え方は同じです。

読み込み再生概要

luaファイルはResources/LuaRoot以下に置きます。
拡張子はunityが読み込めるよう.txtにリネームします。

dramaManager.cs
        _dramaInterpreter.Load(dramaID, uniqueID);
public Status Update()
    {
        Status s = _dramaInterpreter.ExecuteScript();
        return s;
    }

単純化してますが、luaファイルを読み込んで毎フレーム処理してやる。
するとdramaInterpreter内部でluaを適宜パースされうまい具合ドラマ再生されます。
では詳細に見ていきましょう。

luaファイル読み込みと初期化

LuaFile.cs
internal class LuaFile
    {
        private static readonly string LUA_ROOT = "LuaRoot";
        //private static readonly string LUA_ROOT = System.IO.Path.Combine(, "LuaRoot");

        public static FileLoadInfo OpenFile( string filename )
        {
            var path = GetPath( filename );
            Debug.Log( "lua filepath =" + path );
            TextAsset ta = Resources.Load( path, typeof(TextAsset) ) as TextAsset;
            return new FileLoadInfo(  ta.bytes );
        }

        public static bool Readable( string filename )
        {
            var path = GetPath( filename );
            try {
                TextAsset ta = Resources.Load( path, typeof( TextAsset ) ) as TextAsset;
                if( ta != null){
                    return true;
                }else{
                    return false;
                }
            }
            catch( Exception ) {
                return false;
            }
        }
        private static string GetPath( string filename ) {
            //filename = Path.GetFileNameWithoutExtension( filename );
            if( filename.EndsWith( ".lua" ) ) {
                filename = filename.Substring( 0, filename.Length - 4 );
            }
            return LUA_ROOT + "/" + filename;
        }
    }

uniluaはそのままだとResourcesから読み込めないので修正してやります。
他のフォルダやassetbundle、メモリ上のstringから読ませたい場合は適宜修正する必要があります。

dramaInterpreter.cs
public const string LIB_NAME = "FSMEventRunLua.cs";
    public static int OpenLib(ILuaState lua)
    {
        var define = new NameFuncPair[]{
            new NameFuncPair("c_Command", c_Command),
            new NameFuncPair("c_GetVariable",c_GetVariable),
        };
        lua.L_NewLib(define);
        return 1;
    }
    private static DramaInterpreter s_dramaInterpreter = null;
    public static int c_Command(ILuaState lua)
    {
        s_dramaInterpreter.ProcCommandByLua(lua);
        return 1;
    }
    public static int c_GetVariable(ILuaState lua)
    {
        s_dramaInterpreter.ProcGetVariableByLua(lua);
        return 1;
    }
public void Load(int iEventID, int uniqueID)
    {
        if (m_Lua != null)
        {
            m_Lua = null;
        }

        if (m_Lua == null)
        {
            m_Lua = LuaAPI.NewState();
            m_Lua.L_OpenLibs();
            m_Lua.L_RequireF(LIB_NAME, OpenLib, false);
        }
        _isNeedSkip = false;
        _uniqueID = uniqueID;
        string fileName = string.Format("event{0:D4}", iEventID);
        m_Lua.L_DoFile(fileName);
        m_Lua.GetGlobal(fileName);

    }

安全のためlua stateは保持せず毎回作り直します。
luaファイルの読み込みと初期化を行っています。

OpenLib関数が最も重要で
luaファイルをパースして呼ばれるC#関数はここで定義されたもののみです。
ここではc_Commandとc_GetVariableだけが呼ばれます。
これはcommon.luaで呼んでいる関数ですね。
c_Commandに渡される多様な引数に基づき、立ち絵を表示したり、メッセージを再生ししたりします。

lua実行、コマンド実行

dramaInterpreter.cs
public DramaManager. Status ExecuteScript()
    {
        ThreadStatus tstatus = m_Lua.Resume(m_Lua, 0);
        if (tstatus == ThreadStatus.LUA_OK)
        {
            return DramaManager.Status.Done;
        }

        return DramaManager.Status.None;
    }

resumeすると前回yieldしたところから再開します。
unityのcoroutineのような挙動です。
luaスクリプトを最後まで実行するとThreadStatus.LUA_OKが返ってきます。

Resumeの結果先に定義したc_Command関数が呼ばれます。
そのままProcCommandByLuaが呼ばれます。

dramaInterpreter.cs
private void ProcCommandByLua(ILuaState lua)
{
        int n = lua.GetTop();

        List<bool> boolList = new List<bool>(8);
        List<float> floatList = new List<float>(8);
        List<string> strList = new List<string>(8);
        List<int> intList = new List<int>(8);

        // 1 はコマンド ID が入ってる
        for (int i = 2; i <= n; ++i)
        {
            LuaType t = lua.Type(i);
            switch (t)
            {
                case LuaType.LUA_TNIL:
                    break;
                case LuaType.LUA_TBOOLEAN:
                    boolList.Add(lua.ToBoolean(i));
                    break;
                case LuaType.LUA_TLIGHTUSERDATA:
                    break;
                case LuaType.LUA_TNUMBER:
                    floatList.Add((float)lua.ToNumber(i));
                    break;
                case LuaType.LUA_TSTRING:
                    string str = ConvertStringToUtf8(lua.ToString(i));
                    //string str = lua.ToString( i );
                    strList.Add(str);
                    break;
                case LuaType.LUA_TTABLE:
                    break;
                case LuaType.LUA_TFUNCTION:
                    break;
                case LuaType.LUA_TUSERDATA:
                    break;
                case LuaType.LUA_TTHREAD:
                    break;
                case LuaType.LUA_TUINT64:
                    intList.Add((int)lua.ToUInt64(i));
                    break;
            }
        }

        int cmdID = lua.ToInteger(1);

        // スタックを空にする
        lua.Pop(n);
        switch(cmdID){
              case 101:// c_showmessage
                   // メッセージ再生
              case 237:// c_2dcharShow
                   // 立ち絵表示
        }
}

スタックの1個目はコマンドID
それ以降に各コマンドIDに対応したパラメータが入っています。
あとはそれらを元にメッセージ再生したり、立ち絵を表示したりします。

Lua実行、luaに値を返す

ADVでよくある選択肢で分岐を実現するためにはluaに値を返す必要があります。
それを担っているのがc_GetVariableです。

dramaInterprter.cs
 public void ProcGetVariableByLua(ILuaState lua)
    {
        int n = lua.GetTop();
        int idx = lua.ToInteger(1);
        // スタックを空にする
        lua.Pop(n);

        int val = GlobalVariable.instance.Get(idx);
        lua.PushInteger(val);
    }

まとめ

僕のドラマスクリプトのキモはRPGツクールでもおなじみのcmdIDとパラメータです。
それってわざわざLuaでやる必要あるの?ぶっちゃけcsvでよくね という意見もあるでしょう。
でもcsvなりの独自フォーマットの場合if elseやfor loopの実装は結構大変です(階層が深くなったらどうします?
加えてそれを記述するためのエディタの実装も大変です。

一方Luaであれば言語仕様としてif elseが実装されてます。
スクリプト言語なので好きなエディタで記述できます、実にプログラマ向き(僕はeclipseのlua plugin使ってます

そしてもちろん組み込みに対応した環境もとても多いです。
C、C++は公式で標準サポートされてます。
他の言語もサードパーティ製のパーサーないしインターフェイスライブラリがたくさn公開されています
一番上で上げたとおりunity向けのパーサー何種類もあります。
ue4でも読み込みサポートされてるようですし
autodesk stingrayもluaスクリプトが使えます(むしろluaが開発言語?ゲームのロジックまでluaは逆にしんどい気もしますが…

ゲーム業界で最もスタンダードなスクリプト言語Lua使えて損はないですし
unityに組み込んでみてはどうでしょう?