Add NPC animation behavior

This commit is contained in:
Tran Hai Nam
2026-05-27 17:47:58 +07:00
parent 94b6ef8200
commit 9a361247ef
6 changed files with 254 additions and 167 deletions
@@ -307,7 +307,7 @@ public class CombineActHolderFolderAssigner : EditorWindow
if (AssetDatabase.GetMainAssetTypeAtPath(prefabPath) != typeof(GameObject))
return false;
GameObject prefabAsset = AssetDatabase.LoadMainAssetAtPath<GameObject>(prefabPath);
GameObject prefabAsset = (GameObject)AssetDatabase.LoadMainAssetAtPath(prefabPath);
if (prefabAsset == null)
return false;
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6a8ecae27ef44213a7965bfcb0e1cfcf30a013734f9c6fd14095d6440259005
size 145407
oid sha256:ea74bc9aef8c1251035c339a2a463e2d026cc0af379759abf687dcf13c9bda99
size 146707
+35 -23
View File
@@ -1271,43 +1271,55 @@ public class CECModel
/// <returns>Hook Transform or null / 挂点变换,未找到返回null</returns>
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)
+86 -79
View File
@@ -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>();
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);
}
/// <summary>
/// Bind CECModel to the loaded NPC visual (prefab holds NamedAnimancer + CombineActHolder; no SkeletonBuilder).
/// 将CECModel绑定到已加载的NPC视觉对象(预制体含NamedAnimancer与CombineActHolderNPC无SkeletonBuilder)。
/// </summary>
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<NamedAnimancerComponent>(true);
if (animancer != null)
m_pNPCCECModel.SetNamedAnimancerComponent(animancer);
CombineActHolder combineActHolder = modelRoot.GetComponentInChildren<CombineActHolder>(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<SkeletonBuilder>(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; }
@@ -1640,14 +1713,11 @@ 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);
}
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
}
/// <summary>
/// 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延迟绑定)。
/// </summary>
/// <returns>CECModel instance or null / CECModel实例,未找到返回null</returns>
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<NPCVisual>();
if (npcVisual != null)
{
// Find SkeletonBuilder which is typically on the model GameObject
// 查找通常在模型GameObject上的SkeletonBuilder
SkeletonBuilder skeleton = GetComponentInChildren<SkeletonBuilder>(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<SkeletonBuilder>(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<SkeletonBuilder>();
if (skeletonBuilder == null)
{
skeletonBuilder = modelObject.GetComponentInChildren<SkeletonBuilder>(true);
}
if (skeletonBuilder != null)
{
m_pNPCCECModel.SetSkeletonBuilder(skeletonBuilder);
m_pNPCCECModel.InitializeSkeletonBuilder();
}
NamedAnimancerComponent animancer = modelObject.GetComponent<NamedAnimancerComponent>();
if (animancer == null)
{
animancer = modelObject.GetComponentInChildren<NamedAnimancerComponent>(true);
}
if (animancer != null)
{
m_pNPCCECModel.SetNamedAnimancerComponent(animancer);
}
}
}
if (m_pNPCCECModel == null && m_modelVisual != null)
InitializeNPCCECModel(m_modelVisual);
return m_pNPCCECModel;
}
@@ -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 到指定挂点;以 (路径+挂点) 为键去重
+93 -36
View File
@@ -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<string> _animationQueue = new Queue<string>();
[SerializeField] private Queue<AnimationQueue> _animationQueue = new Queue<AnimationQueue>();
[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))
{
if (!bRestart)
return true;
if (!bLoop)
return false;
}
if (namedAnimancer.IsPlaying(animationName)) return false;
_currentState = namedAnimancer.TryPlay(animationName, fadeTime);
if (!InternalPlayAnimation(animationName))
return false;
_previousAnimationName = animationName;
if (isHit)
{
if(_currentState != null)
{
_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;
}
return _currentState != null;
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
/// <param name="modelRoot">The root GameObject of the model to search for NamedAnimancerComponent / 要搜索NamedAnimancerComponent的模型根GameObject</param>
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<NamedAnimancerComponent>();
}
namedAnimancer = modelRoot.GetComponentInChildren<NamedAnimancerComponent>(true);
else
{
// Fallback to searching from this component's hierarchy
// 回退到从此组件的层次结构搜索
namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>();
}
namedAnimancer = GetComponentInChildren<NamedAnimancerComponent>(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.