Unity TimeLine学习笔记

目录

一、快捷键

二、常用轨道介绍

Activetion Track(激活轨道)

Animation Track(动画轨道)

1、添加其已有的动画

2、制作帧动画

Audio Track(音频轨道)

Signal Track(信号轨道)

1、点开Markers,右键创建Signal Emitter

2、选中创建的Signal Emitter,创建一个 Signal assets

3、在Signal Receiver 上给他添加要执行的方法就行了

4、在模式中选择第二个,在编辑器模式下就能看到相关信息,推荐选这个

三、自定义轨道

1、自定义Track

2、自定义Clip

3、自定义Behaviour

4、自定义Mixer

先在Track脚本里创建出Mixer

创建MixerBehaviour

四、自定义clip显示窗口,为自定义的clip写一个Editor

 五、自定义signal

六、常见的对话剧情效果制作总结

1、暂停效果

2、快速通过这个clip(在当前clip中点击时跳转到当前clip的最后一帧)

3、跳转到目标帧

①JumpToMarker的实现

② JumpToClip的实现

③JumpToTime的实现(不推荐使用这种方式)

 ④合并方法


一、快捷键

ctrl + 鼠标中键——滑动中键,缩放轨道宽度

F——选中轨道中的其中一个片段,按住f,会将这个片段缩放到最适合编辑的宽高

A——将窗口调整到能看到所有的编辑片段的大小

L(Locked)——选中某一个轨道,按住L,会锁住选中的轨道,让其不再被编辑

M(Muted)——选中一个轨道,按住M,禁用这一条轨道

space——开始播放TimeLine,再按一下就是暂停播放

二、常用轨道介绍

Activetion Track(激活轨道)

控制一个对象的激活与隐藏,在片段内的时间段时激活这个物体,其与时候关闭这个物体

Animation Track(动画轨道)

控制一个对象动画的播放,以及为其制作帧动画,注意:这个对象要有Animator组件

1、添加其已有的动画

 右键选着Add From Animation Clip 选项进行添加已有的动画

2、制作帧动画

点击录制按钮进行录制

移动场景中对应的物体,打下关键帧

 

再次点击录制按钮,停止录制,右键选择Convert To Clip Track将这一段帧动画转换成一个动画片段 

 双击这个帧动画片段可以进入Animation窗口对其进行更加细致的编辑,选中这个动画片段,在右边有窗口显示其相关属性,可以对其进行调节

Audio Track(音频轨道)

拖入音频即可

选中轨道会弹出相关设置界面

Volume——声音大小

Stereo Pan——控制声道的,往右偏移就偏向右声道,反之偏向左声道

Spatial Blend——是否受到3D影响

Signal Track(信号轨道)

通俗来讲,就是能让你在某一帧的时候调用,某一个方法

操作步骤

1、点开Markers,右键创建Signal Emitter

Markers就相当于是一个全局的Signal Track轨道

2、选中创建的Signal Emitter,创建一个 Signal assets

3、在Signal Receiver 上给他添加要执行的方法就行了

4、在模式中选择第二个,在编辑器模式下就能看到相关信息,推荐选这个

三、自定义轨道

Track——代表的就是轨道体,一行

Clip——这个轨道里面的片段

Behavior(Data)——片段里的行为,会执行什么样的操作

Mixer——混合器,片段与片段之间的混合

自定义一个轨道就是对这4个部分进行自定义

1、自定义Track

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;//要申明TimeLine的命名空间



[TrackColor(100 / 255f, 120 / 255f, 0)]   //定义轨道的颜色
//[TrackClipType(typeof(Light))]   //定义片段类型。但不要声明Unity自带的类型,因为这样的话就只能操作这一个对象,就和自带的轨道一样。我们这里实现可以在一个轨道上操作多个对象
[TrackClipType(typeof(LightClip))] //这里声明我们自定义的Clip类型,实现可以在一个轨道上操作多个对象。在clip里声明对象类型,就可以一个Clip操作一个对象
public class LightTrack : TrackAsset //自定义Track时,要继承自TrackAsset
{
}

2、自定义Clip

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables; //需要引入的命名空间
using UnityEngine.Timeline; //需要引入的命名空间


//自定义Clip
public class LightClip : PlayableAsset, ITimelineClipAsset //需要继承自PlayableAsset, 申明一下ITimelineClipAsset接口
{
    //public Light light;  //这样写是错误的,因为在这里不支持声明此类型,我们要对其进行封装
    public ExposedReference<Light> light;  //对Light类型进行封装,封装成这里可以识别的类型
    public Color Color;
    public float index;


    //在Clip的属性窗口,显示可调节的属性更多(需要有ITimelineClipAsset接口)
    public ClipCaps clipCaps
    {
        get { return ClipCaps.All; }
    }


    //实现创建Clip的抽象方法
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        var playable = ScriptPlayable<LightBehavior>.Create(graph); //给自定义的Clip,添加自定义的Behavior(这里LightBehavior为自定义的Behavior)

        var behavior = playable.GetBehaviour(); //获取到我们自定义的behavior

        behavior.Light = light.Resolve(graph.GetResolver()); //赋值的时候在对其进行解封
        behavior.LightColor = Color;
        behavior.LightIndex = index;

        return playable;
    }
}

3、自定义Behaviour

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;//需要引入的命名空间

//自定义Behavior
[System.Serializable] 
public class LightBehavior : PlayableBehaviour //需要继承自PlayableBehaviour
{
    public Light Light; //光源对象

    public Color LightColor; //光源颜色
    public float LightIndex;//光照强度


    //在(Clip)片段上的每一帧调用
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        Light.color = LightColor;
        Light.intensity = LightIndex;
    }
}

这里只用到了ProcessFrame这一种方法,在PlayableBehaviour里还封装了许多方法

介绍下常用的两个

OnBehaviourPlay 在进入当前Clip时调用一次

OnBehaviourPause 在离开当前Clip时调用一次

4、自定义Mixer

自己对Mixer的理解,Mixer混合器是一种对轨道中所有Clip进行操作的方法

先在Track脚本里创建出Mixer

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Timeline;
using UnityEngine.Playables;
using System.Linq;

[TrackColor(0.8f,0,0)]
[TrackClipType(typeof(DialogClip))]
public class DialogTrack : TrackAsset
{
	/// <summary>
	/// 创建Mixer
	/// </summary>
	/// <param name="graph"></param>
	/// <param name="go"></param>
	/// <param name="inputCount"></param>
	/// <returns></returns>
	public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
	{
		var scriptPlayable = ScriptPlayable<DialogMixerBehaviour>.Create(graph, inputCount);

		DialogMixerBehaviour b = scriptPlayable.GetBehaviour();        //得到Mixer的Behavior
		b.markerClips = new Dictionary<string, double>();
		b.endMarkerClips = new Dictionary<string, double>();
		b.clipNameLis = new List<string>();
		b.markerLis = new List<DestinationMarker>();
  

		DialogTrack dialogTrack = this;
                //DestinationMarker是我自定义的一个signalMarker,这里是因为我后面要做信号跳转所用的
		var markerList = dialogTrack.GetMarkers().OfType<DestinationMarker>().ToList(); //得到当前轨道上的所有DestinationMarker信号标记  
               //存储轨道上的DestinationMarker信号标记
		b.markerLis.AddRange(markerList);
  

		//循环遍历轨道上的所有Clip
		foreach (var c in GetClips())
		{
			DialogClip clip = (DialogClip)c.asset;
                        if (!b.markerClips.ContainsKey(c.displayName))
                           {
                                // 存储Clip的开始帧,Key为Clip的名字                               
                                b.markerClips.Add(c.displayName, (double)c.start);
                                //存储Clip的结束帧,key为Clip的名字
				b.endMarkerClips.Add(c.displayName, (double)c.end);
                           }
			if (!b.clipNameLis.Contains(c.displayName))
			{
                               //存储Clip的名字    
				b.clipNameLis.Add(c.displayName);
			}
		}
		return scriptPlayable;
	}

}

创建MixerBehaviour

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

public class DialogMixerBehaviour : PlayableBehaviour
{
    public Dictionary<string, double> markerClips; //用来存储Clip的开始帧,Key为Clip的名字
    public Dictionary<string, double> endMarkerClips;  //存储Clip的结束帧,key为Clip的名字
	public List<string> clipNameLis;             //存储Clip的名字
	public List<DestinationMarker> markerLis;    //轨道上的DestinationMarker信号标记

	//在整个TimeLine开始播放时会执行一次
	public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
		
		int inputCount = playable.GetInputCount();  //得到轨道上所有Clip的Behaviour对象的数量,即Clip的数量

		for (int i = 0; i < inputCount; i++)
		{
			ScriptPlayable<DialogBehaviour> inputPlayable = (ScriptPlayable<DialogBehaviour>)playable.GetInput(i);
			DialogBehaviour input = inputPlayable.GetBehaviour();
			
			input.markerClips = markerClips;
			input.endMarkerClips = endMarkerClips;
			input.clipName = clipNameLis[i];
		}

		UI.TimeLineDialogUIController.Self.destinations = markerLis;
	}


	/// <summary>
	/// ProcessFrame方法 在Mixerbehavior中时,只要整个Timeline还在运行时就会调用,相当于update
	/// </summary>
	/// <param name="playable"></param>
	/// <param name="info"></param>
	/// <param name="playerData"></param>
	public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {

		
	}
   
}

四、自定义clip显示窗口,为自定义的clip写一个Editor

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;


[CustomEditor(typeof(DialogClip))] //这里要继承自对应的clip
public class DialogClipInspector : Editor
{
	//通用窗口显示的属性
	private SerializedProperty rushThrough;
	private SerializedProperty IsJumpProp;
	private SerializedProperty npcId;
	private SerializedProperty segmentId;
	private SerializedProperty hasToPause;
	private SerializedProperty isChoice;

	private void OnEnable()
	{
		//通用窗口显示的属性
		npcId = serializedObject.FindProperty("npcId");
		segmentId = serializedObject.FindProperty("segmentId");
		rushThrough = serializedObject.FindProperty("rushThrough");
		hasToPause = serializedObject.FindProperty("hasToPause");
		IsJumpProp = serializedObject.FindProperty("isJump");
		isChoice = serializedObject.FindProperty("isChoice");
	}

	public override void OnInspectorGUI()
	{
		//通用窗口显示的属性
		EditorGUILayout.PropertyField(npcId);
		EditorGUILayout.PropertyField(segmentId);
		EditorGUILayout.PropertyField(rushThrough);
		EditorGUILayout.PropertyField(hasToPause);
		//isJump
		EditorGUILayout.PropertyField(IsJumpProp);
		//得到IsJumpProp 的bool值
		bool index = IsJumpProp.boolValue;
		
		if (index)
		{
			//如果为真,窗口显示下面两个属性 (特定显示属性)
			EditorGUILayout.PropertyField(serializedObject.FindProperty("isAutomaticJump"));
			EditorGUILayout.PropertyField(serializedObject.FindProperty("markerLabel"));
		}
		//isChoise
		EditorGUILayout.PropertyField(isChoice);
		bool indexChoice = isChoice.boolValue;
		if (indexChoice)
		{
			//如果勾选isChoice,窗口显示下面的属性(特定显示属性)
			EditorGUILayout.PropertyField(serializedObject.FindProperty("choiceJumpTarget"));
		}

		serializedObject.ApplyModifiedProperties();
	}
}

勾选相应的选项,弹出与之相关的变量

 没勾选就不显示

 五、自定义signal

这里我们以一个自定义的跳转signal为例子,先看一下效果

详请介绍:https://blog.csdn.net/culiao6493/article/details/108641237

里面的实列项目:signal自定义项目

上面这个链接里的文章有很好的讲解,并且有项目源码可以看,这里不做太多的赘述。

六、常见的对话剧情效果制作总结

1、暂停效果

这个用的地方很多,比如某段话完了后要暂停,等待玩家的点击在继续播放下一段对话

官方的项目的做法,用PlayableDirector组件进行控制,简单说一下TimeLine这个组件:

Playable是TimeLine的资源,就是你做的那一堆剧情

PlayableDirector就是TimeLine的播放控制器,里面能够控制播放,暂停,结束等操作

实现方法直接上代码

//这里都写在自定义的Behaviour里面
  
private PlayableDirector director;

public override void OnPlayableCreate(Playable playable)
{
     //得到当前Playable资源挂载的PlayableDirector组件
     director = (playable.GetGraph().GetResolver() as PlayableDirector);    
}

//在(Clip)片段上的每一帧调用
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {

        double duration = playable.GetDuration(); //当前clip 总的时间
        double time = playable.GetTime();   //当前的时间   
        //如果当前clip结束时要暂停                         

        if (pauseScheduled)
        {
            //当播放到离这段clip还差0.1秒结束时,调用暂停播放
            if (time >= (duration - 0.1f))
            {
                UI.TimeLineDialogUIController.Self.PauseTimeline(director);
                pauseScheduled = false;


            }
        } 
    }

这里可能会有一点奇怪,明明已经有了OnBehaviourPause(clip结束帧调用)方法不用,反而要用这种方式,后面给出解释

         /// <summary>
        /// 暂停TimeLine的播放
        /// </summary>
        /// <param name="whichOne"></param>
        public void PauseTimeline(PlayableDirector whichOne)
        {
            activeDirector = whichOne;

            activeDirector.playableGraph.GetRootPlayable(0).SetSpeed(0d);//将TimeLine的播放速度设为0
            //将当前的时间赋值为当前clip的结束帧
            activeDirector.time = clipEndTime;
        }

这里的 clipEndTime 指的是当前clip片段的结束帧,这里讲一下我为啥要这样做,以及为啥不用OnBehaviourPause方法

因为我发现当我在结束帧调用这个方法的时候,其实并不会在当前结束帧就立马暂停,而是向后播放了几帧后才暂停,如果在当前clip后面紧跟着下一个clip,就会播放到下一个clip上面才暂停

至于怎么获取当前clip的结束帧,在自定义Mixer章节里有写。

2、快速通过这个clip(在当前clip中点击时跳转到当前clip的最后一帧)

其实这个功能比较简单,因为我们前面已经储存了所有Clip的结束帧,所以我们只需要判断当前clip在什么时候点击需要跳转到最后一帧就好了

直接上代码

public class DialogBehaviour : PlayableBehaviour
{
    //外部控制这个变量


    public double rushThrough;                      //快速通过这个Clip

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
 
        double duration = playable.GetDuration(); //当前clip 总的时间
        double time = playable.GetTime();   //当前的时间
        
        if (rushThrough == 0)
        {
           //如果为0,整段clip都可以快速通过
            UI.TimeLineDialogUIController.Self.rushThrough = true;
        }
        else if (rushThrough < 0 || rushThrough >= duration)
        {
           //如果小于0或者填的时间大于总的时间,整段clip都不可以快速通过
           UI.TimeLineDialogUIController.Self.rushThrough = false;
        }
        else
        {
            if (time <= rushThrough)
            {
               //在rushThrough时间节点之前点击不能快速通过
               UI.TimeLineDialogUIController.Self.rushThrough = false;
            }
             else
             {
                 //在rushThrough时间节点之后点击可以快速通过
                UI.TimeLineDialogUIController.Self.rushThrough = true;
             }
        }

                       
    }


      

}
        /// <summary>
        /// 跳转到目标帧
        /// </summary>
        /// <param name="whichOne"></param>
        /// <param name="starTime">目标帧</param>
        public void JumpToTragetFrame(PlayableDirector whichOne, double starTime)
        {
            whichOne.time = starTime;
            
        }

3、跳转到目标帧

这个一般用在有选项的情况下比较多,选着不同的选项会跳转到不同的地方

目前来说,我用了三种方式,我定义了一个枚举来进行区分

public enum JumpToType
    { 
      JumpToMarker,  //跳转到信号标记点DestinationMarker
      JumpToTime,   //跳转到摸一个时间节点,格式为 xx:xx , 如 6:30 表示第6秒过30帧
      JumpToClip    //跳转到某一个对话dialogClip
    }

 定义了一个数据结构来进行选择

    /// <summary>
    /// 跳转的数据结构
    /// </summary>
    [Serializable]
    public struct TimelineDiaLogDataBase
    {
        public JumpToType Type;
        public string jumpToValue;
    }

 如果类型选JumpToMarker,value就填marker的名字,JumpToTime,value填时间节点,JumpToClip,value填clip的名字

①JumpToMarker的实现

先自定义一个marker

[DisplayName("Jump/DestinationMarker")]
[CustomStyle("DestinationMarker")]  //自定义marker的图片
public class DestinationMarker : Marker
{
    [SerializeField] public bool active;  //是否启用
    
    void Reset() 
    {
        active = true;
    }
}

关于自定义marker的图片可以看第五节里的链接,里面有详细的讲解和项目

我们就可以在轨道上添加一个marke

然后我们需要在自定义的Track里去获取这个轨道上的所有marke,至于我为啥要在Track里取获取,是因为我只在Track继承的父类里找到了相关的获取方法,应该还有其他的我可能没找到

在自定义Mixer那一章的代码段里有写

② JumpToClip的实现

实现也很简单,把轨道上每个clip的开始帧存下来,点击的时候,将时间设置为对应clip的开始帧就ok了

至于怎么存clip的开始帧,在自定义Mixer一章里有写

③JumpToTime的实现(不推荐使用这种方式)

首先,讲一下我为啥要用xx:xx , 如 6:30 表示第6秒过30帧这种格式,这与timeLine上的显示相对应,方便查看填写

先将填写字符转换成对应的时间,然后再点击的时候,跳转到这个时间点就ok了

        /// <summary>
        /// 将“6:30”(冒号前为时间,后为帧数)格式的字符串进行转换成时间
        /// </summary>
        /// <param name="timeOrFrame"></param>
        public float ConversionCharacter(string timeOrFrame)
        {
            float allTime;
            string[] arr = timeOrFrame.Split(':');
            float result;
            bool isNum = isNumberic(arr[1], out result);
            if (isNum)
            {
                float timeMs = result / 60;
                bool isNumber = isNumberic(arr[0], out result);
                allTime = result + timeMs;
            }
            else
            {
                allTime = 0;
            }
            return allTime;
        }
        /// <summary>
        /// 判断是否为数字
        /// </summary>
        /// <param name="message"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        protected bool isNumberic(string message, out float result)
        {
            System.Text.RegularExpressions.Regex rex =
            new System.Text.RegularExpressions.Regex(@"^(-?\d+)(\.\d+)?$");
            result = -1;
            if (rex.IsMatch(message))
            {
                result = float.Parse(message);
                return true;
            }
            else
                return false;
        }

 ④合并方法

上面三种其实到最后都是转换成相应的时间节点,来进行跳转,所以我们合并一下,统一调用

        /// <summary>
        /// 得到跳转项的目标时间
        /// </summary>
        /// <param name="dataBase"></param>
        /// <returns></returns>
        public double GetChoiceTargetFrame(TimelineDiaLogDataBase dataBase)
        {
            double valueTime = 0;
            if (dataBase.Type == JumpToType.JumpToClip)
            {
                foreach (var makers in markerClips)
                {
                    if (makers.Key == dataBase.jumpToValue)
                    {
                        valueTime = makers.Value;
                        break;
                    }
                }
            }
            else if (dataBase.Type == JumpToType.JumpToTime)
            {
                var value = dataBase.jumpToValue;
                valueTime = ConversionCharacter(value);

            }
            else if (dataBase.Type == JumpToType.JumpToMarker)
            {
                foreach (var marker in destinations)
                {
                    if (marker.active) //marker是否启用
                    {                       
                        if (marker.name == dataBase.jumpToValue) //通过marker的名字来进行寻找
                        {
                            valueTime = marker.time;
                            break;
                        }
                    } 
                }
            }
            return valueTime;
        }

注意:clip和marker的名字不能重复

猜你喜欢

转载自blog.csdn.net/such_so/article/details/125800990