155 lines
6.9 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|