Overview
主要封装了Timeline的PlayableDirector
- SetBinding的使用更友好一些(原来的API使用设置,真不知道为何官方要用:PlayableBinding.source来做key,这里我们改成string做key了)
- 还有对timeline的Animation Track使用的binding对想同步控制:播放、暂停、继续
下面演示主要是运行中,动态生成 Animation Track(动画轨道)要控制的:新对象、切换掉旧对象、销魂就对象 下面gif中的ChangeChar就是切换角色,ChanageLight切换灯光的演示
还有:PlayableAsset, PlayableBehaviour的简单使用,放在Playable Track轨道中来使用(脚本轨道)
Running(由于CSDN限制gif的大小不能超过5MB,所以录制过程我操作比较快,而且内容尽量缩减,gif质量降到最低了,不然大小限制)
TimelineProject 开发环境:
- vs 2017
- unity 2017.4.3f
- unity记得要使用Scripting Runtime Version : Stable (.NET 4.6 Equivalent)
- 以上设置位置所在:Edite->Project Setting->Player->Inspector面板中的 Configuration
(代码有丢丢多,所以将Running放前面,Code放这)
TimelineProxy/*
* FileName: TimelineProxy
* Author: Jave.Lin
* CreateTime: #TIME#
* Description: [Description]
*
*/
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
using System;
///
/// Timeline代理的播放状态枚举
///
public enum TimelineProxyPlayState
{
Playing,
Paused,
Stopped
}
///
/// 对外使用的:Timeline辅助类
///
public class TimelineHelper
{
public static TimelineProxy AddTimeline(GameObject go, string timelineName, bool stopAnima = true)
{
var unit = new TimelineProxy();
var director = go.GetComponent();
if (null == director)
director = go.AddComponent();
// 这里为了测试方便,直接从Resources下读取
// 当然啊也是可以从AssetBundle下载、读取
var asset = Resources.Load(timelineName);
unit.Init(timelineName, director, asset);
return unit;
}
}
///
/// 对外使用的:Timeline二次封装的代理类
/// author : Jave.Lin
/// date : 2018-08-20
///
public class TimelineProxy : IDisposable
{
private Dictionary tracksMap;
private Dictionary tracksClipsMap;
private List animatorList;
public bool SyncAnimator { get; private set; }
public string Name { get; private set; }
public PlayableDirector Director { get; private set; }
public PlayableAsset Asset { get; private set; }
public TimelineProxyPlayState PlayState { get; private set; }
public TimelineProxy() { }
public void Dispose()
{
if (tracksMap != null)
{
tracksMap.Clear();
tracksMap = null;
}
if (tracksClipsMap != null)
{
foreach (var item in tracksClipsMap)
{
item.Value.Clear();
}
tracksClipsMap.Clear();
tracksClipsMap = null;
}
if (animatorList != null)
{
animatorList.Clear();
animatorList = null;
}
Director = null;
Asset = null;
}
///
/// 初始化
///
/// 代理的名称
/// 代理的Director
/// 代理的Asset
/// 是否同步代理所有动画Animator(就是代码在播放、暂停、恢复时,同步处理Animator的暂停、继续,这个暂时使用这种方式来处理,API上没有相关的处理,所以这里才有封装的必要,估计以后Unity会改进timeline的功能,提供更友好的API供我们使用)
public void Init(string name, PlayableDirector director, PlayableAsset asset, bool syncAnimator = true)
{
director.playableAsset = asset;
this.Name = name;
this.Director = director;
this.Asset = asset;
this.SyncAnimator = syncAnimator;
this.PlayState = TimelineProxyPlayState.Paused;
tracksMap = new Dictionary();
tracksClipsMap = new Dictionary();
animatorList = new List();
foreach (var o in asset.outputs)
{
var trackName = o.streamName;
tracksMap.Add(trackName, o);
var trackAsset = o.sourceObject as TrackAsset;
var clipList = trackAsset.GetClips();
foreach (var c in clipList)
{
if (!tracksClipsMap.ContainsKey(trackName))
{
tracksClipsMap[trackName] = new Dictionary();
}
var map = tracksClipsMap[trackName];
if (!map.ContainsKey(c.displayName))
{
map.Add(c.displayName, c.asset as PlayableAsset);
}
if (o.streamType == DataStreamType.Animation)
{
var go = director.GetGenericBinding(o.sourceObject) as GameObject;
if (go != null)
{
var animator = go.GetComponent();
if (animator != null)
{
if (syncAnimator) animator.enabled = false;
PushAsyncAnimatorList(go);
}
}
}
}
}
}
///
/// 设置轨道绑定的对象
///
/// 轨道名称
/// 需要绑定的对象
public void SetBinding(string trackName, UnityEngine.Object o)
{
PlayableBinding binding = default(PlayableBinding);
if (tracksMap.TryGetValue(trackName, out binding))
{
Director.SetGenericBinding(binding.sourceObject, o);
var go = o as GameObject;
if (go != null)
{
var animator = go.GetComponent();
if (animator != null)
{
if (PlayState != TimelineProxyPlayState.Playing)
{
animator.enabled = false;
}
PushAsyncAnimatorList(go);
}
}
}
}
///
/// 根据指定的轨道名称,获取对应轨道绑定的对象
///
/// 轨道名称
/// 返回绑定
public UnityEngine.Object GetBinding(string trackName)
{
PlayableBinding binding = default(PlayableBinding);
if (tracksMap.TryGetValue(trackName, out binding))
{
return Director.GetGenericBinding(binding.sourceObject);
}
return null;
}
///
/// 根据指定的轨道名称,获取对象的轨道
///
/// 轨道名称
/// 返回对应的轨道
public PlayableBinding GetTrack(string trackName)
{
PlayableBinding ret = default(PlayableBinding);
if (tracksMap.TryGetValue(trackName, out ret))
{
return ret;
}
return ret;
}
///
/// 根据指定的轨道名称、剪辑名称,获取对象的轨道上的剪辑
///
/// 返回剪辑对象(PlayableAsset)
/// 轨道名称
/// 剪辑名称
/// 返回对应的剪辑
public T GetClip(string trackName, string clipName) where T : PlayableAsset
{
Dictionary track = null;
if (tracksClipsMap.TryGetValue(trackName, out track))
{
PlayableAsset ret = null;
if (track.TryGetValue(clipName, out ret))
{
return ret as T;
}
else
{
Debug.LogError($"GetClip trackName: {trackName} not found clipName: {clipName}");
}
}
else
{
Debug.LogError($"GetClip not found trackName: {trackName}");
}
return null;
}
///
/// 播放
///
public void Play()
{
PlayState = TimelineProxyPlayState.Playing;
if (SyncAnimator) EnabledAllAnimator(true);
Director.Play();
}
///
/// 暂停
///
public void Pause()
{
PlayState = TimelineProxyPlayState.Paused;
if (SyncAnimator) EnabledAllAnimator(false);
Director.Pause();
}
///
/// 恢复
///
public void Resume()
{
PlayState = TimelineProxyPlayState.Playing;
if (SyncAnimator) EnabledAllAnimator(true);
Director.Resume();
}
///
/// 停止
///
public void Stop()
{
PlayState = TimelineProxyPlayState.Stopped;
if (SyncAnimator) EnabledAllAnimator(false);
Director.Stop();
}
private void EnabledAllAnimator(bool value)
{
for (int i = 0, len = animatorList.Count; i < len; i++)
{
var item = animatorList[i];
if (item == null)
{
animatorList.RemoveAt(i);
--len;
--i;
continue;
}
item.GetComponent().enabled = value;
}
}
private void PushAsyncAnimatorList(GameObject go)
{
if (!animatorList.Contains(go))
{
animatorList.Add(go);
}
}
}
PlayableAsset, PlayableBehaviour
PlayableAsset
/*
* FileName: TestingPlayableAsset
* Author: Jave.Lin
* CreateTime: #2018-08-02#
* Description: [Description]
*
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
[System.Serializable]
public class TestingPlayableAsset : PlayableAsset
{
public GameObject removeObj;
public GameObject prefab;
public OpType opType;
public string Name;
public string Msg;
// Factory method that generates a playable based on this asset
public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
{
Debug.Log("TestingPlayableAsset.CreatePlayable");
var b = new TestingPlayableBehaviour();
b.removeObj = removeObj;
b.prefab = prefab;
b.opType = opType;
b.Name = Name;
b.Msg = Msg;
//return Playable.Create(graph);
return ScriptPlayable.Create(graph, b);
}
}
public enum OpType
{
Add,Remove
}
PlayableBehaviour
/*
* FileName: TestingPlayableBehaviour
* Author: Jave.Lin
* CreateTime: #2018-08-02#
* Description: [Description]
*
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
// A behaviour that is attached to a playable
public class TestingPlayableBehaviour : PlayableBehaviour
{
public GameObject removeObj;
public GameObject prefab;
public OpType opType;
public string Name;
public string Msg;
// Called when the owning graph starts playing
public override void OnGraphStart(Playable playable) {
Debug.Log($"{Name}.OnGraphStart Msg:{Msg}");
}
// Called when the owning graph stops playing
public override void OnGraphStop(Playable playable) {
Debug.Log($"{Name}.OnGraphStop Msg:{Msg}");
}
// Called when the state of the playable is set to Play
public override void OnBehaviourPlay(Playable playable, FrameData info) {
Debug.Log($"{Name}.OnBehaviourPlay Msg:{Msg}, info:{str(info)}");
if (removeObj != null)
{
Object.Destroy(removeObj);
removeObj = null;
}
if (opType == OpType.Add)
{
if (prefab != null)
{
var inst = GameObject.Find("new go");
if (inst == null)
{
inst = GameObject.Instantiate(prefab);
inst.name = "new go";
inst.transform.position = new Vector3(0, -2, 0);
var s = Random.Range(1f, 3f);
inst.transform.localScale = new Vector3(s, s, s);
}
}
}
else
{
var inst = GameObject.Find("new go");
if (inst != null)
{
Object.Destroy(inst);
}
}
}
// Called when the state of the playable is set to Paused
public override void OnBehaviourPause(Playable playable, FrameData info) {
Debug.Log($"{Name}.OnBehaviourPause Msg:{Msg}");//, info:{str(info)}");
}
// Called each frame while the state is set to Play
public override void PrepareFrame(Playable playable, FrameData info) {
Debug.Log($"{Name}.PrepareFrame Msg:{Msg}");//, info:{str(info)}");
}
private string str(FrameData info)
{
return string.Join(", ", new string[]
{
$"frameId:{info.frameId}",
$"deltaTime:{info.deltaTime}",
$"weight:{info.weight}",
$"effectiveWeight:{info.effectiveWeight}",
$"effectiveParentDelay:{info.effectiveParentDelay}",
$"effectiveParentSpeed:{info.effectiveParentSpeed}",
$"effectiveSpeed:{info.effectiveSpeed}",
$"evaluationType:{info.evaluationType}",
$"seekOccurred:{info.seekOccurred}",
$"timeLooped:{info.timeLooped}",
$"timeHeld:{info.timeHeld}",
});
}
}
Test bed
/*
* FileName: TestingScript
* Author: Jave.Lin
* CreateTime: #2018-08-02#
* Description: [Description]
*
*/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
public class TestingScript : MonoBehaviour
{
// 需要挂载timeline的对象
// (其实也可以实时new一个GameObject.AddComponent()是一样的效果的)
public GameObject timelineHolder;
// 二次封装后的Timeline代理对象
private TimelineProxy tu;
// 角色类型,测试数据用,0:原始,1:新替换的
private int charType = 0;
// 灯光类型,测试数据用,0:原始,1:新替换的
private int lightType = 0;
#region timeline的播放控制
// 开始播放timeline
public void OnStart()
{
tu.Play();
}
// 停止播放timeline
public void OnPause()
{
tu.Pause();
}
// 回复播放timeline
public void OnResume()
{
tu.Resume();
}
#endregion
#region // 运行过程中改变timeline某个轨道的binding
// 改变角色
public void OnChangedChar()
{
var srcGo = tu.GetBinding("CA") as GameObject;
var error = false;
GameObject go = null;
try
{
if (charType == 0)
{
go = Object.Instantiate(Resources.Load("Prefabs/Chars/MultiCube_Char"));
charType = 1;
}
else
{
go = Object.Instantiate(Resources.Load("Prefabs/Chars/SingleCube_Char"));
charType = 0;
}
tu.SetBinding("CA", go);
}
catch
{
error = true;
}
if (error == false && srcGo != null)
{
CopyTrans(srcGo, go);
Object.Destroy(srcGo);
}
}
// 改变灯光
public void OnChangedLight()
{
var srcGo = tu.GetBinding("LA") as GameObject;
var error = false;
GameObject go = null;
try
{
if (lightType == 0)
{
go = Object.Instantiate(Resources.Load("Prefabs/Lights/NormalSunDirLight_red"));
lightType = 1;
}
else
{
go = Object.Instantiate(Resources.Load("Prefabs/Lights/NormalSunDirLight_yellow"));
lightType = 0;
}
tu.SetBinding("LA", go);
}
catch
{
error = true;
}
if (error == false && srcGo != null)
{
CopyTrans(srcGo, go);
Object.Destroy(srcGo);
}
}
#endregion
private void Awake()
{
tu = TimelineHelper.AddTimeline(timelineHolder, "Timelines/TestingTimeline");
// 设置timeline循环播放
tu.Director.extrapolationMode = UnityEngine.Playables.DirectorWrapMode.Loop;
// 设置timeline的CA轨道的绑定对象(CA: Character Animation)
tu.SetBinding("CA", GameObject.Find("SingleCube_Char"));
// 设置timeline的LA轨道的绑定对象(LA: Light Animation)
tu.SetBinding("LA", GameObject.Find("NormalSunDirLight_yellow"));
var prefab = Resources.Load("Prefabs/Chars/InstaniteGo");
var asset = tu.GetClip("MyPlayableTrack", "AddObjScript");
asset.removeObj = GameObject.Find("RemoveSphere1"); // 设置timeline中PlayableBehaviour来测试删除的对象
asset.opType = OpType.Add;
asset.prefab = prefab; // 设置timeline中PlayableBehaviour来测试控制的对象(根据opType来控制到底是删除还是添加,当然你也可以另创建一个PlayableBehaviour来分别控制删除还是添加的逻辑)
asset = tu.GetClip("MyPlayableTrack", "RemoveObjScript");
asset.removeObj = GameObject.Find("RemoveSphere2"); // 设置timeline中PlayableBehaviour来测试删除的对象
asset.opType = OpType.Remove;
asset.prefab = prefab; // 作用同上
}
private void OnDestroy()
{
if (tu != null)
{
tu.Dispose();
tu = null;
}
timelineHolder = null;
}
private void CopyTrans(GameObject src, GameObject dest)
{
if (src.transform.parent != null)
{
dest.transform.parent = src.transform.parent;
dest.transform.localPosition = src.transform.localPosition;
dest.transform.localScale = src.transform.localScale;
dest.transform.localRotation = src.transform.localRotation;
}
else
{
dest.transform.position = src.transform.position;
dest.transform.localScale = src.transform.localScale;
dest.transform.rotation = src.transform.rotation;
}
}
}
Reference
unity Timeline封装