【Unity】LuaPlayableAsset を作ってみる【Timeline】
動機
UnityではAssetBundleにスクリプト(c#)を入れれない制約と、機能を追加するたびに PlayableBehaviour を継承したコードが増えるのをどうにかしたかった。
結果
想定通り動きました。
アプローチ
Luaが動くようにする
Luaには xLua を使用します。
xLua の導入は紹介サイトが大量にあるので省略します。
シングルトンの Lua を回す Behaviour を作成します。
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
//[ExecuteInEditMode]
public sealed class LuaEngine : UnityEngine.MonoBehaviour
{
static public XLua.LuaEnv LuaEnv
{
get
{
if( luaEnv == null)
{
luaEnv = new XLua.LuaEnv();
}
return luaEnv;
}
}
static protected XLua.LuaEnv luaEnv = null;
//----------------------------------------------------------------------------
private void Update()
{
luaEnv?.Tick();
}
} // class LuaEngine
//[ExecuteInEditMode] 部分のコメントアウトを外すと エディット中もTick が動くようになりますが、今回は実行中のみ動くようにしてます。
タイムラインを接続する
今回はデフォルトのSphereオブジェクトに Playable Director コンポーネントを接続します。
Timeline は Assets > Create > Timeline から適当に作成し Playable に設定します。
LuaPlayableAsset を作る
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
public class LuaPlayableAsset : PlayableAsset
{
[SerializeField]
public TextAsset textAsset;
//----------------------------------------------------------------------------
public override Playable CreatePlayable(
UnityEngine.Playables.PlayableGraph graph,
GameObject owner
)
{
var behaviour = new LuaPlayableBehaviour();
behaviour.textAsset = textAsset;
behaviour.owner = owner;
return ScriptPlayable<LuaPlayableBehaviour>.Create( graph, behaviour);
}
} // class LuaPlayableAsset
TextAsset textAsset に Luaコードを設定する部分になります。
string luaText; とかにして生Lua を設定するのも可能でが、一文字入力するたび Luaが走るので syntax error が出まくります。いい感じにチューニングしてください。
LuaPlayableBehaviour を作る
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
public class LuaPlayableBehaviour : PlayableBehaviour
{
public TextAsset textAsset{ get; set; }
public GameObject owner{ get; set; }
private XLua.LuaTable scriptEnv = null;
private Action luaUpdate;
private Action luaOnDestroy;
//----------------------------------------------------------------------------
// PlayState.Pausedに変更されたときに呼び出される
public override void OnBehaviourPause(
UnityEngine.Playables.Playable playable,
UnityEngine.Playables.FrameData info
)
{
luaOnDestroy?.Invoke();
luaOnDestroy = null;
scriptEnv?.Dispose();
scriptEnv = null;
luaUpdate = null;
}
//----------------------------------------------------------------------------
// PlayState.Playingに変更されたときに呼び出される
public override void OnBehaviourPlay(
UnityEngine.Playables.Playable playable,
UnityEngine.Playables.FrameData info
)
{
if( textAsset != null)
{
scriptEnv = LuaEngine.LuaEnv.NewTable();
// スクリプトごとに別々の環境を設定すると、スクリプト間でのグローバル変数と関数の競合をある程度防ぐことができます。
XLua.LuaTable meta = LuaEngine.LuaEnv.NewTable();
meta.Set( "__index", LuaEngine.LuaEnv.Global);
scriptEnv.SetMetaTable( meta);
meta.Dispose();
scriptEnv.Set( "self", owner);
LuaEngine.LuaEnv.DoString( textAsset.text, textAsset.GetHashCode().ToString(), scriptEnv);
Action luaAwake = scriptEnv.Get<Action>( "awake");
scriptEnv.Get( "update", out luaUpdate);
scriptEnv.Get( "ondestroy", out luaOnDestroy);
luaAwake?.Invoke();
}
}
//----------------------------------------------------------------------------
// PlayableGraphのProcessFrameフェーズの間に呼び出される
// ProcessFrameはあなたのPlayableがその仕事をする段階です。
// PlayableがPlayPlayableOutputを直接または間接的に再生して接続するときに、各フレームに対して呼び出されます。
public override void ProcessFrame(
UnityEngine.Playables.Playable playable,
UnityEngine.Playables.FrameData info,
object playerData
)
{
if( Application.isPlaying)
{
luaUpdate?.Invoke();
}
}
} // class LuaPlayableBehaviour
挙動の方のコードになります。
OnBehaviourPlay が呼ばれたときに Lua を走らせ、もし “awake” “update” “ondestroy” という function があれば必要に応じて呼んであげます。
Luaのコルーチンを使いたかったので、毎フレーム update() を呼ぶようにします。
OnBehaviourPauseでは後処理をしておきます。
余談ですが、最初に作ったときは、OnBehaviourPlay で XLua.LuaEnv のインスタンスを作って、1アセット1LuaEnvで作ってました。予想はしてましたが、インスタンス時にスパイクが発生し、かなりカクつくので今の形になってます。
Luaを書く
かなり大雑把な Lua コードを 4つほど用意して タイムラインに乗せてます。Lua初心者なので、セオリーから外れているかもしれないです。ご了承ください。
軽く説明すると awake() で コルーチンを作成し、update() でコルーチンを進めてます。ondestroy()で後処理。あとはコルーチンにした関数で挙動を書いてます。
このコードは、生 GameObject を生成したり操作していますが、全くいい例ではありません。本来なら インターフェイスを用意して MonoBehaviour をキャストしインターフェイス経由で操作することを強くおすすめします。
くるくる撃つ Lua
--[[*****************************************************************************
くるくる撃つよ
*****************************************************************************]]--
local delegate = nil
local fireDelay = 0.01 -- 0.01秒ごとに発射
----------------------------------------------------------------------------
function coSequence()
local angle = 0
local delay = 0
local selfTransform = self:GetComponent( typeof( CS.UnityEngine.Transform))
while true do
if delay > 0 then
delay = delay - CS.UnityEngine.Time.deltaTime
goto continue
else
delay = fireDelay
end
angle = angle + 20
dir = CS.UnityEngine.Quaternion.AngleAxis( angle, CS.UnityEngine.Vector3.forward) * CS.UnityEngine.Vector3.right
bullet = CS.UnityEngine.GameObject( '弾丸')
bullet:AddComponent( typeof( CS.BulletBehaviour))
textMesh = bullet:AddComponent( typeof( CS.UnityEngine.TextMesh))
textMesh.text = "弾"
rigidbody = bullet:AddComponent( typeof( CS.UnityEngine.Rigidbody))
rigidbody.transform.position = selfTransform.position
rigidbody.velocity = 50 * dir
rigidbody.useGravity = false
::continue::
coroutine.yield()
end
end
----------------------------------------------------------------------------
function awake()
delegate = coroutine.create( coSequence)
end
----------------------------------------------------------------------------
function ondestroy()
delegate = nil
end
----------------------------------------------------------------------------
function update()
if delegate ~= nil then
coroutine.resume( delegate)
if coroutine.status( delegate ) == "dead" then
delegate = nil
end
end
end
PlayableTrack に配置していく
Timelineウィンドウから Add > PlayableTrack を選択しトラックを追加します (水色の部分) 。トラック上で右クリックから Add Lua Playable Asset でクリップを追加します。(緑色の部分)
追加後 LuaPlayableTrack の Inspector の 赤の部分 TestAsset に先程の Lua ファイルを設定します。クリップの再生中 update() が呼ばれるようになります。
TestAsset は再利用可能で、何種類作成しておいて組み合わせで攻撃パターンを作ることができるかと思います。
トラックは複数作成できるので、移動用、攻撃用など並行で走らせれるのが便利です。
終わりに
別にLuaを使わないでも Animation や Signal 使えばいいやん と思われますが、頻繁な仕様変更に対応するのとAssetBundle だけで挙動が変更できるので スクリプトの PlayableAsset には利用価値があると思います。
プロジェクトは後日公開する予定です。
サンプルを GitHub に公開しました。