using System; using UnityEngine; namespace OctoberStudio.VAT { /// /// 用 MaterialPropertyBlock 控制 VAT Atlas 上的当前 clip 与播放时间偏移。 /// 不切换 material → 不破坏 SRP Batcher / GPU Instancing,适合大量同种角色同屏。 /// /// 使用方式: /// 1. 挂在含 MeshRenderer 的 GameObject 上 (或子物体 View 上,把 meshRenderer 指过来) /// 2. 拖入 clipDatabase /// 3. 调 Play("Idle" / "Run" / "Die") /// [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; /// 当前播放的 clip 名(可能为空字符串) public string CurrentClip => _hasCurrent ? _current.name : string.Empty; /// 当前左右朝向(+1 / -1) public int CurrentFacingSign => _currentFacingSign; protected virtual void Awake() { if (meshRenderer == null) meshRenderer = GetComponentInChildren(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); } /// /// 切到指定 clip。如果 name 不存在,会按 fallback 链尝试(Idle→Ready→Stand→Wait)。 /// 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; } /// /// 查找 clip,Idle 找不到时按 fallback 链 (Ready/Stand/Wait) 继续找。 /// 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; } /// /// 设置左右朝向。sign >= 0 → facingRightY,sign < 0 → facingLeftY。 /// 同一朝向重复调用是 no-op。 /// 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; } /// 停止 (实际是 freeze 在最后一帧)。 public void Stop() { if (meshRenderer == null) return; meshRenderer.GetPropertyBlock(_mpb); _mpb.SetFloat(AnimLoopID, 0f); // 让 t 趋于 >=1: timeOffset 设很久以前即可 _mpb.SetFloat(AnimTimeOffsetID, -1e6f); meshRenderer.SetPropertyBlock(_mpb); } } }