経緯と概要
以前、LuaファイルをTextAssetに対応するという記事を書きました。
今回はカスタムスクリプトローダーを作成し、Luaスクリプトの読み込みを便利にしようと思います。
UnityAssetsScriptLoaderを公式が提供してはいますが、現在非推奨のResources
を要求しているので、自前で実装する必要があります。
調査
まずは、公式チュートリアルと、公式のGithubで公開されているUnityAssetsScriptLoaderを確認します。
その上で分かった最低限必要なことは以下の2つ。
-
ScriptLoaderBase
(またはIScriptLoader
)を継承する - 直接アセットを読みに行くのではなく、Luaのファイル名とその内容を連想配列で保持する
実装
今回の目的は以下の通りです。
- DoFileでLuaスクリプトを実行する
- Luaの
require
を使えるようにする
ScriptLoaderBaseを継承
まず、ScriptLoaderBaseを継承したクラスを定義し、override必須の関数を実装します。
UnityAssetsScriptLoaderをベースに実装しますが、Luaスクリプトをアセットから取得して、連想配列に格納する処理は別で実装します。
using System;
using System.Collections.Generic;
using MoonSharp.Interpreter.Loaders;
public class CustomScriptLoader : ScriptLoaderBase
{
private Dictionary<string, string> _resources;
public CustomScriptLoader(Dictionary<string, string> resources)
{
_resources = resources;
}
// [必須] 実際に読み込むファイル情報を返す
public override object LoadFile(string file, Table globalContext)
{
if (_resources.ContainsKey(file))
{
return _resources[file];
}
else
{
throw new Exception($"Cannot lead script '{file}'.");
}
}
// [必須] 読み込みたいファイルが存在しているかを返す
public override bool ScriptFileExists(string name)
{
return _resources.ContainsKey(name);
}
}
これで基礎的な部分は終了です。
適当なコードで実験してみましょう。
挙動確認
連想配列でluaスクリプト群を格納する処理は今回は省きます。
using System.Collections.Generic;
using UnityEngine;
using MoonSharp.Interpreter;
public class Test : MonoBehaviour
{
Dictionary<string, string> _luaAssets;
void Start()
{
Script script = new Script();
script.Options.ScriptLoader = new CustomScriptLoader( _luaAssets );
var value = script.DoFile("sample");
Debug.Log( value.String );
}
}
return 'Hello'
手段は問いませんが、sample.luaのファイル名と中身を _luaAssetsの中に格納して実行します。
[00:00:00] Hello
UnityEngine.Debug:Log (object)
以上の結果が得られれば大丈夫です。
以前はDoStringで実行していましたが、これでDoFileで実行できるようになりました。
requireへ対応する
最初の目的である、DoFileでのLuaスクリプトの実行は成功しましたが、requireは動作しません。
これには、IScriptLoader
で定義されている関数をoverrideする必要があります。
(ScriptLoaderBaseを継承しているために警告として表示されていません)
using System;
using System.Collections.Generic;
using MoonSharp.Interpreter.Loaders;
public class CustomScriptLoader : ScriptLoaderBase
{
/* 省略 */
public override string ResolveFileName(string fileName, Table globalContext) => fileName;
public override string ResolveModuleName(string modName, Table globalContext) => _resources.ContainsKey(modName) ? modName : null;
}
ResolveFileName
とResolveModuleName
という2つの関数を実装しました。
ScriptLoaderBaseを継承しているので、必須なのはResolveModuleName
だけですが、両方実装したのは気分です。
ザックリと説明するならば、requireで要求したモジュールが本当に存在しているのかを調べ、存在しているならばそのファイル名を返します。今回はファイル名とモジュール名を完全一致させる予定なので、そのままmodNameを返しています。
挙動確認
では今回は、sample_module.luaというファイルに関数を定義し、sample.luaで使ってみましょう。
public class Test : MonoBehaviour
{
Dictionary<string, string> _luaAssets;
void Start()
{
Script script = new Script();
script.Options.ScriptLoader = new CustomScriptLoader( _luaAssets );
var value = script.DoFile("sample");
Debug.Log( value.Number );
}
}
require('some_module')
return pow( 5 )
function pow (n)
return n * n
end
[00:00:00] 25
UnityEngine.Debug:Log (object)
残る課題
ひとまず当初の目的は達成できました。
Luaスクリプトをロードする処理自体は下記のような形で、Addressableで適当なグループに突っ込んだものを一括で読み込む処理を実装すればひとまずは解決でしょう。
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEditor.AddressableAssets;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableLuaAssetLoader
{
public static async Task<Dictionary<string, string>> LoadFromGroupAsync(string groupName)
{
var group = AddressableAssetSettingsDefaultObject.Settings.FindGroup(groupName);
if (group != null)
{
var resources = new Dictionary<string, string>();
foreach (var item in group.entries)
{
var asyncOperation = Addressables.LoadAssetAsync<TextAsset>(item.guid);
await asyncOperation.Task;
if (asyncOperation.Status == AsyncOperationStatus.Succeeded)
{
var loadedAsset = asyncOperation.Result;
resources.Add(loadedAsset.name, loadedAsset.text);
}
else
{
Debug.LogError("Failed to load assets guid: " + item.guid);
}
}
return resources;
}
return null;
}
}
ただ、現在のままではLuaスクリプトが更新された時にすぐに反映はされないという問題が残っています。
そこまで対応して初めてLuaの良さを活かし切れると言っても過言ではないでしょう。
幸い、Unityには AssetPostprocessor
というものが提供されているので、実装は苦ではないはずです。
ですが、今回の目的からは外れてしまうのでここまでとします。