UnityVATTool/Assets/Plugins/AnimMapBaker/Scripts/VATAnimationController.cs

155 lines
6.9 KiB
C#

using System;
using UnityEngine;
namespace OctoberStudio.VAT
{
/// <summary>
/// 用 MaterialPropertyBlock 控制 VAT Atlas 上的当前 clip 与播放时间偏移。
/// 不切换 material → 不破坏 SRP Batcher / GPU Instancing,适合大量同种角色同屏。
///
/// 使用方式:
/// 1. 挂在含 MeshRenderer 的 GameObject 上 (或子物体 View 上,把 meshRenderer 指过来)
/// 2. 拖入 clipDatabase
/// 3. 调 Play("Idle" / "Run" / "Die")
/// </summary>
[DisallowMultipleComponent]
public class VATAnimationController : MonoBehaviour
{
private static readonly int AnimYStartID = Shader.PropertyToID("_AnimYStart");
private static readonly int AnimYSpanID = Shader.PropertyToID("_AnimYSpan");
private static readonly int AnimLenID = Shader.PropertyToID("_AnimLen");
private static readonly int AnimTimeOffsetID = Shader.PropertyToID("_AnimTimeOffset");
private static readonly int AnimLoopID = Shader.PropertyToID("_AnimLoop");
[Tooltip("要控制的 MeshRenderer。留空则自动在自身/子物体获取。")]
public MeshRenderer meshRenderer;
[Tooltip("烘焙时生成的 VAT 元数据资产。")]
public VATClipDatabase clipDatabase;
[Tooltip("启动时自动播放的 clip 名(找不到则不播)。Play 时如果找不到 'Idle' 会自动 fallback 到 'Ready'。")]
public string defaultClip = "Idle";
[Tooltip("把 Play(\"Idle\") 重定向到此 clip 名。空=不重定向。例:模型 Idle 不好看,设为 \"Ready\" 让站立时播 Ready。")]
public string idleClipOverride = "";
[Header("Facing (左右朝向)")]
[Tooltip("用来旋转的 transform (通常就是 VAT root 自身)。SetFacing(±1) 会改写它的 localEulerAngles.y。")]
public Transform facingTransform;
[Tooltip("朝右 (sign>=0) 时 facingTransform.localEulerAngles.y 的目标值。对称 -15 让走右时人物也微微侧身。")]
public float facingRightY = -15f;
[Tooltip("朝左 (sign<0) 时 facingTransform.localEulerAngles.y 的目标值。15 = 微旋。180 = 完全镜像(突兀)。")]
public float facingLeftY = 15f;
// Idle 找不到时尝试播这些 clip(按顺序)
private static readonly string[] IdleFallbacks = { "Ready", "Stand", "Wait" };
private MaterialPropertyBlock _mpb;
private VATClipEntry _current;
private bool _hasCurrent;
// 0 = 未初始化(强制下一次 SetFacing 一定执行,即使 sign 跟默认 +1 重合)
private int _currentFacingSign = 0;
/// <summary>当前播放的 clip 名(可能为空字符串)</summary>
public string CurrentClip => _hasCurrent ? _current.name : string.Empty;
/// <summary>当前左右朝向(+1 / -1)</summary>
public int CurrentFacingSign => _currentFacingSign;
protected virtual void Awake()
{
if (meshRenderer == null) meshRenderer = GetComponentInChildren<MeshRenderer>(true);
if (facingTransform == null) facingTransform = transform;
_mpb = new MaterialPropertyBlock();
}
protected virtual void OnEnable()
{
// 启动时强制把朝向应用到 facingTransform (避免初始 Y=0,走右后仍 Y=0 看起来"右边没旋转"的 bug)
SetFacing(1);
if (!string.IsNullOrEmpty(defaultClip)) Play(defaultClip);
}
/// <summary>
/// 切到指定 clip。如果 name 不存在,会按 fallback 链尝试(Idle→Ready→Stand→Wait)。
/// </summary>
public bool Play(string clipName, bool loop = true)
{
if (meshRenderer == null || clipDatabase == null) return false;
// Idle 重定向: 允许把 Play("Idle") 实际播放 idleClipOverride 指向的 clip
if (!string.IsNullOrEmpty(idleClipOverride) && string.Equals(clipName, "Idle", StringComparison.OrdinalIgnoreCase))
{
clipName = idleClipOverride;
}
// 同一个 clip 且循环标记一致 → 不重置时间,避免 SetSpeed 每帧重置导致动画卡在第 0 帧
if (_hasCurrent && string.Equals(_current.name, clipName, StringComparison.OrdinalIgnoreCase) && _current.loop == loop)
return true;
if (!ResolveClip(clipName, out var entry))
{
Debug.LogWarning($"[VATAnimationController] Clip '{clipName}' not found in {clipDatabase.name}");
return false;
}
entry.loop = loop;
_current = entry;
_hasCurrent = true;
// 用 propertyBlock,不修改 sharedMaterial。
meshRenderer.GetPropertyBlock(_mpb);
_mpb.SetFloat(AnimYStartID, entry.yStart);
_mpb.SetFloat(AnimYSpanID, entry.ySpan);
_mpb.SetFloat(AnimLenID, entry.length);
_mpb.SetFloat(AnimTimeOffsetID, Time.timeSinceLevelLoad); // 从"现在"开始播
_mpb.SetFloat(AnimLoopID, loop ? 1f : 0f);
meshRenderer.SetPropertyBlock(_mpb);
return true;
}
/// <summary>
/// 查找 clip,Idle 找不到时按 fallback 链 (Ready/Stand/Wait) 继续找。
/// </summary>
private bool ResolveClip(string clipName, out VATClipEntry entry)
{
if (clipDatabase.TryFind(clipName, out entry)) return true;
// 只有请求 Idle 时才走 fallback,其他 clip 找不到就老实报错
if (string.Equals(clipName, "Idle", StringComparison.OrdinalIgnoreCase))
{
foreach (var alt in IdleFallbacks)
{
if (clipDatabase.TryFind(alt, out entry)) return true;
}
}
return false;
}
/// <summary>
/// 设置左右朝向。sign >= 0 → facingRightY,sign < 0 → facingLeftY。
/// 同一朝向重复调用是 no-op。
/// </summary>
public void SetFacing(int sign)
{
int s = sign >= 0 ? 1 : -1;
// _currentFacingSign == 0 表示尚未初始化,必须执行一次(让初始 facingTransform.Y 从 0 切到 facingRightY)
if (s == _currentFacingSign && _currentFacingSign != 0) return;
_currentFacingSign = s;
if (facingTransform == null) return;
var e = facingTransform.localEulerAngles;
e.y = s > 0 ? facingRightY : facingLeftY;
facingTransform.localEulerAngles = e;
}
/// <summary>停止 (实际是 freeze 在最后一帧)。</summary>
public void Stop()
{
if (meshRenderer == null) return;
meshRenderer.GetPropertyBlock(_mpb);
_mpb.SetFloat(AnimLoopID, 0f);
// 让 t 趋于 >=1: timeOffset 设很久以前即可
_mpb.SetFloat(AnimTimeOffsetID, -1e6f);
meshRenderer.SetPropertyBlock(_mpb);
}
}
}