[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);
}

_layerIndex = _layerIndexProp.intValue;
_layerIndex = EditorGUILayout.Popup("Layer", _layerIndex, _layerNames.ToArray());
_layerIndexProp.intValue = _layerIndex;

// 添加 SubMachine AnimatorState
var stateMachine = _animatorController.layers[_layerIndex].stateMachine;
CollectStateNames(stateMachine);

if (_targetStateList == null)
{
InitTargetStateList();
}


_targetStateList.DoLayoutList();
}

if (EditorGUI.EndChangeCheck())
{
_target._stateParentLookup = _stateParentLookup;

serializedObject.ApplyModifiedProperties();
Debug.Log("Editor Change");
}
}

private void CollectStateNames(AnimatorStateMachine stateMachine)
{
_stateParentLookup = new MySerializedDictionary<int, int>();
var _name = stateMachine.name;
var hash = Animator.StringToHash(_name);

GetAllStateMachine(stateMachine, $"{_name}.", hash, "");
}

private void GetAllStateMachine(AnimatorStateMachine stateMachine, string hashPrefix, int parentHash,
string displayPrefix)
{
if (!stateMachine)
{
return;
}

GetAllState(stateMachine, hashPrefix, parentHash, displayPrefix);

foreach (var subStateMachine in stateMachine.stateMachines)
{
int hash = Animator.StringToHash($"{hashPrefix}{subStateMachine.stateMachine.name}");
string _name = subStateMachine.stateMachine.name;

AddState(hash, parentHash, _name);
GetAllStateMachine(subStateMachine.stateMachine, $"{hashPrefix}{_name}.", hash, $"{displayPrefix}{_name}.");
}
}

private void GetAllState(AnimatorStateMachine stateMachine, string hashPrefix, int parentHash, string displayPrefix)
{
foreach (var state in stateMachine.states)
{
        AddState(Animator.StringToHash($"{hashPrefix}{state.state.name}")
, parentHash
, $"{displayPrefix}{state.state.name}");
}
}

  private void AddState(int hash, int parentHash, string displayName)
{
    if (parentHash != 0)
{
      _stateParentLookup.Add(hash, parentHash);
}

    _stateNames.Add(displayName);
    _states.Add(hash);
}


  private void InitTargetStateList()
{
    _targetStateList = new ReorderableList(serializedObject,
    serializedObject.FindProperty("_targetStates"),
    true, true, true, true);

  float vSpace = 2;

  _targetStateList.drawHeaderCallback = (rect) => { EditorGUI.LabelField(rect, "State"); };

  _targetStateList.drawElementCallback = (rect, index, active, focused) =>
  {
rect.y += vSpace;
rect.height = EditorGUIUtility.singleLineHeight;
rect.width /= 2;

    var targetState = _targetStateList.serializedProperty.GetArrayElementAtIndex(index);
    var cur = GetStateHashIndex(targetState.intValue);
    int select = EditorGUI.Popup(rect, cur, _stateNames.ToArray());
targetState.intValue = _states[select];
};
}

  private int GetStateHashIndex(int hash)
{
    int index = -1;

    foreach (var state in _states)
{
index++;
      if (state == hash)
{
        return index;
}
}

    return 0;
}
}

 

using System.Collections.Generic;
using Manager;
using UnityEngine;

public enum PlayerState
{
Normal,
Combat,
}

public class PlayerBehavior : MonoBehaviour
{
public PlayerState _playerState = PlayerState.Normal;

public Animator _animator;

[HideInInspector] public int _layerIndex;

[HideInInspector] public MySerializedDictionary<int, int> _stateParentLookup;

public List<int> _targetStates;

private void OnEnable()
{
CameraManager.Instance.FollowPlayer(GlobalManager.Instance._pc.followTarget.transform);
}

private bool isChange = false;
private void Update()
{
isChange = false;

foreach (var targetState in _targetStates)
{
CheckState(targetState);
if(isChange) break;
}
}

private void CheckState(int state)
{
var s = _animator.GetCurrentAnimatorStateInfo(_layerIndex);

if (state != s.fullPathHash && _stateParentLookup != null)
{
int hash = s.fullPathHash;
while (hash != 0 && _stateParentLookup.ContainsKey(s.fullPathHash))
{
hash = _stateParentLookup.ContainsKey(hash) ? _stateParentLookup[hash] : 0;
if (state == hash)
{
_playerState = PlayerState.Combat;
isChange = true;
break;
}

_playerState = PlayerState.Normal;
}
}
}
}

 


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

[Serializable]
public class MySerializedDictionary<K, V> : Dictionary<K, V>, ISerializationCallbackReceiver
{
[SerializeField] List<K> _keys = new List<K>();

[SerializeField] List<V> _values = new List<V>();


public void OnBeforeSerialize()
{
_keys.Clear();
_values.Clear();

foreach (var kvp in this)
{
_keys.Add(kvp.Key);
_values.Add(kvp.Value);
}
}

public void OnAfterDeserialize()
{
for (int i = 0; i < _keys.Count; i++)
{
if (!ContainsKey(_keys[i]))
{
Add(_keys[i], _values[i]);
}
else
{
break;
}
}

_keys.Clear();
_values.Clear();
}
}
 

 最后改写一下 SerializedDictionary ,因为原来的存在报错,虽然没有影响但是看这很烦,使用 ISerializationCallbackReceiver 是实现 Dictionary 序列化的方式之一,个人认为很方便

 

 效果如上图,右侧的 Player State 会根据 Animator 的状态而改变

 

posted @ 2021-08-20 19:44  当麻  阅读(111)  评论(0编辑  收藏  举报