From 9a361247ef0b25fbd97ab801cc7606e9d6aaa5eb Mon Sep 17 00:00:00 2001 From: Tran Hai Nam Date: Wed, 27 May 2026 17:47:58 +0700 Subject: [PATCH] Add NPC animation behavior --- .../Editor/CombineActHolderFolderAssigner.cs | 2 +- Assets/PerfectWorld/Scene/AnimationTest.unity | 4 +- Assets/PerfectWorld/Scripts/NPC/CECModel.cs | 58 +++--- Assets/PerfectWorld/Scripts/NPC/CECNPC.cs | 169 +++++++++--------- .../Scripts/NPC/CECNPCModelDefaultPolicy.cs | 53 +++--- Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs | 135 ++++++++++---- 6 files changed, 254 insertions(+), 167 deletions(-) diff --git a/Assets/ModelRenderer/Editor/CombineActHolderFolderAssigner.cs b/Assets/ModelRenderer/Editor/CombineActHolderFolderAssigner.cs index b968bd727f..06b661f1f3 100644 --- a/Assets/ModelRenderer/Editor/CombineActHolderFolderAssigner.cs +++ b/Assets/ModelRenderer/Editor/CombineActHolderFolderAssigner.cs @@ -307,7 +307,7 @@ public class CombineActHolderFolderAssigner : EditorWindow if (AssetDatabase.GetMainAssetTypeAtPath(prefabPath) != typeof(GameObject)) return false; - GameObject prefabAsset = AssetDatabase.LoadMainAssetAtPath(prefabPath); + GameObject prefabAsset = (GameObject)AssetDatabase.LoadMainAssetAtPath(prefabPath); if (prefabAsset == null) return false; diff --git a/Assets/PerfectWorld/Scene/AnimationTest.unity b/Assets/PerfectWorld/Scene/AnimationTest.unity index c7afb1d85c..c5e9edb093 100644 --- a/Assets/PerfectWorld/Scene/AnimationTest.unity +++ b/Assets/PerfectWorld/Scene/AnimationTest.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6a8ecae27ef44213a7965bfcb0e1cfcf30a013734f9c6fd14095d6440259005 -size 145407 +oid sha256:ea74bc9aef8c1251035c339a2a463e2d026cc0af379759abf687dcf13c9bda99 +size 146707 diff --git a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs index 52f0bccff9..58a750dcf7 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs @@ -1271,43 +1271,55 @@ public class CECModel /// Hook Transform or null / 挂点变换,未找到返回null public Transform GetHook(string hookName, bool recursive = true) { - // Auto-initialize if not set (lazy initialization) - // 如果未设置则自动初始化(延迟初始化) - if (m_skeletonBuilder == null) - { - InitializeSkeletonBuilder(); - if (m_skeletonBuilder == null) - { - return null; // Still no skeleton found - } - } - if (string.IsNullOrEmpty(hookName)) - { return null; - } // Check cache first // 首先检查缓存 if (m_hookCache.TryGetValue(hookName, out Transform cachedHook)) { - if (cachedHook != null) // Unity "fake null" check - { + if (cachedHook != null) return cachedHook; - } - m_hookCache.Remove(hookName); // Remove invalid entry + m_hookCache.Remove(hookName); } - // Lookup from skeleton - // 从骨架查找 - Transform hook = m_skeletonBuilder.GetHook(hookName, recursive); + Transform hook = null; + + if (m_skeletonBuilder == null) + InitializeSkeletonBuilder(); + + if (m_skeletonBuilder != null) + { + hook = m_skeletonBuilder.GetHook(hookName, recursive); + } + else if (m_transform != null) + { + // [中文] NPC 无 SkeletonBuilder:按子节点名称查找挂点 + // [English] NPCs without SkeletonBuilder: resolve hooks by child transform name + hook = FindHookByChildName(m_transform, hookName, recursive); + } if (hook != null) - { - m_hookCache[hookName] = hook; // Cache for performance - } + m_hookCache[hookName] = hook; return hook; } + + static Transform FindHookByChildName(Transform root, string hookName, bool recursive) + { + if (root == null || string.IsNullOrEmpty(hookName)) + return null; + if (root.name == hookName) + return root; + if (!recursive) + return null; + foreach (Transform child in root) + { + Transform found = FindHookByChildName(child, hookName, true); + if (found != null) + return found; + } + return null; + } public AnimationClip GetAnimationClip(string animationName) { if (m_pMapAnimancer != null) diff --git a/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs b/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs index 0c66ce1fb7..806cd9db93 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs @@ -12,6 +12,7 @@ using UnityEngine; using BrewMonster.Scripts.Skills; using BrewMonster.Network; using Animancer; +using BrewMonster.Scripts.ECModel; public class CECNPC : CECObject { @@ -801,7 +802,9 @@ public class CECNPC : CECObject Array.Clear(m_aExtStates, 0, m_aExtStates.Length); m_pNPCModelPolicy = null; + m_pNPCCECModel = null; PoolManager.Instance.Despawn(m_modelVisual); + m_modelVisual = null; /*if (m_pPateName) { delete m_pPateName; @@ -1145,28 +1148,56 @@ public class CECNPC : CECObject return; } + uint bornStampAtRequest = GetBornStamp(); + int nidAtRequest = m_NPCInfo.nid; + + if (m_modelVisual != null) + { + PoolManager.Instance.Despawn(m_modelVisual); + m_modelVisual = null; + m_pNPCCECModel?.InvalidateHookCache(); + } + + GameObject loadedVisual = null; try { szModelFile = AFile.NormalizePath(szModelFile.ToLower(), true); - m_modelVisual = await NPCBuilder.Instance.GetModelByPath(szModelFile); - if (m_modelVisual == null) + loadedVisual = await NPCBuilder.Instance.GetModelByPath(szModelFile); + if (loadedVisual == null) { - m_modelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule); - m_modelVisual.name = szModelFile; + loadedVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule); + loadedVisual.name = szModelFile; BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} "); } } catch { - m_modelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule); + loadedVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule); BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} "); } - //var monsterModel = Instantiate(model, transform); + if (this == null || !gameObject) + return; + + if (GetBornStamp() != bornStampAtRequest || m_NPCInfo.nid != nidAtRequest) + { + if (loadedVisual != null) + PoolManager.Instance.Despawn(loadedVisual); + return; + } + + m_modelVisual = loadedVisual; m_modelVisual.transform.SetParent(transform, false); m_modelVisual.SetActive(true); + + InitializeNPCCECModel(m_modelVisual); + var npcVisual = GetComponent(); - npcVisual?.InitNPCEventDoneHandler(m_NPCInfo); + if (npcVisual != null) + { + npcVisual.RefreshNamedAnimancer(m_modelVisual); + npcVisual.InitNPCEventDoneHandler(m_NPCInfo); + } // UINPC.Start can run before async model instantiate; refresh anchor once SMR hierarchy exists. // UINPC.Start可能在异步模型实例化之前执行;SMR层次就绪后刷新名牌锚点。 @@ -1177,6 +1208,48 @@ public class CECNPC : CECObject //QueueECModelForLoad(MTL_ECM_NPC, GetNPCInfo().nid, GetBornStamp(), GetServerPos(), szModelFile, tid); } + + /// + /// Bind CECModel to the loaded NPC visual (prefab holds NamedAnimancer + CombineActHolder; no SkeletonBuilder). + /// 将CECModel绑定到已加载的NPC视觉对象(预制体含NamedAnimancer与CombineActHolder;NPC无SkeletonBuilder)。 + /// + void InitializeNPCCECModel(GameObject modelRoot) + { + if (modelRoot == null) + return; + + if (m_pNPCCECModel == null) + m_pNPCCECModel = new CECModel(); + + m_pNPCCECModel.SetId(m_NPCInfo.nid); + m_pNPCCECModel.SetTransform(modelRoot.transform); + m_pNPCCECModel.InvalidateHookCache(); + + NamedAnimancerComponent animancer = modelRoot.GetComponentInChildren(true); + if (animancer != null) + m_pNPCCECModel.SetNamedAnimancerComponent(animancer); + + CombineActHolder combineActHolder = modelRoot.GetComponentInChildren(true); + if (combineActHolder != null) + { + try + { + if (combineActHolder.ActionSO != null) + m_pNPCCECModel.SetCombinedAction(combineActHolder.ActionSO); + } + catch (Exception ex) + { + BMLogger.LogWarning($"[CECNPC] Failed to read CombineActHolder.ActionSO on '{modelRoot.name}': {ex.Message}"); + } + } + + SkeletonBuilder skeletonBuilder = modelRoot.GetComponentInChildren(true); + if (skeletonBuilder != null) + { + m_pNPCCECModel.SetSkeletonBuilder(skeletonBuilder); + m_pNPCCECModel.InitializeSkeletonBuilder(); + } + } public ROLEBASICPROP GetBasicProps() { return m_BasicProps; } public ROLEEXTPROP GetExtendProps() { return m_ExtProps; } public void SetSelectedTarget(int id) { m_idSelTarget = id; } @@ -1641,13 +1714,10 @@ public class CECNPC : CECObject public bool ShouldPlayNewActionFor(int iMoveMode) { - if (m_pNPCModelPolicy.IsPlayingAction()) - { - int iAction = GetMoveAction(iMoveMode); - return !m_pNPCModelPolicy.IsPlayingAction(iAction) - && m_pNPCModelPolicy.HasAction(iAction); - } - return false; + int iAction = GetMoveAction(iMoveMode); + if (!m_pNPCModelPolicy.HasAction(iAction)) + return false; + return !m_pNPCModelPolicy.IsPlayingAction(iAction); } public int GetMoveAction(int iMoveMode) { @@ -1833,77 +1903,14 @@ public class CECNPC : CECObject } /// - /// Get NPC's CECModel instance - /// 获取NPC的CECModel实例 + /// Get NPC's CECModel instance (bound after QueueLoadNPCModel, or lazily from m_modelVisual). + /// 获取NPC的CECModel实例(QueueLoadNPCModel后绑定,或从m_modelVisual延迟绑定)。 /// /// CECModel instance or null / CECModel实例,未找到返回null public CECModel GetModel() { - // Initialize CECModel if not already created - // 如果尚未创建,则初始化CECModel - if (m_pNPCCECModel == null) - { - // Find the model GameObject (typically a child of this NPC) - // 查找模型GameObject(通常是此NPC的子对象) - GameObject modelObject = null; - - // Try to find model in children (where NPC models are typically placed) - // 尝试在子对象中查找模型(NPC模型通常放置在那里) - NPCVisual npcVisual = GetComponent(); - if (npcVisual != null) - { - // Find SkeletonBuilder which is typically on the model GameObject - // 查找通常在模型GameObject上的SkeletonBuilder - SkeletonBuilder skeleton = GetComponentInChildren(true); - if (skeleton != null) - { - modelObject = skeleton.gameObject; - } - } - - // If no model found, try to find any child with SkeletonBuilder - // 如果未找到模型,尝试查找任何带有SkeletonBuilder的子对象 - if (modelObject == null) - { - SkeletonBuilder skeleton = GetComponentInChildren(true); - if (skeleton != null) - { - modelObject = skeleton.gameObject; - } - } - - if (modelObject != null) - { - // Create and initialize CECModel instance - // 创建并初始化CECModel实例 - m_pNPCCECModel = new CECModel(); - m_pNPCCECModel.SetTransform(modelObject.transform); - - // Find and set SkeletonBuilder - // 查找并设置SkeletonBuilder - SkeletonBuilder skeletonBuilder = modelObject.GetComponent(); - if (skeletonBuilder == null) - { - skeletonBuilder = modelObject.GetComponentInChildren(true); - } - - if (skeletonBuilder != null) - { - m_pNPCCECModel.SetSkeletonBuilder(skeletonBuilder); - m_pNPCCECModel.InitializeSkeletonBuilder(); - } - NamedAnimancerComponent animancer = modelObject.GetComponent(); - if (animancer == null) - { - animancer = modelObject.GetComponentInChildren(true); - } - if (animancer != null) - { - m_pNPCCECModel.SetNamedAnimancerComponent(animancer); - } - } - } - + if (m_pNPCCECModel == null && m_modelVisual != null) + InitializeNPCCECModel(m_modelVisual); return m_pNPCCECModel; } diff --git a/Assets/PerfectWorld/Scripts/NPC/CECNPCModelDefaultPolicy.cs b/Assets/PerfectWorld/Scripts/NPC/CECNPCModelDefaultPolicy.cs index 0afdf771a1..30070bc7a7 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECNPCModelDefaultPolicy.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECNPCModelDefaultPolicy.cs @@ -10,8 +10,8 @@ using UnityEngine; public class CECNPCModelDefaultPolicy : CECNPCModelPolicy { - CECModel m_pNPCModel; CECNPC m_pNPC; + CECModel NPCModel => m_pNPC?.GetModel(); int m_nBrushes; A3DAABB m_CHAABB; // AABB Updated with m_ppBrushes // number of brush object used in collision @@ -26,10 +26,17 @@ public class CECNPCModelDefaultPolicy public CECNPCModelDefaultPolicy(CECNPC pNPC) { - m_pNPCModel = new CECModel(); m_pNPC = pNPC; } + static bool IsMoveAction(int iAction) + { + return iAction == (int)NPCActionIndex.ACT_WALK + || iAction == (int)NPCActionIndex.ACT_RUN + || iAction == (int)NPCActionIndex.ACT_NPC_WALK + || iAction == (int)NPCActionIndex.ACT_NPC_RUN; + } + public string GetActionName(int iAct, bool bAttackStart = false) { // Tạo builder thay cho static char[128] @@ -52,10 +59,8 @@ public class CECNPCModelDefaultPolicy } public override void ClearComActFlag(bool bSignalCurrent) { - if (m_pNPCModel != null) - { - m_pNPCModel.ClearComActFlag(bSignalCurrent); - } + if (NPCModel != null) + NPCModel.ClearComActFlag(bSignalCurrent); } public override bool PlayAttackAction(int nAttackSpeed, CECAttackEvent attackEvent) { @@ -70,10 +75,8 @@ public class CECNPCModelDefaultPolicy } public override void StopChannelAction() { - if (m_pNPCModel != null) - { - m_pNPCModel.StopChannelAction(0, true); - } + if (NPCModel != null) + NPCModel.StopChannelAction(0, true); } public override bool GetCHAABB(ref A3DAABB aabb) { @@ -92,8 +95,8 @@ public class CECNPCModelDefaultPolicy } bool HasCHAABB() { - return m_pNPCModel != null - && m_pNPCModel.HasCHAABB() + return NPCModel != null + && NPCModel.HasCHAABB() && m_nBrushes > 0; } public override bool PlayModelAction(int iAction, bool bRestart, CECAttackEvent cECAttackEvent) @@ -125,11 +128,11 @@ public class CECNPCModelDefaultPolicy if (_npcVisual.IsAnimationExist(szAct)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct, ref ignoreRef, 0, 0, false, false, false); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct, ref ignoreRef, 0, 0, false, false, false); } if (_npcVisual.IsAnimationExist(szAct2)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } } } @@ -141,7 +144,7 @@ public class CECNPCModelDefaultPolicy string szAct2 = GetActionName((int)NPCActionIndex.ACT_STAND); if (_npcVisual.IsAnimationExist(szAct2)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } //m_pNPCModel->QueueAction(GetActionName((int)NPCActionIndex.ACT_STAND)); } @@ -154,7 +157,7 @@ public class CECNPCModelDefaultPolicy string szAct2 = GetActionName((int)NPCActionIndex.ACT_NPC_STAND); if (_npcVisual.IsAnimationExist(szAct2)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } //m_pNPCModel->QueueAction(GetActionName((int)NPCActionIndex.ACT_NPC_STAND)); } @@ -169,11 +172,11 @@ public class CECNPCModelDefaultPolicy string szAct2 = GetActionName((int)NPCActionIndex.ACT_NPC_STAND); if (_npcVisual.IsAnimationExist(szAct)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct, ref ignoreRef, 0, 0, false, false, false); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct, ref ignoreRef, 0, 0, false, false, false); } if (_npcVisual.IsAnimationExist(szAct2)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } } } @@ -185,14 +188,19 @@ public class CECNPCModelDefaultPolicy string szAct2 = GetActionName((int)NPCActionIndex.ACT_STAND); if (_npcVisual.IsAnimationExist(szAct2)) { - m_pNPCModel.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); + NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } //m_pNPCModel->QueueAction(GetActionName((int)NPCActionIndex.ACT_STAND)); } } else { - result = _npcVisual.TryPlayAction(GetActionName(iAction), cECAttackEvent); + result = _npcVisual.TryPlayAction( + GetActionName(iAction), + cECAttackEvent, + isHit: false, + bRestart: bRestart, + bLoop: true); } return result; } @@ -229,7 +237,10 @@ public class CECNPCModelDefaultPolicy // [English] Returns true when the NPC model is present and the game object is active public override bool IsModelLoaded() { - return m_pNPCModel != null && m_pNPC != null && m_pNPC.gameObject.activeInHierarchy; + return m_pNPC != null + && m_pNPC.gameObject.activeInHierarchy + && NPCModel != null + && NPCModel.transform != null; } // [中文] 异步加载并挂载状态效果 GFX 到指定挂点;以 (路径+挂点) 为键去重 diff --git a/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs b/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs index e42f2c3b3a..f8fe7e8eb2 100644 --- a/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs +++ b/Assets/PerfectWorld/Scripts/NPC/NPCVisual.cs @@ -1,5 +1,6 @@ using Animancer; using BrewMonster; +using BrewMonster.Scripts.ECModel; using System; using System.Collections.Generic; using UnityEngine; @@ -9,29 +10,82 @@ public class NPCVisual : MonoBehaviour { [SerializeField] NamedAnimancerComponent namedAnimancer; protected CECNPC.INFO m_NPCInfo; - [SerializeField] private Queue _animationQueue = new Queue(); + [SerializeField] private Queue _animationQueue = new Queue(); [SerializeField] private AnimancerState _currentState; + private string _previousAnimationName; + private string _currentAnimationName; private bool debugNamePlateBounds = false; - private const float fadeTime = .2f; + private const float FadeTime = 100f; 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) + public bool TryPlayAction(string animationName, CECAttackEvent cECAttackEvent, bool isHit = false, bool bRestart = true, bool bLoop = false) { + if (namedAnimancer == null) + return false; - if (namedAnimancer == null) return false; - - if (namedAnimancer.IsPlaying(animationName)) return false; - _currentState = namedAnimancer.TryPlay(animationName, fadeTime); - if (isHit) + if (namedAnimancer.IsPlaying(animationName)) { - if(_currentState != null) - { - _currentState.Events.OnEnd = () => SetHitOnEnd(cECAttackEvent); - } + if (!bRestart) + return true; + if (!bLoop) + return false; } - return _currentState != null; + + if (!InternalPlayAnimation(animationName)) + return false; + + _previousAnimationName = animationName; + if (isHit) + _currentState.Events.OnEnd = () => SetHitOnEnd(cECAttackEvent); + else + ApplyAnimationEndCallbacks(animationName, bLoop); + + return true; + } + + bool InternalPlayAnimation(string animationName, float duration = FadeTime, FadeMode fadeMode = FadeMode, float playSpeed = 1.0f) + { + if (namedAnimancer == null) + return false; + + if (!namedAnimancer.States.TryGet(animationName, out _)) + return false; + + _currentState = namedAnimancer.TryPlay(animationName, duration / 1000f, fadeMode); + if (_currentState == null) + return false; + + _currentState.Time = 0; + _currentState.Speed = playSpeed > 0f ? playSpeed : 1.0f; + _currentAnimationName = animationName; + return true; + } + + void ApplyAnimationEndCallbacks(string animationName, bool isLoop) + { + if (_currentState == null) + return; + _currentState.Events.OnEnd = () => + { + if (isLoop) + EnqueueAnimationForLooping(animationName); + }; + } + + bool EnqueueAnimationForLooping(string animationName) + { + if (namedAnimancer == null || _animationQueue.Count > 0) + return false; + + _animationQueue.Enqueue(new AnimationQueue + { + AnimationName = animationName, + IsForceStopPrevious = false, + IsLoop = true, + }); + return true; } private void SetHitOnEnd(CECAttackEvent cECAttackEvent) { @@ -69,23 +123,35 @@ public class NPCVisual : MonoBehaviour public bool EnqueueAnimation(QueueNPCActionEvent @event) { if (namedAnimancer == null) return false; - _animationQueue.Enqueue(@event.AnimationName); - + _animationQueue.Enqueue(new AnimationQueue + { + AnimationName = @event.AnimationName, + IsForceStopPrevious = false, + IsLoop = false, + }); return true; } - public string animName1; + private void PlayNext() { if (_animationQueue.Count == 0) - { return; + + if (_animationQueue.Peek().IsForceStopPrevious) + { + _currentState?.Stop(); + _currentState = null; } - if (_currentState == null) return; - animName1 = _animationQueue.Peek(); - if (_currentState.NormalizedTime < 1f) return; - string animName = _animationQueue.Dequeue(); - _currentState = namedAnimancer.TryPlay(animName); + if (_currentState != null && _currentState.NormalizedTime < 1f) + return; + + var animationQueue = _animationQueue.Dequeue(); + _previousAnimationName = animationQueue.AnimationName; + float duration = animationQueue.ITransTime > 0 ? animationQueue.ITransTime : FadeTime; + if (!InternalPlayAnimation(animationQueue.AnimationName, duration, FadeMode, animationQueue.PlaySpeed)) + return; + ApplyAnimationEndCallbacks(animationQueue.AnimationName, animationQueue.IsLoop); } private void OnDestroy() { @@ -114,27 +180,18 @@ public class NPCVisual : MonoBehaviour /// The root GameObject of the model to search for NamedAnimancerComponent / 要搜索NamedAnimancerComponent的模型根GameObject public void RefreshNamedAnimancer(GameObject modelRoot = null) { + _currentState = null; + _previousAnimationName = null; + _currentAnimationName = null; + _animationQueue.Clear(); + if (modelRoot != null) - { - // Search specifically within the model GameObject's hierarchy - // 在模型GameObject的层次结构中搜索 - namedAnimancer = modelRoot.GetComponentInChildren(); - } + namedAnimancer = modelRoot.GetComponentInChildren(true); else - { - // Fallback to searching from this component's hierarchy - // 回退到从此组件的层次结构搜索 - namedAnimancer = GetComponentInChildren(); - } + namedAnimancer = GetComponentInChildren(true); 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"}"); - } + BMLogger.LogWarning($"NPCVisual: RefreshNamedAnimancer - namedAnimancer == null (modelRoot: {modelRoot?.name ?? "null"})"); } // Cached reference to the bone with the highest world-Y position at model-ready time.