【Unity】LuaPlayableAsset を作ってみる【Timeline】

動機

UnityではAssetBundleにスクリプト(c#)を入れれない制約と、機能を追加するたびに PlayableBehaviour を継承したコードが増えるのをどうにかしたかった。

結果

想定通り動きました。

アプローチ

Luaが動くようにする

https://github.com/Tencent/xLua

Luaには xLua を使用します。
xLua の導入は紹介サイトが大量にあるので省略します。

シングルトンの Lua を回す Behaviour を作成します。

Luaが動いているだけのオブジェクト
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 が動くようになりますが、今回は実行中のみ動くようにしてます。

タイムラインを接続する

test.timelin.asset は新規に作成追加

今回はデフォルトの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 に公開しました。

One Comment

Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です