你说得对,但是EmiteInnaACTSystemFramework是一款由EmiteInna自主研发,适用于PC平台的Unity第三人称上帝视角3D动作游戏代码框架。您将在游戏里扮演一个名叫游戏程序的角色,通过跳跃,冲刺和各种各样的丝滑小连招击败你的对手,修复程序中的bug,找回失散多年的代码能力,发掘未知的设计模式和模拟算法,揭开游戏行业无法入行、毕业即失业的真相。

本篇是ACT系列之一,github仓库位于:https://github.com/EmiteInna/EmiteInnaActSystem

简述

动画系统采用了Playable的方案,目前而言我似乎并没有把多个动画blend在一起的需求,所以用了一个简单的两个playableclip互相切的方案,同时做了个表来存clip的映射,一方面避免每次播放动画都要create结构体,一方面避免连续播放同一个动画的时候产生逻辑问题。

动画组件的代码

代码
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

namespace EmiteInnaACT
{
/// <summary>
/// 一个结构体,为了应付连续播放相同动画的情况。
/// </summary>
public struct AnimationClipStruct
{
public AnimationClipPlayable cp0;
public AnimationClipPlayable cp1;
public int now;
}
/// <summary>
/// 动画控制器,注意这个控件是挂在显示层上而不是根部。
/// 很简单的控制器,只能负责两个动画的切换和过渡。
/// </summary>
[RequireComponent(typeof(Animator))]
public class CharacterAnimationController:MonoBehaviour
{
public Dictionary<int,AnimationClipStruct> clipDict = new Dictionary<int, AnimationClipStruct>();
public CharacterControllerBase controller;
public Animator animator;
PlayableGraph graph;
AnimationClipPlayable clip1;
AnimationClipPlayable clip2;
AnimationMixerPlayable rootmixer;
bool isFirstPlay = true;
float precentWeight;
Coroutine swap;
/// <summary>
/// 调用Controller的相应函数
/// </summary>
/// <param name="str"></param>
public void OnApplyCharacterEvent(string str)
{
controller.OnApplyCharacterEvent(str);
}
/// <summary>
/// 动画机的初始化
/// </summary>
public void Initialize(CharacterControllerBase controller)
{
controller.animationController = this;
this.controller = controller;
animator = GetComponent<Animator>();
graph = PlayableGraph.Create("Player");
graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);
rootmixer = AnimationMixerPlayable.Create(graph, 2);
var playableOutput = AnimationPlayableOutput.Create(graph, "输出", animator);
playableOutput.SetSourcePlayable(rootmixer);
}
/// <summary>
/// 释放
/// </summary>
public void Destroy()
{
clipDict.Clear();
clipDict = null;
graph.Stop();
graph.Destroy();
}
/// <summary>
/// 获取ClipPlayble,没有则创建一个。
/// </summary>
/// <param name="clip"></param>
/// <returns></returns>
public AnimationClipPlayable GetClipPlayable(AnimationClip clip)
{
if(clipDict.TryGetValue(clip.GetInstanceID(),out AnimationClipStruct c))
{
//如果是同类就取copy
if (c.now==0)
{
// Debug.Log("我是0");
c.now = 1;
clipDict[clip.GetInstanceID()] = c;
return c.cp1;
}
// Debug.Log("我是1");
c.now = 0;
clipDict[clip.GetInstanceID()] = c;
return c.cp0;
}
else
{
AnimationClipPlayable cp = AnimationClipPlayable.Create(graph, clip);
AnimationClipPlayable cp1 = AnimationClipPlayable.Create(graph, clip);
AnimationClipStruct s = new AnimationClipStruct();
s.cp0 = cp;
s.cp1 = cp1;
s.now = 0;
clipDict.Add(clip.GetInstanceID(), s);
return cp;
}
}
/// <summary>
/// 滚动播放,主播放的永远是2
/// </summary>
/// <param name="clip">播放的clip</param>
/// <param name="transition">过渡时间</param>
public void PlayAnimation(AnimationClip clip,float transition=0f,float speed=1f)
{
// if(clip1.GetAnimationClip()!=null&&clip2.GetAnimationClip()!=null)
if (isFirstPlay)
{
precentWeight = 1;
clip2 = GetClipPlayable(clip);
graph.Connect(clip2, 0, rootmixer, 0);
rootmixer.SetInputWeight(0, 1f);
}
else
{

clip1 = clip2;
clip2 = GetClipPlayable(clip);
clip2.SetTime(0);
clip2.SetSpeed(speed);
graph.Disconnect(rootmixer, 0);
graph.Disconnect(rootmixer, 1);

graph.Connect(clip1, 0, rootmixer, 0);
graph.Connect(clip2, 0, rootmixer, 1);
if (swap != null) controller.StopCoroutine(swap);
swap = controller.StartCoroutine(DoPlayAnimation(transition));
}
isFirstPlay = false;
if (graph.IsPlaying() == false)
{
graph.Play();
}
}
IEnumerator DoPlayAnimation(float transition)
{
if (transition > 0)
{
float speed = Time.fixedDeltaTime / transition;
precentWeight = 1 - precentWeight;
while (precentWeight < 1)
{
precentWeight += speed;
rootmixer.SetInputWeight(0, 1 - precentWeight);
rootmixer.SetInputWeight(1, precentWeight);
yield return new WaitForFixedUpdate();
}
}
precentWeight = 1;
rootmixer.SetInputWeight(0, 0);
rootmixer.SetInputWeight(1, 1);
}
}
}

众所周知现在的配置里其实放了很多资源文件,而这些资源文件其实应该是由AB包管理的,不过现在还没有做AB包而已。

回头想了想打算把AB包管理和不AB包管理分为两个版本分别出一个框架的想法,后来想想果然最后有余力还是全改成资源管理比较好。

留个行为树的坑,打算后期的时候再管(做完3C和技能

最终还是用了rigidbody的物理方案。

踩了两个坑,一个是y有值的时候clamp速度一定要先转换为平面速度。

第二个是动画自己切到自己的时候会出问题。

加了跳跃的状态,细说一下昨天结尾的时候说的状态机的不足吧:

我们会发现动作游戏里玩家的几乎每一个动作都需要一个状态,这是好的,这并不是什么要不得的东西,我们总是希望跳跃时在上升、下降、着陆的过程中都能有自己喜欢的定制表现,因此这些重复是必要的。但是状态之间的转移却会产生代码的不必要重复,举个例子,就拿加了跳跃后的状态机,它是这样的。

图片

当然,我们也可以进一步封装,把连接本身也封装成一个状态,甚至搞点状态间的blend(还是算了吧),但是这个东西本身已经没有一开始看起来那么美好了。

这个时候我们就会想到分层状态机,很显然,Idle,walk和sprint这些东西到jumprising和jumplanding的转移是一样的,那么是否可以看作它们属于一个大节点,而这个大节点存在一个到jumprising和jumplanding的转移,实际上我最后也是这么做的。

如果这样写会发现越来越像行为树了不是吗?

当然,我还是很信任状态机的,状态机也有很多好处,他在维护角色操作这种实际上逻辑并没有那么复杂(相对其它怪物的AI逻辑)的东西上非常舒服,希望在加入技能系统之后它不会变得乱到无法辨识……