背景
游戏对象录制好动画后,再次拖回到首帧重新录制首帧动画,此时游戏对象的实际姿态跟 Animator 播放时首帧动画不一致
可能出现闪现现象,同时在关闭 Animation 录制面板的 "预览/Preview" 时,游戏对象的姿态回恢复到与动画首帧姿态不一样(姿态=Position+Rotation)的状态;
我亟需将这个首帧的姿态同步给游戏对象,确保游戏对象默认状态和首帧状态是一致的。
实现
可以通过手动将每个游戏对象的姿态从Animation面板拷贝过去,但是这样非常耗时间,使用下面代码实现将指定帧姿态应用到游戏对象上:
using System.Collections.Generic;
using UnityEngine;
namespace Bian
{
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.Animations;
[CustomEditor(typeof(AnimationClipFrameSyncer))]
public class AnimationClipFrameSyncerEditor : Editor
{
GUIContent syncButtonContent = new GUIContent("", "将动画状态机中指定帧的姿态同步到物体上");
class StateInfo
{
public string stateName;
}
readonly List<StateInfo> stateInfos = new List<StateInfo>();
string[] stateNames = System.Array.Empty<string>();
int selectedIndex = -1;
public override void OnInspectorGUI()
{
serializedObject.Update();
var myScript = (AnimationClipFrameSyncer)target;
// 收集 Animator 的 State 列表
FetchStateInfos(myScript.animator);
// 为 fullStateName 提供下拉
var propFullStateName = serializedObject.FindProperty("fullStateName");
stateNames = new string[stateInfos.Count];
selectedIndex = -1;
for (int i = 0; i < stateInfos.Count; i++)
{
stateNames[i] = stateInfos[i].stateName;
if (myScript.fullStateName == stateNames[i]) selectedIndex = i;
}
bool isMissing = false;
if (!string.IsNullOrEmpty(myScript.fullStateName) && selectedIndex == -1 && stateInfos.Count > 0)
{
isMissing = true;
var tempList = new List<string>(stateNames);
tempList.Insert(0, myScript.fullStateName);
stateNames = tempList.ToArray();
selectedIndex = 0;
}
// 绘制除 fullStateName 以外的字段
var iterator = serializedObject.GetIterator();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren))
{
enterChildren = false;
using var disabledScopeScr = new EditorGUI.DisabledScope(iterator.name == "m_Script");
if (iterator.name == "fullStateName") continue;
EditorGUILayout.PropertyField(iterator, true);
}
// FullStateName 下拉或提示
if (stateInfos.Count > 0 || isMissing)
{
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup("FullStateName", selectedIndex, stateNames);
if (EditorGUI.EndChangeCheck() && newIndex >= 0 && newIndex < stateNames.Length)
{
propFullStateName.stringValue = stateNames[newIndex];
}
if (isMissing)
{
EditorGUILayout.HelpBox($"StateMissing: {myScript.fullStateName}", MessageType.Error);
}
}
else
{
EditorGUILayout.HelpBox("没有找到 AnimatorStates。", MessageType.Warning);
}
serializedObject.ApplyModifiedProperties();
// 同步按钮
EditorGUILayout.Space();
syncButtonContent.text = $"同步姿态 (Frame: {myScript.frame})";
if (GUILayout.Button(syncButtonContent))
{
TrySyncPose(myScript);
}
}
void FetchStateInfos(Animator animator)
{
stateInfos.Clear();
if (animator == null || animator.runtimeAnimatorController == null) return;
var controller = GetAnimatorController(animator);
if (controller == null) return;
for (int i = 0; i < controller.layers.Length; i++)
{
var layer = controller.layers[i];
var layerName = layer.name;
var states = layer.stateMachine.states;
foreach (var item in states)
{
var state = item.state;
if (state == null) continue;
stateInfos.Add(new StateInfo { stateName = string.Format("{0}.{1}", layerName, state.name) });
}
}
}
void TrySyncPose(AnimationClipFrameSyncer syncer)
{
if (syncer == null || syncer.animator == null)
{
Debug.LogError("AnimationClipFrameSyncer: 请先指定 Animator");
return;
}
if (string.IsNullOrEmpty(syncer.fullStateName))
{
Debug.LogError("AnimationClipFrameSyncer: 请填写 fullStateName (格式: Layer.State)");
return;
}
// 解析 Layer.State
string layerName, stateName;
if (!ParseFullStateName(syncer.fullStateName, out layerName, out stateName))
{
Debug.LogError("AnimationClipFrameSyncer: fullStateName 格式错误,应为 Layer.State");
return;
}
var controller = GetAnimatorController(syncer.animator);
if (controller == null)
{
Debug.LogError("AnimationClipFrameSyncer: 无法获取 AnimatorController");
return;
}
// 查找 Layer
int layerIndex = -1;
AnimatorStateMachine stateMachine = null;
for (int i = 0; i < controller.layers.Length; i++)
{
if (controller.layers[i].name == layerName)
{
layerIndex = i;
stateMachine = controller.layers[i].stateMachine;
break;
}
}
if (layerIndex < 0 || stateMachine == null)
{
Debug.LogError($"AnimationClipFrameSyncer: 未找到 Layer: {layerName}");
return;
}
// 查找 State
AnimatorState targetState = FindStateRecursive(stateMachine, stateName);
if (targetState == null)
{
Debug.LogError($"AnimationClipFrameSyncer: 未找到状态: {stateName}");
return;
}
// 解析 Motion 到 AnimationClip(支持 BlendTree)
var clip = ResolveToClip(targetState.motion);
if (clip == null)
{
Debug.LogError("AnimationClipFrameSyncer: 目标状态未关联 AnimationClip(或不支持的 Motion 类型)");
return;
}
// 将指定帧数转换为时间
float fps = Mathf.Max(1e-3f, clip.frameRate);
float time = Mathf.Clamp(syncer.frame / fps, 0f, clip.length);
// 准备采样 Root(使用 Animator 挂载对象作为根)
var rootGO = syncer.animator.gameObject;
var allTransforms = rootGO.GetComponentsInChildren<Transform>(true);
// 预览采样,捕获整棵层级的局部位姿
var localPositions = new Vector3[allTransforms.Length];
var localRotations = new Quaternion[allTransforms.Length];
AnimationMode.StartAnimationMode();
try
{
AnimationMode.SampleAnimationClip(rootGO, clip, time);
for (int i = 0; i < allTransforms.Length; i++)
{
var t = allTransforms[i];
localPositions[i] = t.localPosition;
localRotations[i] = t.localRotation;
}
}
finally
{
AnimationMode.StopAnimationMode();
}
// 应用到整棵层级(仅同步 Transform 的本地位置与旋转)
Undo.RecordObjects(allTransforms, "Sync Hierarchy Pose From Animation");
for (int i = 0; i < allTransforms.Length; i++)
{
allTransforms[i].localPosition = localPositions[i];
allTransforms[i].localRotation = localRotations[i];
EditorUtility.SetDirty(allTransforms[i]);
}
Debug.Log($"AnimationClipFrameSyncer: 已从 {syncer.fullStateName} 的第 {syncer.frame} 帧同步整层级姿态({allTransforms.Length} 个 Transform)");
}
static bool ParseFullStateName(string full, out string layer, out string state)
{
layer = null;
state = null;
if (string.IsNullOrEmpty(full)) return false;
var parts = full.Split('.');
if (parts.Length != 2) return false;
layer = parts[0];
state = parts[1];
return !string.IsNullOrEmpty(layer) && !string.IsNullOrEmpty(state);
}
static AnimatorController GetAnimatorController(Animator animator)
{
if (animator == null) return null;
var rac = animator.runtimeAnimatorController;
if (rac == null) return null;
// 处理 OverrideController
var aoc = rac as AnimatorOverrideController;
if (aoc != null)
{
return aoc.runtimeAnimatorController as AnimatorController;
}
return rac as AnimatorController;
}
static AnimatorState FindStateRecursive(AnimatorStateMachine sm, string stateName)
{
if (sm == null) return null;
foreach (var cs in sm.states)
{
if (cs.state != null && cs.state.name == stateName)
return cs.state;
}
foreach (var child in sm.stateMachines)
{
var found = FindStateRecursive(child.stateMachine, stateName);
if (found != null) return found;
}
return null;
}
static AnimationClip ResolveToClip(Motion motion)
{
if (motion == null) return null;
var clip = motion as AnimationClip;
if (clip != null) return clip;
var tree = motion as BlendTree;
if (tree != null)
{
// 选择第一个子节点的 Clip(简单策略)
if (tree.children != null && tree.children.Length > 0)
{
return ResolveToClip(tree.children[0].motion);
}
}
return null;
}
}
#endif
/// <summary>
/// 用于将动画状态机中的指定的帧Property获取,如果是 Position 就同步改位置到物体,如果是 Rotation 就同步改旋转到物体。
/// 多用于解决物体初始状态与动画机初始帧不一致的问题。
/// 这里扩展成可以指定帧数的功能。
/// </summary>
public class AnimationClipFrameSyncer : MonoBehaviour
{
public int frame = 0;
public Animator animator;
public string fullStateName;
#if UNITY_EDITOR
private void Reset()
{
animator = GetComponent<Animator>();
}
#endif
}
}
使用方式
- 将脚本内容拷贝到工程,命名为:AnimationClipFrameSyncer.cs
- 挂载在有 Animator 的脚本上
- 选择指定的状态机 State
- 指定帧为 0
-
点击按钮“同步姿态 (Frame: 0)” 即可完成首帧数据应用到游戏对象
Tips: 可以指定任意帧实现将任意帧的数据应用到游戏对象!
写到最后
如有更优解,欢迎讨论!
版权所有,转载请注明出处~
