using System; using Animancer; using BrewMonster; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace BrewMonster { public struct AnimationQueue { public string AnimationName; public bool IsForceStopPrevious; public int ITransTime; public CECAttackEvent AttackEvent; public bool IsLoop; public ChannelAct ChannelAct; public int Rank; } public class PlayerVisual : MonoBehaviour { [SerializeField] NamedAnimancerComponent namedAnimancer; [SerializeField] private INFO _playerInfo; private Dictionary _activeStates = new(); [SerializeField] private AnimancerState _currentState; [SerializeField] private Queue _animationQueue = new Queue(); [SerializeField] private List _animationList = new List(); [SerializeField] private bool isHit; [SerializeField] private int id; [SerializeField] private bool isDebug; [SerializeField] private bool debugNamePlateBounds; private bool _eventBusSubscribed; private const float FadeTime = 100; private const FadeMode FadeMode = Animancer.FadeMode.FixedDuration; QueueActionEvent queueActionEvent; private string previousAnimationName; private void PlayActionEventHandler(PlayActionEvent @event) { //prevent enqueue the same loop animation bool loopcheck = @event.IsLoop == true && previousAnimationName == @event.AnimationName; if(loopcheck) { return; } if (_animationQueue.Count > 0) { if(@event.IsForceStopPrevious) { _animationQueue.Clear(); } else { _animationQueue.Enqueue(new AnimationQueue { AnimationName = @event.AnimationName, IsForceStopPrevious = @event.IsForceStopPrevious, AttackEvent = @event.AttackEvent, ChannelAct = @event.ChannelAct, Rank = @event.Rank }); _animationList = _animationQueue.Select(q => q.AnimationName).ToList(); return; } } previousAnimationName = @event.AnimationName; InternalPlayAnimation(@event.AnimationName, @event.ITransTime, FadeMode); ApplyAnimationEndCallbacks(@event.AttackEvent, @event.ChannelAct, @event.Rank, @event.AnimationName, @event.IsLoop); } public void InitPlayerEventDoneHandler() { namedAnimancer = GetComponentInChildren(); if (namedAnimancer == null) { BrewMonster.BMLogger.LogWarning("InitPlayerEventDoneHandler animancer == null"); return; } var player = GetComponentInParent(); if (player == null) { BMLogger.LogWarning("player == null"); return; } if (_eventBusSubscribed) UnregisterPlayerEventHandlers(); _playerInfo = player.GetPlayInfo(); id = _playerInfo.cid; EventBus.SubscribeChannel(_playerInfo.cid, PlayActionEventHandler); EventBus.SubscribeChannelClass(_playerInfo.cid, QueueActionEventHandler); EventBus.SubscribeChannel(_playerInfo.cid, ClearComActFlagAllRankNodesEventHandler); _eventBusSubscribed = true; } /// /// Unsubscribe from per-cid animation events (e.g. hand off to navigate clone). / 取消按 cid 订阅(例如交给导航克隆体) /// public void UnregisterPlayerEventHandlers() { if (!_eventBusSubscribed) return; EventBus.UnsubscribeChannel(_playerInfo.cid, PlayActionEventHandler); EventBus.UnsubscribeChannelClass(_playerInfo.cid, QueueActionEventHandler); EventBus.UnsubscribeChannel(_playerInfo.cid, ClearComActFlagAllRankNodesEventHandler); _eventBusSubscribed = false; } // public void InitElsePlayerEventDoneHandler(INFO playerInfo) // { // namedAnimancer = GetComponentInChildren(); // if (namedAnimancer == null) // { // BrewMonster.BMLogger.LogError("animancer == null"); // return; // } // //var player = GetComponentInParent(); // //if (player == null) // //{ // // BrewMonster.BMLogger.LogError("player == null"); // // return; // //} // _playerInfo = playerInfo;//player.GetPlayInfo(); // EventBus.SubscribeChannel(_playerInfo.cid, PlayActionEventHandler); // EventBus.SubscribeChannelClass(_playerInfo.cid, QueueActionEventHandler); // EventBus.SubscribeChannel(_playerInfo.cid, CleearComActFlagAllRankNodesEventHandler); // } private void ClearComActFlagAllRankNodesEventHandler(ClearComActFlagAllRankNodesEvent @event) { _animationQueue.Clear(); _animationList = _animationQueue.Select(q => q.AnimationName).ToList(); if (isHit) { ApplyDamage(); } //todo: this is dummy to force change to idle state // EventBus.PublishChannel(_playerInfo.cid, new PlayActionEvent("站立_通用")); } private void QueueActionEventHandler(QueueActionEvent @event) { if (!EnqueueAnimation(@event)) { BMLogger.LogError("HoangDev : EnqueueAnimation Failed"); } } private void Update() { PlayNext(); } public bool EnqueueAnimation(QueueActionEvent @event) { if (namedAnimancer == null) { return false; } if(previousAnimationName == @event.AnimationName) { return false; } previousAnimationName = @event.AnimationName; _animationQueue.Enqueue(new AnimationQueue { AnimationName = @event.AnimationName, IsForceStopPrevious = @event.IsForceStopPrevious, ITransTime = @event.ITransTime, AttackEvent = @event.AttackEvent, IsLoop = @event.IsLoop, ChannelAct = @event.ChannelAct, Rank = @event.Rank }); _animationList = _animationQueue.Select(q => q.AnimationName).ToList(); if (!isHit) { queueActionEvent = @event; isHit = @event.IsHitAnim; } return true; } /// /// This function is used to enqueue an animation for looping when the animancer is not set to looping /// /// /// private bool EnqueueAnimationForLooping(string animationName) { if (namedAnimancer == null) { return false; } //prevent call if these is a animation already in the queue if(_animationQueue.Count > 0) { return false; } _animationQueue.Enqueue(new AnimationQueue { AnimationName = animationName, IsForceStopPrevious = false, AttackEvent = null, IsLoop = true }); _animationList = _animationQueue.Select(q => q.AnimationName).ToList(); return true; } private void PlayNext() { if (_animationQueue.Count == 0) { return; } if (_animationQueue.Peek().IsForceStopPrevious) { _currentState?.Stop(); _currentState = null; } if (_currentState != null && _currentState.NormalizedTime < 1f) return; if (isHit)// have it relative to check _currentState == null? { ApplyDamage(); } var animationQueue = _animationQueue.Dequeue(); _animationList = _animationQueue.Select(q => q.AnimationName).ToList(); previousAnimationName = animationQueue.AnimationName; InternalPlayAnimation(animationQueue.AnimationName, animationQueue.ITransTime, FadeMode); ApplyAnimationEndCallbacks(animationQueue.AttackEvent, animationQueue.ChannelAct, animationQueue.Rank, animationQueue.AnimationName,animationQueue.IsLoop); } private void ApplyAnimationEndCallbacks(CECAttackEvent attackEvent, ChannelAct channelAct, int rank, string animationName, bool isLoop) { if (_currentState == null) return; _currentState.Events.OnEnd = () => { if (attackEvent != null) attackEvent.m_bSignaled = true; if(isLoop) { EnqueueAnimationForLooping(animationName); } if (channelAct == null || string.IsNullOrEmpty(animationName)) return; var node = channelAct.GetNodeByRank((byte)rank); node?.m_pActive?.m_ActionNames?.Remove(animationName); }; } void ApplyDamage() { if (queueActionEvent == null) { return; } isHit = false; queueActionEvent.SetFlag(true, queueActionEvent.AttackEvent); queueActionEvent = null; } private void OnDestroy() { UnregisterPlayerEventHandlers(); } private void OnDisable() { UnregisterPlayerEventHandlers(); } public bool IsAnimationExist(string animationName) { var exists = namedAnimancer.States.TryGet("ActionName", out var existingState) ? true : false; return exists; } private string _currentAnimationName; /// /// play an animation with name /// /// /// /// private void InternalPlayAnimation(string animationName, float duration = FadeTime, FadeMode fadeMode = FadeMode) { if (namedAnimancer == null) { return; } bool isState = namedAnimancer.States.TryGet(animationName, out var existingState) ? true : false; if (isState) { _currentState = namedAnimancer.TryPlay(animationName, duration / 1000, fadeMode); _currentState.Time = 0; _currentAnimationName = animationName; return; } //BMLogger.LogError($"Null name animation: {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) { // Reset old runtime state when swapping model roots to avoid stale AnimancerState references. // 切换模型根节点时重置旧运行时状态,避免持有过期的AnimancerState引用。 _currentState = null; _currentAnimationName = 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($"PlayerVisual: RefreshNamedAnimancer - namedAnimancer == null after refresh (modelRoot: {modelRoot?.name ?? "null"})"); } else { BMLogger.Log($"PlayerVisual: RefreshNamedAnimancer - Successfully refreshed namedAnimancer from model: {modelRoot?.name ?? "default"}"); } } /// /// Resolve nameplate 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) { Debug.Log($"[Cuong] [PlayerVisual] 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] [PlayerVisual] TryGetNamePlateAnchorWorld: no valid SkinnedMeshRenderer/sharedMesh."); } return false; } // Use merged bounds center for X/Z and merged top for Y. // 使用合并包围盒中心作为X/Z,使用合并包围盒顶部作为Y。 worldPos = new Vector3(combinedBounds.center.x, combinedBounds.max.y, combinedBounds.center.z); if (debugNamePlateBounds) { Debug.Log($"[Cuong] [PlayerVisual] combinedCenter={combinedBounds.center} combinedSize={combinedBounds.size} anchorWorld={worldPos}"); } return true; } } }