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 _animationQueue = new Queue(); [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) { // BMLogger.LogMono(this, "HoangDev: TryPlayAction: " + animationName); if (namedAnimancer == null) return false; // BMLogger.LogMono(this, "HoangDev: namedAnimancer == null: " + animationName); if (namedAnimancer.IsPlaying(animationName)) return false; // BMLogger.LogMono(this, "HoangDev: namedAnimancerIsPlaying == null1: " + animationName); _currentState = namedAnimancer.TryPlay(animationName, fadeTime); if (isHit) { if(_currentState != null) { _currentState.Events.OnEnd = () => SetHitOnEnd(cECAttackEvent); } } // if (_currentState != null) // BMLogger.LogMono(this, "HoangDev: _currentState != null1: " + _currentState.Clip.name); return _currentState != null; } private void SetHitOnEnd(CECAttackEvent cECAttackEvent) { cECAttackEvent.m_bSignaled = true; } public void InitNPCEventDoneHandler(CECNPC.INFO iNFO) { m_NPCInfo = iNFO; namedAnimancer = GetComponentInChildren(); if (namedAnimancer == null) { BMLogger.LogWarning("animancer == null"); return; } namedAnimancer.Animator.cullingMode = AnimatorCullingMode.CullUpdateTransforms; EventBus.SubscribeChannel(m_NPCInfo.nid, OnQueueAction); EventBus.SubscribeChannel(m_NPCInfo.nid, OnClearComActFlagEvent); } private void OnClearComActFlagEvent(ClearComActFlagEvent @event) { // if (_currentState != null) // BMLogger.LogError("HoangDev: OnClearComActFlagEvent _currentState:" + _currentState.Clip.name); /*foreach (var state in _animationQueue) { BMLogger.LogMono(this,"HoangDev: OnClearComActFlagEvent state:" + state); } BMLogger.LogMono(this,"HoangDev: OnClearComActFlagEvent");*/ _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(); BMLogger.LogMono(this,"HoangDev: PlayNext: " + animName); _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); } /// /// Refresh the namedAnimancer reference when the model changes (e.g., shape change) /// 当模型更改时(例如形状更改)刷新namedAnimancer引用 /// /// The root GameObject of the model to search for NamedAnimancerComponent / 要搜索NamedAnimancerComponent的模型根GameObject public void RefreshNamedAnimancer(GameObject modelRoot = null) { if (modelRoot != null) { // Search specifically within the model GameObject's hierarchy // 在模型GameObject的层次结构中搜索 namedAnimancer = modelRoot.GetComponentInChildren(); } else { // Fallback to searching from this component's hierarchy // 回退到从此组件的层次结构搜索 namedAnimancer = GetComponentInChildren(); } 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; /// /// 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协程中调用)。 /// public void CacheTopBone() { var smrs = GetComponentsInChildren(true); var seen = new HashSet(); 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}"); } /// /// Return the world position of the cached top bone (set by ). /// Returns false if no bone has been cached yet. /// 返回缓存的最高骨骼世界坐标。若尚未缓存则返回false。 /// public bool TryGetTopBoneWorld(out Vector3 worldPos) { if (_cachedTopBone == null) { worldPos = default; return false; } worldPos = _cachedTopBone.position; return true; } /// /// Resolve world anchor from merged bounds of all SkinnedMeshRenderers. /// 从所有SkinnedMeshRenderer合并后的包围盒解析世界锚点。 /// public bool TryGetNamePlateAnchorWorld(out Vector3 worldPos) { worldPos = default; var skinnedMeshRenderers = GetComponentsInChildren(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; } }