[Unity] 实现由 Animator 驱动的组件

[Unity] 实现由 Animator 驱动的组件

上图是 使用 CinemachineStateDrivenCamera 实现的视角变化,该组件是由 Animator 进行驱动的,在使用时,非常的方便,不用再写额外的代码

在使用了该组件之后,我也想使用 Animator 来改变角色的状态,于是乎,我开始参考 CinemachineStateDrivenCamera 来实现这样的功能

CinemachineStateDrivenCamera 的代码 分为两部分,CinemachineStateDrivenCamera 和 CinemachineStateDrivenCameraEditor

Editor 编程可以参考 Editor Scripting - Unity Learn

实现还挺复杂,我也不是很了解,在这里记录一下,希望对大家有所帮助,通过断点一步一步运行,也可以了解这部分代码是如何运作的

首先是处理 Editor 相关的代码,第一步是收集所有的 State / State Machine / Clip 并显示出来

[Unity] 实现由 Animator 驱动的组件

hash 是 Animator.StringToHash(name),name 为 StateMachine.name

[Unity] 实现由 Animator 驱动的组件

 hash 为 状态机前缀 和 AnimatorState.name 转换而来,等价于 fullPathHash

[Unity] 实现由 Animator 驱动的组件

 递归添加 StateMachine

[Unity] 实现由 Animator 驱动的组件

 mStateIndexLookup 是 Dictionary<int, int> 类型,主要作用是将 State / Clip / State Machine 生成的 Hash 形成了树状结构,mStateNames 是显示编辑器上的数据,mStates 是存储对应的 Hash,顺序与 mStateNames 一致

[Unity] 实现由 Animator 驱动的组件

 实现的效果如上图

 

接下来是 CinemachineStateDrivenCamera 的代码

 [Unity] 实现由 Animator 驱动的组件

 以上是主要的部分,用于对比的 hash 通过 GetClipHash 获得

[Unity] 实现由 Animator 驱动的组件

 获得的 hash 在这里进行比较,Instruction 是编辑器中设定好的数据

[Unity] 实现由 Animator 驱动的组件

参数

hash : 一般是 AnimatorStateInfo.fullPathHash

clips : 一般是 Animator 返回的 CurrentAnimatorClipInfo 或者 Next……

[Unity] 实现由 Animator 驱动的组件

 [Unity] 实现由 Animator 驱动的组件

在获取到 FakeHash 后还有一个关键的步骤,在之前获取所有 State 和 StateMachine 的过程中,已经将这两者的 Hash 组成树状结构,即除根节点,每个 Hash 都有自己的 ParentHash

[Unity] 实现由 Animator 驱动的组件

新生成的 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 UnityEngine;

public enum PlayerState
{
Normal,
Combat,
}

public class PlayerBehavior : MonoBehaviour
{

public PlayerState _playerState = PlayerState.Normal;

public Animator _animator;

[HideInInspector]
public int _layerIndex;

[HideInInspector]
public RuntimeAnimatorController _runtimeAnimatorController;

[HideInInspector]
public int _targetState;

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

private void OnEnable()
{
_runtimeAnimatorController = _animator.runtimeAnimatorController;

CameraManager.Instance.FollowPlayer(transform);
}

private void Update()
{
var s = _animator.GetCurrentAnimatorStateInfo(_layerIndex);
var c = _animator.GetNextAnimatorClipInfo(_layerIndex);

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

_playerState = PlayerState.Normal;
}
}
}

}

 

using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;

[CustomEditor(typeof(PlayerBehavior))]
public class PlayerBehaviorEditor : BaseEditor<PlayerBehavior>
{
private AnimatorController _animatorController;
private List<string> _layerNames = new List<string>();
private List<string> _stateNames = new List<string>();
private List<int> _states = new List<int>();
private int _stateIndex;
private Dictionary<int, int> _stateParentLookup = new Dictionary<int, int>();

public override void OnInspectorGUI()
{
base.OnInspectorGUI();

_target._runtimeAnimatorController = _target._animator.runtimeAnimatorController;
_animatorController = _target._runtimeAnimatorController as AnimatorController;

// 添加 Layer
for (int i = 0; i < _target._animator.layerCount; i++)
{
_layerNames.Add(_animatorController.layers[i].name);
}

_target._layerIndex = EditorGUILayout.Popup("Layer", 0, _layerNames.ToArray());

// 添加 SubMachine 和 AnimatorState
var stateMachine = _animatorController.layers[_target._layerIndex].stateMachine;
_stateNames.Clear();
_states.Clear();
_stateParentLookup.Clear();
CollectStateNames(stateMachine);

_target._stateParentLookup = _stateParentLookup;

_stateIndex = EditorGUILayout.Popup("State", _stateIndex, _stateNames.ToArray());
_target._targetState = _states[_stateIndex];
}

private void CollectStateNames(AnimatorStateMachine stateMachine)
{
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[hash] = parentHash;
}

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

 

[Unity] 实现由 Animator 驱动的组件

 

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

 

[Unity] 实现由 Animator 驱动的组件

上一篇:python内置模块


下一篇:Java23种设计模式