398 lines
16 KiB
C#
398 lines
16 KiB
C#
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<string, AnimancerState> _activeStates = new();
|
|
[SerializeField] private AnimancerState _currentState;
|
|
[SerializeField] private Queue<AnimationQueue> _animationQueue = new Queue<AnimationQueue>();
|
|
[SerializeField] private List<string> _animationList = new List<string>();
|
|
[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)
|
|
{
|
|
_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, @event.IsLoop);
|
|
ApplyAnimationEndCallbacks(@event.AttackEvent, @event.ChannelAct, @event.Rank, @event.AnimationName);
|
|
}
|
|
public void InitPlayerEventDoneHandler()
|
|
{
|
|
namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>();
|
|
if (namedAnimancer == null)
|
|
{
|
|
BrewMonster.BMLogger.LogWarning("InitPlayerEventDoneHandler animancer == null");
|
|
return;
|
|
}
|
|
var player = GetComponentInParent<CECPlayer>();
|
|
if (player == null)
|
|
{
|
|
BMLogger.LogWarning("player == null");
|
|
return;
|
|
}
|
|
|
|
if (_eventBusSubscribed)
|
|
UnregisterPlayerEventHandlers();
|
|
|
|
_playerInfo = player.GetPlayInfo();
|
|
id = _playerInfo.cid;
|
|
EventBus.SubscribeChannel<PlayActionEvent>(_playerInfo.cid, PlayActionEventHandler);
|
|
EventBus.SubscribeChannelClass<QueueActionEvent>(_playerInfo.cid, QueueActionEventHandler);
|
|
EventBus.SubscribeChannel<ClearComActFlagAllRankNodesEvent>(_playerInfo.cid, ClearComActFlagAllRankNodesEventHandler);
|
|
_eventBusSubscribed = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unsubscribe from per-cid animation events (e.g. hand off to navigate clone). / 取消按 cid 订阅(例如交给导航克隆体)
|
|
/// </summary>
|
|
public void UnregisterPlayerEventHandlers()
|
|
{
|
|
if (!_eventBusSubscribed)
|
|
return;
|
|
EventBus.UnsubscribeChannel<PlayActionEvent>(_playerInfo.cid, PlayActionEventHandler);
|
|
EventBus.UnsubscribeChannelClass<QueueActionEvent>(_playerInfo.cid, QueueActionEventHandler);
|
|
EventBus.UnsubscribeChannel<ClearComActFlagAllRankNodesEvent>(_playerInfo.cid, ClearComActFlagAllRankNodesEventHandler);
|
|
_eventBusSubscribed = false;
|
|
}
|
|
|
|
// public void InitElsePlayerEventDoneHandler(INFO playerInfo)
|
|
// {
|
|
// namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>();
|
|
// if (namedAnimancer == null)
|
|
// {
|
|
// BrewMonster.BMLogger.LogError("animancer == null");
|
|
// return;
|
|
// }
|
|
// //var player = GetComponentInParent<CECPlayer>();
|
|
// //if (player == null)
|
|
// //{
|
|
// // BrewMonster.BMLogger.LogError("player == null");
|
|
// // return;
|
|
// //}
|
|
// _playerInfo = playerInfo;//player.GetPlayInfo();
|
|
// EventBus.SubscribeChannel<PlayActionEvent>(_playerInfo.cid, PlayActionEventHandler);
|
|
// EventBus.SubscribeChannelClass<QueueActionEvent>(_playerInfo.cid, QueueActionEventHandler);
|
|
// EventBus.SubscribeChannel<CleearComActFlagAllRankNodesEvent>(_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;
|
|
}
|
|
/// <summary>
|
|
/// This function is used to enqueue an animation for looping when the animancer is not set to looping
|
|
/// </summary>
|
|
/// <param name="animationName"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
string animationQueueString = "";
|
|
foreach(var animation in _animationQueue)
|
|
{
|
|
animationQueueString += animation.AnimationName + ", ";
|
|
}
|
|
}
|
|
|
|
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, animationQueue.IsLoop);
|
|
ApplyAnimationEndCallbacks(animationQueue.AttackEvent, animationQueue.ChannelAct, animationQueue.Rank, animationQueue.AnimationName);
|
|
}
|
|
|
|
private void ApplyAnimationEndCallbacks(CECAttackEvent attackEvent, ChannelAct channelAct, int rank, string animationName)
|
|
{
|
|
if (_currentState == null) return;
|
|
_currentState.Events.OnEnd = () =>
|
|
{
|
|
if (attackEvent != null)
|
|
attackEvent.m_bSignaled = true;
|
|
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();
|
|
}
|
|
public bool IsAnimationExist(string animationName)
|
|
{
|
|
var exists = namedAnimancer.States.TryGet("ActionName", out var existingState) ? true : false;
|
|
return exists;
|
|
}
|
|
|
|
private string _currentAnimationName;
|
|
/// <summary>
|
|
/// play an animation with name
|
|
/// </summary>
|
|
/// <param name="animationName"></param>
|
|
/// <param name="duration"></param>
|
|
/// <param name="fadeMode"></param>
|
|
private void InternalPlayAnimation(string animationName, float duration = FadeTime, FadeMode fadeMode = FadeMode, bool isLoop = false)
|
|
{
|
|
if (namedAnimancer == null)
|
|
{
|
|
return;
|
|
}
|
|
bool isState = namedAnimancer.States.TryGet(animationName, out var existingState) ? true : false;
|
|
if (isState)
|
|
{
|
|
_currentState = namedAnimancer.TryPlay(animationName, duration / 1000, fadeMode);
|
|
_currentAnimationName = animationName;
|
|
//if the animation is looping and the current state is not looping, play the animation again
|
|
if(isLoop == true && _currentState.IsLooping == false)
|
|
{
|
|
_currentState.Time = 0;
|
|
_currentState.Events.OnEnd = () => EnqueueAnimationForLooping(animationName);
|
|
}
|
|
return;
|
|
}
|
|
//BMLogger.LogError($"Null name animation: {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)
|
|
{
|
|
// 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<NamedAnimancerComponent>();
|
|
}
|
|
else
|
|
{
|
|
// Fallback to searching from this component's hierarchy
|
|
// 回退到从此组件的层次结构搜索
|
|
namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>();
|
|
}
|
|
|
|
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"}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve nameplate 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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
} |