260 lines
9.1 KiB
C#
260 lines
9.1 KiB
C#
using Animancer;
|
|
using BrewMonster;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using static CECModel;
|
|
|
|
public class NPCVisual : MonoBehaviour
|
|
{
|
|
[SerializeField] NamedAnimancerComponent namedAnimancer;
|
|
protected CECNPC.INFO m_NPCInfo;
|
|
[SerializeField] private Queue<string> _animationQueue = new Queue<string>();
|
|
[SerializeField] private AnimancerState _currentState;
|
|
private bool debugNamePlateBounds = false;
|
|
private const float fadeTime = .2f;
|
|
private const FadeMode FadeMode = Animancer.FadeMode.FixedDuration;
|
|
|
|
public CECNPC.INFO GetNPCINFO => m_NPCInfo;
|
|
|
|
public bool TryPlayAction(string animationName, CECAttackEvent cECAttackEvent, bool isHit = false, bool bRestart = true)
|
|
{
|
|
|
|
if (namedAnimancer == null) return false;
|
|
|
|
if (namedAnimancer.IsPlaying(animationName)) return false;
|
|
_currentState = namedAnimancer.TryPlay(animationName, fadeTime);
|
|
if (isHit)
|
|
{
|
|
if(_currentState != null)
|
|
{
|
|
_currentState.Events.OnEnd = () => SetHitOnEnd(cECAttackEvent);
|
|
}
|
|
}
|
|
return _currentState != null;
|
|
}
|
|
private void SetHitOnEnd(CECAttackEvent cECAttackEvent)
|
|
{
|
|
cECAttackEvent.m_bSignaled = true;
|
|
}
|
|
public void InitNPCEventDoneHandler(CECNPC.INFO iNFO)
|
|
{
|
|
m_NPCInfo = iNFO;
|
|
namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>();
|
|
if (namedAnimancer == null)
|
|
{
|
|
BMLogger.LogWarning("animancer == null");
|
|
return;
|
|
}
|
|
namedAnimancer.Animator.cullingMode = AnimatorCullingMode.CullUpdateTransforms;
|
|
EventBus.SubscribeChannel<QueueNPCActionEvent>(m_NPCInfo.nid, OnQueueAction);
|
|
EventBus.SubscribeChannel<ClearComActFlagEvent>(m_NPCInfo.nid, OnClearComActFlagEvent);
|
|
}
|
|
private void OnClearComActFlagEvent(ClearComActFlagEvent @event)
|
|
{
|
|
_animationQueue.Clear();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
PlayNext();
|
|
}
|
|
private void OnQueueAction(QueueNPCActionEvent @event)
|
|
{
|
|
if (!EnqueueAnimation(@event))
|
|
{
|
|
BMLogger.LogError("HoangDev : EnqueueAnimation Failed");
|
|
}
|
|
}
|
|
public bool EnqueueAnimation(QueueNPCActionEvent @event)
|
|
{
|
|
if (namedAnimancer == null) return false;
|
|
_animationQueue.Enqueue(@event.AnimationName);
|
|
|
|
return true;
|
|
}
|
|
public string animName1;
|
|
private void PlayNext()
|
|
{
|
|
if (_animationQueue.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_currentState == null) return;
|
|
animName1 = _animationQueue.Peek();
|
|
if (_currentState.NormalizedTime < 1f) return;
|
|
string animName = _animationQueue.Dequeue();
|
|
_currentState = namedAnimancer.TryPlay(animName);
|
|
}
|
|
private void OnDestroy()
|
|
{
|
|
EventBus.UnsubscribeAllInChannel(m_NPCInfo.nid);
|
|
}
|
|
public bool IsAnimationExist(string animationName)
|
|
{
|
|
if (namedAnimancer == null) return false;
|
|
return namedAnimancer.States.TryGet(animationName, out var existingState) ? true : false;
|
|
}
|
|
public bool IsPlayAnimation()
|
|
{
|
|
if (namedAnimancer == null) return false;
|
|
return namedAnimancer.IsPlaying();
|
|
}
|
|
public bool IsPlayAnimation(string animationName)
|
|
{
|
|
if (namedAnimancer == null) return false;
|
|
return namedAnimancer.IsPlaying(animationName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refresh the namedAnimancer reference when the model changes (e.g., shape change)
|
|
/// 当模型更改时(例如形状更改)刷新namedAnimancer引用
|
|
/// </summary>
|
|
/// <param name="modelRoot">The root GameObject of the model to search for NamedAnimancerComponent / 要搜索NamedAnimancerComponent的模型根GameObject</param>
|
|
public void RefreshNamedAnimancer(GameObject modelRoot = null)
|
|
{
|
|
if (modelRoot != null)
|
|
{
|
|
// Search specifically within the model GameObject's hierarchy
|
|
// 在模型GameObject的层次结构中搜索
|
|
namedAnimancer = modelRoot.GetComponentInChildren<NamedAnimancerComponent>();
|
|
}
|
|
else
|
|
{
|
|
// Fallback to searching from this component's hierarchy
|
|
// 回退到从此组件的层次结构搜索
|
|
namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>();
|
|
}
|
|
|
|
if (namedAnimancer == null)
|
|
{
|
|
BMLogger.LogWarning($"NPCVisual: RefreshNamedAnimancer - namedAnimancer == null after refresh (modelRoot: {modelRoot?.name ?? "null"})");
|
|
}
|
|
else
|
|
{
|
|
BMLogger.LogMono(this, $"NPCVisual: RefreshNamedAnimancer - Successfully refreshed namedAnimancer from model: {modelRoot?.name ?? "default"}");
|
|
}
|
|
}
|
|
|
|
// Cached reference to the bone with the highest world-Y position at model-ready time.
|
|
// 模型就绪时世界Y最高的骨骼缓存引用。
|
|
private Transform _cachedTopBone;
|
|
|
|
/// <summary>
|
|
/// Scan all SkinnedMeshRenderer bones, find the one with the highest world-Y, and cache it.
|
|
/// Call once after the model and its first animation frame are ready (e.g. from UINPC coroutine).
|
|
/// 扫描所有SkinnedMeshRenderer骨骼,找到世界Y最高者并缓存。
|
|
/// 在模型及首帧动画就绪后调用一次(例如从UINPC协程中调用)。
|
|
/// </summary>
|
|
public void CacheTopBone()
|
|
{
|
|
var smrs = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|
var seen = new HashSet<Transform>();
|
|
Transform highest = null;
|
|
float highestY = float.MinValue;
|
|
foreach (var smr in smrs)
|
|
{
|
|
if (smr == null || smr.bones == null)
|
|
continue;
|
|
foreach (var bone in smr.bones)
|
|
{
|
|
if (bone == null || !seen.Add(bone))
|
|
continue;
|
|
float y = bone.position.y;
|
|
if (y > highestY)
|
|
{
|
|
highestY = y;
|
|
highest = bone;
|
|
}
|
|
}
|
|
}
|
|
_cachedTopBone = highest;
|
|
if (debugNamePlateBounds && _cachedTopBone != null)
|
|
Debug.Log($"[Cuong] [NPCVisual] CacheTopBone: topBone={_cachedTopBone.name} worldY={highestY:F3}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return the world position of the cached top bone (set by <see cref="CacheTopBone"/>).
|
|
/// Returns false if no bone has been cached yet.
|
|
/// 返回缓存的最高骨骼世界坐标。若尚未缓存则返回false。
|
|
/// </summary>
|
|
public bool TryGetTopBoneWorld(out Vector3 worldPos)
|
|
{
|
|
if (_cachedTopBone == null)
|
|
{
|
|
worldPos = default;
|
|
return false;
|
|
}
|
|
worldPos = _cachedTopBone.position;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve world anchor from merged bounds of all SkinnedMeshRenderers.
|
|
/// 从所有SkinnedMeshRenderer合并后的包围盒解析世界锚点。
|
|
/// </summary>
|
|
public bool TryGetNamePlateAnchorWorld(out Vector3 worldPos)
|
|
{
|
|
worldPos = default;
|
|
|
|
var skinnedMeshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
|
if (skinnedMeshRenderers == null || skinnedMeshRenderers.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Bounds combinedBounds = default;
|
|
bool hasAnySkinnedMesh = false;
|
|
for (int i = 0; i < skinnedMeshRenderers.Length; i++)
|
|
{
|
|
var renderer = skinnedMeshRenderers[i];
|
|
if (renderer == null || renderer.sharedMesh == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var meshBounds = renderer.sharedMesh.bounds;
|
|
var scale = renderer.transform.lossyScale;
|
|
var worldCenter = renderer.transform.TransformPoint(meshBounds.center);
|
|
var worldSize = new Vector3(
|
|
Mathf.Abs(meshBounds.size.x * scale.x),
|
|
Mathf.Abs(meshBounds.size.y * scale.y),
|
|
Mathf.Abs(meshBounds.size.z * scale.z)
|
|
);
|
|
var currentBounds = new Bounds(worldCenter, worldSize);
|
|
if (debugNamePlateBounds)
|
|
{
|
|
BMLogger.Log($"[Cuong] [NPCVisual] smr={renderer.name} localCenter={meshBounds.center} localSize={meshBounds.size} worldCenter={worldCenter} worldSize={worldSize} worldMaxY={currentBounds.max.y}");
|
|
}
|
|
|
|
if (!hasAnySkinnedMesh)
|
|
{
|
|
combinedBounds = currentBounds;
|
|
hasAnySkinnedMesh = true;
|
|
}
|
|
else
|
|
{
|
|
combinedBounds.Encapsulate(currentBounds);
|
|
}
|
|
}
|
|
|
|
if (!hasAnySkinnedMesh)
|
|
{
|
|
if (debugNamePlateBounds)
|
|
{
|
|
Debug.LogWarning("[Cuong] [NPCVisual] TryGetNamePlateAnchorWorld: no valid SkinnedMeshRenderer/sharedMesh.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
worldPos = new Vector3(combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z);
|
|
if (debugNamePlateBounds)
|
|
{
|
|
Debug.Log($"[Cuong] [NPCVisual] combinedCenter={combinedBounds.center} combinedSize={combinedBounds.size} anchorWorld={worldPos}");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|