TimeLine是unity项目内置的工具,使用时不需要主动下载(可能需要更新至最新版本)。
TimeLine是一个全新的可视化工具,而且是一个很好的序列管理工具,可以给不同的对象创建不同类型的轨道,而且在每个轨道当中都可以单独编辑,轨道中的不同资源也可以有序排列融合,作用上可以预先渲染过场动画,适合影视、可交互动画片段的制作。

一、创建TimeLine

TimeLine是Unity内置的工具我们可以直接在Window -> Sequencing -> TimeLine打开TimeLine窗口
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_System
接着创建一个空物体,命名为TimeLine作为TimeLine工具的承载体,打开TimeLine窗口后选中创建的物体,点击Create即可创建TimeLine轨道列表
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_其他_02
选择指定文件夹创建.palyable文件,成功创建的界面如下:
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_03
在进行操作之前注意先将TimeLine窗口锁定,否则选定其它物体的时候它会询问你是否在这个物体上也创建TimeLine工具。
TimeLine窗口的左侧允许我们创建许多不同的轨道,然后在右侧编辑资源片段,点击Add按钮,即可创建不同的轨道
Track Group相当于我们熟知的文件夹,可以对轨道进行分类unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_04

二、人物动画的制作

这里我们需要完成NPC角色的动画效果,因此创建Animation轨道这里可以点击加号创建,也可以直接将所绑定的物体拖至轨道列表创建,拖拽至此系统会自动弹出该游戏物体可以创建的轨道类型)。创建轨道完成并绑定游戏物体后,我们右键单击右侧轨道选择Add From Animation Clip就会弹出你创建好的动画列表unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_右键_05
这里创建好之后如下:unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_右键_06
我们可以在轨道上创建多个不同的动画片段,可以轻松实现动作的衔接。并且可以观察到,第二条轨道的动画有三角形的重叠区域,这便是TimeLine的强大之处,通过两个动画的重叠可以轻松实现两个动画片段的平滑衔接

三、Cut Scene(镜头切换)的实现

当我们存在多个人物角色,并且想要给他们说话或者做动作时分别给不同的角度的特写时就需要用到Cut Scene。
我们有两种Cut Scene实现方法:

  1. 创建多个相机,通过切换它们的存在状态实现
  2. 利用Cinemachine的虚拟相机实现

方法1:创建多个相机

创建多个相机,并调整好它们的位置后,逐个拖拽至轨道列表创建Activation Track。这里创建好之后如下:
创建相机时注意把新建的相机的Audio Listener取消勾选,并且保证同一时间点只有一个相机起作用。
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_其他_07
Active片段代表相机的作用时间,总体达到的效果如下:
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_其他_08
通过这种方法实现有两个缺陷:
1. 每创建一个相机视角就要多创建一个轨道,如果视角过多可能导致TimeLine乱且不易编辑。
3. 该方法实现的镜头切换并不能体现TimeLine的强大功能 – 平滑切换(即使你将两个相机的存在状态重合)

方法二:Cinemachine

Cinemachine使用之前需要在Package Manager中下载。
Cinemachine可以理解为是一个相机管理类工具
在安装完成之后unity的界面上就会出现Cinemachine窗口栏(如果没有,你也可以在Component中找到Cinemachine),并且在Packages文件夹中会出现Cinemachine。unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_右键_09
右键单击Hierarchy界面选择Cinemachine创建普通虚拟相机。创建完成后它会自动在你的主相机上添加Cinemachine Brain组件。将虚拟相机移动到想要的位置这里建议按下想要移动的虚拟相机上的solo按钮,否则使用Ctrl + Shift + F移动虚拟相机时可能出现所有虚拟相机都移动到同一个位置的BUG),然后创建Cinemachine轨道,该轨道绑定的是带有Cinemachine Brain组件的游戏物体即我们的主相机安装Cinemachine后出现的新轨道),在将相应的虚拟相机拖拽至轨道上即可创建镜头片段也可右键单击创建然后在属性面板赋值)。创建完成后TimeLine窗口如下:
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_10
可以看到我们以及可以通过TimeLine实现镜头平滑过渡的效果了,总体效果如下:
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_System_11

四、自定义轨道

1、TimeLine轨道的四个组成部分

  1. Track / 轨道
    [ 继承自TrackAsset ]
    unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_System_12

  2. Clip / 资源、片段
    [ 继承自PlayableAsset ]
    Clip即轨道上可创建的资源片段。
    unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_13

  3. DataBehaviour / 行为
    [ 继承自PlayableBehaviour ]
    DataBehaviour 为每个资源片段中对应的行为逻辑(可以理解为Clip中的值)
    unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_其他_14

  4. Mixer / 混合器
    [ 继承自PlayableBehaviour]
    Mixer用于处理有交互的、融合叠加的、渐入渐出的相邻片段的相关逻辑
    unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_15

2、创建对话轨道的示例(代码控制TimeLine自定义轨道)

第一步:创建轨道
创建C#脚本DialogueTrack,添加命名空间using UnityEngine.Timeline;
修改脚本继承的基类为TrackAsset。这时候保存脚本我们就可以在TimeLine窗口上创建名为Dialogue Track的轨道了。
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_System_16
如果我们想让该轨道像其它轨道一样可以绑定指定类型的游戏对象,只需要添加特性:[TrackBindingType(typeof(Rigidbody))]
修改轨道颜色,只需要添加特性:
[TrackColor(1f,1f,1f)]
这里我不需要绑定游戏对象,只将轨道修改为白色。
暂时的代码如下:
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_其他_17
第二步:创建Clip资源
我们先创建DialogueBehaviour脚本(因为clip资源需要特定的behaviour才能创建)。
添加命名空间using UnityEngine.Playables;并将继承类修改为PlayableBehaviour
【这里我们只是创建脚本用于clip资源的创建,待会我们会再回到该脚本对它进行修改】
创建DialogueClip脚本代码如下:

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

public class DialogueClip : PlayableAsset
{
    //创建DialogueBehaviour类型的实例(注意访问级别一定要是public)
    public DialogueBehaviour template = new DialogueBehaviour();

    //当脚本继承自PlayableAsset时,它会报错,点击快速修复实现该虚基类的虚方法即可创建该方法
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        //通过Create函数创建Playable类型的实例(注意参数类型 <PlayableGraph,DialogueBehaviour>)
        var playable = ScriptPlayable<DialogueBehaviour>.Create(graph,template);
        return playable;
    }
}

但是我们创建这两个脚本后依旧无法再DialogueTrack上创建DialogueClip片段,这是因为我们的轨道还没有跟我们的资源片段进行关联。
我们回到DialogueTrack脚本,添加特性:[TrackClipType(typeof(DialogueClip))]
现在,我们就可以添加DialogueClip资源片段了,接下来我们只需要修改DialogueBehaviour脚本来实现对资源片段的编辑即可。
第三步:实现对Clip的修改,同时实现TimeLine的功能
虚基类PlayableBehaviour存在许多内置的虚函数,我们可以通过重写这些函数来与TimeLine进行交互。
PlayableBehaviour代码如下:

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

[System.Serializable]//序列化。(如不添加该特性就无法在inspector中编辑Clip片段)
public class DialogueBehaviour : PlayableBehaviour
{
    private PlayableDirector playableDirector;//获得TimeLine游戏物体上的PlayableDirector组件

    public string characterName;
    [TextArea(8, 1)] public string dialogueLine;
    public int dialogueSize;

    private bool isClipPlayed;//该Clip片段是否已经播放结束
    public bool requirePause;//用户设置:该对话完成后,是否需要按空格键才能继续
    private bool pauseScheduled;//临时判断逻辑(功能上与requirePause是一致的)

    //在片段创建时被调用
    public override void OnPlayableCreate(Playable playable)
    {
        //获得PlayableDirector对象
        playableDirector = playable.GetGraph().GetResolver() as PlayableDirector;
    }

    //类似与MonoBehaviour中的Update,每一帧都会调用
    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        //这里虽然调用的是每一帧都会调用的函数,但是实际上只会调用一次
        //我们需要的只是判断数据权重是否大于0,来判断我们是否需要进行该操作
        if (isClipPlayed == false && info.weight > 0)
        {
            //进行UI界面的赋值
            UIManager.instance.SetupDialogue(characterName, dialogueLine, dialogueSize);

            //如果需要进行暂停,就把临时判断逻辑pauseScheduled设置为TRUE
            if (requirePause)
            {
                pauseScheduled = true;
            }

            //片段已经播放过,保证只进行一次操作
            isClipPlayed = true;
        }
    }

    //通过Debug可以看出该函数会在TimeLine动画开始时调用一次(但不会实现if语句中的逻辑),并且在Clip片段结束后调用一次
    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        isClipPlayed = false;
        Debug.Log("Stoooooooooooop!!");

        if (pauseScheduled)
        {
            //调用一次后将判断条件设置为false
            pauseScheduled = false;
            GameManager.instance.PauseTimeLine(playableDirector);
        }
        else
        {
            UIManager.instance.ToggleDialogueBox(false);
        }
    }
}

注意添加序列化特性
本例中PlayableBehaviour脚本主要用到三个内置虚函数:

  1. OnPlayableCreate 创建时调用;
  2. ProcessFrame 每帧调用;
  3. OnBehaviourPause TimeLine动画开始时调用一次(但不会实现if语句中的逻辑),并且在Clip片段结束后调用一次;

为了便于代码逻辑的理解,我会将GameManager和UIManager的代码附在后面,里面会有一些基本的注释,同时可以注意一下GameManager中 暂停方法的实现

脚本写完后inspector编辑窗口和TimeLine界面截图如下:
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_18
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_其他_19

GameManager代码如下:

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

public class GameManager : MonoBehaviour
{
    public static GameManager instance;
    public enum GameMode
    { 
        GamePlay,  //动画播放的模式
        DialogueMoment  //资源暂停时刻的模式
    }
    public GameMode gameMode;
    private PlayableDirector currentPlayableDirector;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            if (instance != this)
            {
                Destroy(gameObject);
            }
        }
        DontDestroyOnLoad(gameObject);

        gameMode = GameMode.GamePlay;
        Application.targetFrameRate = 30;//限制游戏帧数
    }  //单例模式

    private void Update()
    {
        if (gameMode == GameMode.DialogueMoment)
        {
            if (Input.GetKeyDown(KeyCode.Space))
            {
                ResumeTimeLine();
            }
        }
    }

    //暂停函数
    public void PauseTimeLine(PlayableDirector _playableDirector)
    {
        currentPlayableDirector = _playableDirector;
        gameMode = GameMode.DialogueMoment;
        //可以类比Time.TimeScale,将动画播放速度设置为0(但仅针对TimeLine)
        currentPlayableDirector.playableGraph.GetRootPlayable(0).SetSpeed(0d);

        UIManager.instance.ToggleSpaceBar(true);
    }

    public void ResumeTimeLine()
    {
        gameMode = GameMode.GamePlay;
        //可以类比Time.TimeScale,将动画播放速度设置为1(但仅针对TimeLine)
        currentPlayableDirector.playableGraph.GetRootPlayable(0).SetSpeed(1d);

        UIManager.instance.ToggleSpaceBar(false);
        UIManager.instance.ToggleDialogueBox(true);
    }
}

UIManager代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIManager : MonoBehaviour
{
    public static UIManager instance;

    public GameObject dialogueBox;

    public Text charaterNameText;
    public Text dialogueLineText;
    public GameObject spacebar;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else
        {
            if(instance != this)
            {
                Destroy(gameObject);
            }
        }
        DontDestroyOnLoad(gameObject);
    }  //单例模式

    //对话框的开关
    public void ToggleDialogueBox(bool _isActive)
    {
        dialogueBox.gameObject.SetActive(_isActive);
    }

    //空格继续文本的开关
    public void ToggleSpaceBar(bool _isActive)
    {
        spacebar.gameObject.SetActive(_isActive);
    }

    //设置文本内容
    public void SetupDialogue(string _name, string _line,int _size)
    {
        charaterNameText.text = _name;
        dialogueLineText.text = _line;
        dialogueLineText.fontSize = _size;

        ToggleDialogueBox(true);
    }
}

TimeLine的关键帧

点击图钉按钮打开Markers轨道,右键点击添加关键帧。
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_右键_20
关键帧的inspector截图如下(附对应解释):
unity timeline播放完怎么停留在最后一帧而不是回到第一帧 unity timeline在哪_ide_21