Files
test/Assets/Scripts/PlayerVisual.cs
T

329 lines
13 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 CECAttackEvent AttackEvent;
}
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 void PlayActionEventHandler(PlayActionEvent @event)
{
//when this trigger, clear all the animation in the queue which in the same layer of animancer
if (_animationQueue.Count > 0)
{
_animationQueue.Enqueue(new AnimationQueue
{
AnimationName = @event.AnimationName,
IsForceStopPrevious = @event.IsForceStopPrevious,
AttackEvent = @event.AttackEvent
});
_animationList = _animationQueue.Select(q => q.AnimationName).ToList();
return;
}
InternalPlayAnimation(@event.AnimationName, @event.ITransTime);
ApplyAttackSignalOnAnimationEnd(@event.AttackEvent);
}
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;
_animationQueue.Enqueue(new AnimationQueue
{
AnimationName = @event.AnimationName,
IsForceStopPrevious = @event.IsForceStopPrevious,
AttackEvent = null
});
_animationList = _animationQueue.Select(q => q.AnimationName).ToList();
if (!isHit)
{
queueActionEvent = @event;
isHit = @event.IsHitAnim;
}
return true;
}
private void PlayNext()
{
if (_animationQueue.Count == 0)
{
return;
}
// if (_currentState == null)
// {
// _animationQueue.Dequeue();
// return;
// }
//peek next if IsForceStopPrevious is true, force end
if (_animationQueue.Peek().IsForceStopPrevious)
{
Debug.Log($" InternalPlayAnimation PlayNext: Force Stop Previous");
_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();
InternalPlayAnimation(animationQueue.AnimationName);
ApplyAttackSignalOnAnimationEnd(animationQueue.AttackEvent);
}
private void ApplyAttackSignalOnAnimationEnd(CECAttackEvent attackEvent)
{
if (attackEvent == null || _currentState == null)
{
return;
}
_currentState.Events.OnEnd = () =>
{
attackEvent.m_bSignaled = true;
};
}
void ApplyDamage()
{
if (queueActionEvent == null) return;
isHit = false;
queueActionEvent.SetFlag(true, queueActionEvent.AttackEvent);
queueActionEvent = null;
}
private void OnDestroy()
{
UnregisterPlayerEventHandlers();
}
public bool IsAnimationExist(string animationName)
{
return namedAnimancer.States.TryGet("ActionName", out var existingState) ? true : false;
}
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 isState = namedAnimancer.States.TryGet(animationName, out var existingState) ? true : false;
if (isState)
{
_currentState = namedAnimancer.TryPlay(animationName, duration / 1000, fadeMode);
_currentAnimationName = animationName;
//Debug.Log($"InternalPlayAnimation: removeShapeName 1 TriggerName={removeShapeName}");
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)
{
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;
}
}
}