[Unity] 实现由 Animator 驱动的组件
上图是 使用 CinemachineStateDrivenCamera 实现的视角变化,该组件是由 Animator 进行驱动的,在使用时,非常的方便,不用再写额外的代码
在使用了该组件之后,我也想使用 Animator 来改变角色的状态,于是乎,我开始参考 CinemachineStateDrivenCamera 来实现这样的功能
CinemachineStateDrivenCamera 的代码 分为两部分,CinemachineStateDrivenCamera 和 CinemachineStateDrivenCameraEditor
Editor 编程可以参考 Editor Scripting - Unity Learn
实现还挺复杂,我也不是很了解,在这里记录一下,希望对大家有所帮助,通过断点一步一步运行,也可以了解这部分代码是如何运作的
首先是处理 Editor 相关的代码,第一步是收集所有的 State / State Machine / Clip 并显示出来
hash 是 Animator.StringToHash(name),name 为 StateMachine.name
hash 为 状态机前缀 和 AnimatorState.name 转换而来,等价于 fullPathHash
递归添加 StateMachine
mStateIndexLookup 是 Dictionary<int, int> 类型,主要作用是将 State / Clip / State Machine 生成的 Hash 形成了树状结构,mStateNames 是显示编辑器上的数据,mStates 是存储对应的 Hash,顺序与 mStateNames 一致
实现的效果如上图
接下来是 CinemachineStateDrivenCamera 的代码
以上是主要的部分,用于对比的 hash 通过 GetClipHash 获得
获得的 hash 在这里进行比较,Instruction 是编辑器中设定好的数据
参数
hash : 一般是 AnimatorStateInfo.fullPathHash
clips : 一般是 Animator 返回的 CurrentAnimatorClipInfo 或者 Next……
在获取到 FakeHash 后还有一个关键的步骤,在之前获取所有 State 和 StateMachine 的过程中,已经将这两者的 Hash 组成树状结构,即除根节点,每个 Hash 都有自己的 ParentHash
新生成的 FakeHash 是无法直接使用,还需要通过 mStateParentLookup 来寻找需要的 Hash
总结:
此方法的前提是:mStates 中保存的 Hash,即 name 在 Animator.StringToHash 转换后 与 AnimatorStateInfo.fullPathHash 是一致的
从实现的来说,就是将 State 或者 StateMachine 保存为 Hash 且存储结构为树形结构,在运行时将当前的 State 或 下一个 State 的 Hash (Cinemachine 中是用 FakeHash,但是会进行 ParentHash 的查找,直到有符合条件的 Hash 或返回默认,一般最后用于比较的 Hash 还是 fullPathHash) 用于比较,与设定好的 State / StateMahcine 的 Hash (在 Editor 保存的是 fullPathHash) 进行比较
此外,使用 FakeHash 的原因,是为了使 BlendTree 中的 AnimationClip 也能触发事件
StateMachine 和 BlendTree 中的 AnimationClip 都是没有自己的 Hash 的,但是可以通过树状结构将他们生成的 Hash 与其他 State 的 fullPathHash 产生联系
以下是我自己的实现,并不是完全根据 Cinemachine 实现的,因为不需要实现 BlendTree 内的 Clip 作为驱动事件
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEditorInternal;
using UnityEngine;
using AnimatorController = UnityEditor.Animations.AnimatorController;
[CustomEditor(typeof(PlayerBehavior))]
public class PlayerBehaviorEditor : BaseEditor<PlayerBehavior>
{
private AnimatorController _animatorController;
public List<string> _layerNames = new List<string>();
public List<string> _stateNames = new List<string>();
public List<int> _states = new List<int>();
public MySerializedDictionary<int, int> _stateParentLookup;
public int _stateIndex;
public int _layerIndex;
private Animator _animator;
private SerializedProperty _layerIndexProp;
private ReorderableList _targetStateList;
private void OnEnable()
{
_layerIndexProp = serializedObject.FindProperty("_layerIndex");
_targetStateList = null;
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
serializedObject.Update();
_layerNames.Clear();
_stateNames.Clear();
_states.Clear();
_animator = _target._animator;
EditorGUI.BeginChangeCheck();
if (_animator != null)
{
_animatorController = _target._animator.runtimeAnimatorController as AnimatorController;
}
if (_animatorController != null)
{
// 添加 Layer
for (int i = 0; i < _animatorController.layers.Length; i++)
{
_layerNames.Add(_animatorController.layers[i].name