Featured image of post 麦田物语开发日记(十四)

麦田物语开发日记(十四)

利用Timeline完成游戏动画制作

TimeLine开场动画

创建TimeLine

  • 打开Timeline面板

在菜单栏点击Window->Sequnence->Timeline

  • 为某一个GO添加Timeline组件

选中目标的GO,然后在Timeline窗口新建一个Timeline即可对选中的GO创建Timeline组件

  • 制作动画

将动画Canvas拖入Timeline栏并设置为Animation Track,使用Timeline窗口的录制功能即可实现动画录制

一个技巧:先在靠后位置完成动画的结尾,保存当前场景所有内容的位置,颜色,打开状态、大小等,然后从前往后以此制作动画过程。

为了保证动画在规定时间执行,因此可以将存放动画的Panel放入Timeline轴中,然后为期设置在动画节点才能启动

image-20230301181833490

创建Timeline对话

在出场动画结束后,执行开启强制执行的对话,因此必须将该部分也放在Timeline中,为了控制该部分内容,需要通过代码来控制Timeline

由于Timeline本身没有对话相关的轨道和片段,因此需要编写相应的脚本来创建对应的动画片段。相应的就需要设计轨道脚本DialogueTrack用于让Unity得知有自定义的轨道;轨道中的动画片段脚本DialogueClip,用于能够在轨道中创建动画片段,以及最主要的动画片段内容DialogueBehaviour,用于具体设计该动画需要呈现的内容。

新建代码脚本DialogueBehaviour.cs,注意该类不是MonoBehaviour,而是PlayableBehaviour

image-20230301194128214

该脚本能够控制我们整个对话播放过程的执行,暂停等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class DialogueBehaviour : PlayableBehaviour
{
    private PlayableDirector _director;
    public DialoguePiece dialoguePiece;

    public override void OnPlayableCreate(Playable playable)
    {
        // 通过当前的播放的graph方向得到PlayableDirector
        _director = (playable.GetGraph().GetResolver() as PlayableDirector);
    }
}

然后创建一个用于控制我们的每一条轨道的内容的代码DialogueClip,其需要继承两个特殊的接口PlayableAsset, ITimelineClipAsset

其中PlayableAsset用于获得实例化的Playable的资源,也就是每一条轨道上的内容

ITimelineClipAsset是播放剪辑功能必备的组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class DialogueClip : PlayableAsset, ITimelineClipAsset
{
    public DialogueBehaviour dialogueTemplate = new DialogueBehaviour();
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        /* 每次新建,都以dialogueTemplate为模板在graph中新建可以编辑的片段 */
        var playable = ScriptPlayable<DialogueBehaviour>.Create(graph, dialogueTemplate);
        return playable;
    }

    public ClipCaps clipCaps => ClipCaps.None;
}

创建TimelineDialogue轨道,在该代码中,只需要让系统知道,你在该轨道中放的片段类型是什么,不需要实际的代码

1
2
3
[TrackClipType(typeof(DialogueClip))]
public class DialogueTrack : TrackAsset
{}

从而得到以下内容

image-20230301200900697

image-20230301200908048

然后为了显示具体的对话内容,需要对DialogueBehaviour中的部分方法进行重写,来执行该动画片段的具体内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DialogueBehaviour : PlayableBehaviour
{
	//....
    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        // 呼叫启动对话UI
        EventHandler.CallShowDialogueEvent(dialoguePiece);
        if (Application.isPlaying)
        {
            if (dialoguePiece.hasToPause)
            {
                // 暂停Timeline,等待按下空格
                TimelineManager.Instance.PauseTimeline(_director);
            }
            else
            {
                // 关闭当前dialogue
                EventHandler.CallShowDialogueEvent(null);
            }
            
        }
    }
}

由于设计对话的时候使用了 通过空格键在继续对话内容,因此为了实现对话持续需要继续增加按键播放。

而等待空格键的过程中,Timeline必须停止,而且不能使用Timeline自带的暂停功能,其会导致暂停结束后快速播放后续内容,因此需要通过一个Timeline的控制器来控制其暂停。

而控制暂停的方式就是让TimeLine的时间播放速度为0,当检测到空格则恢复播放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TimelineManager : Singleton<TimelineManager>
{
    public PlayableDirector startDirector;  /* 游戏开局的timeline */
    private PlayableDirector _currentDirector;   /* 当前正在播放的director */
    private bool _isPause;

    protected override void Awake()
    {
        base.Awake();
        _currentDirector = startDirector;
    }

    private void Update()
    {
        if (_isPause && Input.GetKeyDown(KeyCode.Space))
        {
            _currentDirector.playableGraph.GetRootPlayable(0).SetSpeed(1d);
        }
    }

    public void PauseTimeline(PlayableDirector director)
    {
        _currentDirector = director;
        /* 获得当前director中graph中的根节点, 并将其速度设置为0 */
        _currentDirector.playableGraph.GetRootPlayable(0).SetSpeed(0d);
        _isPause = true;
    }
}

控制Timeline启动与暂停

由于对话动画的播放并不是及时播放完成,因此上节代码按空格直接开启timeline的播放有误,本节将通过从Timeline自带的方法中获得对话是否播放完成的标志_isDone,通过该标志保证只有当对话动画播放完成才能启动timeline

设置标志

1
2
3
4
5
6
// timelineManager.cs
private bool _isDone;
public bool IsDone  /* 为_isDone赋值 */
{
    set => _isDone = value;
}

timeline播放时逐帧执行dialogpiece播放完成的赋值

1
2
3
4
5
6
7
8
9
// DialogueBehaviou.cs
/* 在timeline播放过程中逐帧执行 */
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
    if (Application.isPlaying)
    {
        TimelineManager.Instance.IsDone = dialoguePiece.isDone;
    }
}

保证对话框的关闭

由于OnBehaviourPlay方法只能在timeline执行的时候才能启动,因此如果timeline结束前有对话框,那么关闭对话框的代码就不能执行,因此需要在timeline结束以后也执行关闭对话框

1
2
3
4
5
6
// DialogueBehaviou.cs
/* 如果timeline最后是对话框,则强制关闭对话框 */
public override void OnBehaviourPause(Playable playable, FrameData info)
{
    EventHandler.CallShowDialogueEvent(null);
}

保证timeline开启的时候,游戏时间不会流逝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// DialogueBehaviou.cs
/// <summary>
///  当当前对话开启的时候,游戏时间暂停
/// </summary>
/// <param name="playable"></param>
public override void OnGraphStart(Playable playable)
{
    EventHandler.CallUpdateGameStateEvent(GameState.Pause);
}

public override void OnGraphStop(Playable playable)
{
    EventHandler.CallUpdateGameStateEvent(GameState.Gameplay);
}

修改时间流逝的内容

1
2
3
4
5
// timeManager.cs
private void OnUpdateGameStateEvent(GameState state)
{
    _gameClockPause = state == GameState.Gameplay;
}

获取当前场景内容

1
2
3
4
5
6
7
// TimelineManager.cs
private void OnAfterSceneLoadEvent()
{
    _currentDirector = FindObjectOfType<PlayableDirector>();
    if (_currentDirector != null)
        _currentDirector.Play();
}
Built with Hugo
Theme Stack designed by Jimmy