https://masakami.com/archives/2020/05/09/648/
xlua版の記事書きました
動機
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 で遊べるはらぺこウィッチーズで実際に使われたスクリプトの抜粋です。
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
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にリネームします。
_dramaInterpreter.Load(dramaID, uniqueID);
public Status Update()
{
Status s = _dramaInterpreter.ExecuteScript();
return s;
}
単純化してますが、luaファイルを読み込んで毎フレーム処理してやる。
するとdramaInterpreter内部でluaを適宜パースされうまい具合ドラマ再生されます。
では詳細に見ていきましょう。
luaファイル読み込みと初期化
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から読ませたい場合は適宜修正する必要があります。
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実行、コマンド実行
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が呼ばれます。
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
です。
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に組み込んでみてはどうでしょう?