diff --git a/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs index 6611d201e1..a1755d82ad 100644 --- a/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading.Tasks; using UnityEngine; using static Unity.Cinemachine.CinemachineFreeLookModifier; @@ -46,31 +45,23 @@ namespace BrewMonster A3DSkillGfxComposer pComposer, long nHostID, long nTargetID, - string szFlyGfx, - string szHitGfx, uint dwFlyTimeSpan, bool bTraceTarget, GfxMoveMode FlyMode, int nFlyGfxCount, uint dwInterval, - GFX_SKILL_PARAM param, - - float fFlyGfxScale, - - float fHitGfxScale, + GFX_SKILL_PARAM param, uint dwModifier, bool bOnlyOneHit, - bool bFadeOut, - bool bIsGoblinSkill, - bool bReverse ) { - + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddSkillGfxEvent: host={nHostID}, target={nTargetID}, fly={szFlyGfx ?? "NULL"}, hit={szHitGfx ?? "NULL"}, flyTime={dwFlyTimeSpan}, mode={FlyMode}, count={nFlyGfxCount}, interval={dwInterval}"); + bool bRet = true, bCluster; uint dwDelayTime; @@ -84,9 +75,14 @@ namespace BrewMonster dwDelayTime = 0; bCluster = true; } + + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddSkillGfxEvent: Creating {nFlyGfxCount} event(s), cluster={bCluster}, delay={dwDelayTime}"); + for (int i = 0; i < nFlyGfxCount; i++) { string value = bOnlyOneHit && i != nFlyGfxCount - 1 ? "" : szHitGfx; + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddSkillGfxEvent: Creating event {i + 1}/{nFlyGfxCount}, hitGfx={value ?? "NULL"}"); + if (!AddOneSkillGfxEvent( pComposer, nHostID, @@ -95,53 +91,66 @@ namespace BrewMonster FlyMode, dwDelayTime, dwFlyTimeSpan, - value, + value, param, bTraceTarget, - fFlyGfxScale, - fHitGfxScale, dwModifier, bCluster, bFadeOut, bIsGoblinSkill, bReverse )) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddSkillGfxEvent: AddOneSkillGfxEvent FAILED for event {i + 1}"); bRet = false; + } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddSkillGfxEvent: AddOneSkillGfxEvent SUCCESS for event {i + 1}"); + } dwDelayTime += dwInterval; } return bRet; - } public bool AddOneSkillGfxEvent( A3DSkillGfxComposer pComposer, long nHostID, long nTargetID, - string szFlyGfx, GfxMoveMode mode, uint dwDelayTime, uint dwFlyTimeSpan, string szHitGfx, - - GFX_SKILL_PARAM param, - + GFX_SKILL_PARAM param, bool bTraceTarget, - - float fFlyGfxScale, - - float fHitGfxScale, uint dwModifier, bool bCluster, - bool bFadeOut, - bool bIsGoblinSkill, - bool bReverse) { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneSkillGfxEvent: host={nHostID}, target={nTargetID}, fly={szFlyGfx ?? "NULL"}, hit={szHitGfx ?? "NULL"}, mode={mode}, flyTime={dwFlyTimeSpan}, delay={dwDelayTime}"); + + // Validate host ID + // 验证施法者ID + if (nHostID == 0) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneSkillGfxEvent: WARNING - Invalid host ID (0), skipping event creation"); + return false; + } + + // Validate target ID - allow 0 for area skills, but warn about suspiciously large negative values + // 验证目标ID - 允许0用于区域技能,但对可疑的大负值发出警告 + if (nTargetID < -1000000000) // Suspiciously large negative value (likely uninitialized) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneSkillGfxEvent: WARNING - Suspicious target ID ({nTargetID}), this may be uninitialized. Event will be created but may fail target lookup."); + } + A3DSkillGfxEvent pEvent = SkillGfxMan.InstanceSub.GetEmptyEvent(mode); + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneSkillGfxEvent: Event created from pool, type={pEvent.GetType().Name}"); + pEvent.SetComposer(pComposer); pEvent.SetHostID(nHostID); pEvent.SetTargetID(nTargetID); @@ -163,34 +172,18 @@ namespace BrewMonster pEvent.SetHostModelCreatedByGfx(Prop.bHostECMCreatedByGfx); } - /* if (szFlyGfx != 0) - { - A3DGFXEx pGfx = pEvent.LoadFlyGfx(m_pDevice, szFlyGfx.ToString()); - if (pGfx != null) - { - pGfx.SetScale(fFlyGfxScale); - pGfx.SetDisableCamShake(pEvent.GetDisableCamShake()); - pGfx.SetCreatedByGFXECM(pEvent.GetHostModelCreatedByGfx()); - pGfx.SetUseLOD(pEvent.GetGfxUseLod()); - pGfx.SetId(pEvent.GetHostID()); - pEvent.SetFlyGfx(pGfx); - } - }*/ + // NOTE: In Unity, GFX are Particle Systems — scaling is handled by the particle system itself, + // not by code. The C++ pGfx.SetScale() calls are not needed. + // 注意:在Unity中,GFX是粒子系统 — 缩放由粒子系统自身处理,不需要代码设置。 - if (string.IsNullOrEmpty(szHitGfx)) - { - /*game pGfx = pEvent.LoadHitGfx(m_pDevice, szHitGfx.ToString()); - if (pGfx != null) - { - pGfx.SetScale(fHitGfxScale); - pEvent.SetHitGfx(pGfx); - }*/ - } + // Fly GFX instantiation is handled by CECSkillGfxEvent.SpawnFlyGfx() + // Hit GFX instantiation is handled by CECSkillGfxEvent.SpawnHitGfx() #if !_SKILLGFXCOMPOSER pEvent.Tick(0); #endif PushEvent(pEvent); + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneSkillGfxEvent: Event pushed to SkillGfxMan, active events={SkillGfxMan.InstanceSub.m_EventLst.Count}"); return true; } public virtual bool GetPropertyById(long nId, ref ECMODEL_GFX_PROPERTY pProperty) => false; @@ -397,35 +390,70 @@ namespace BrewMonster // 在Unity中,命中特效通过Destroy(obj, 3f)自动销毁。立即转为Finished状态。 m_enumState = GfxSkillEventState.enumFinished; } - else if (m_dwCurSpan > m_dwFlyTimeSpan) // 飞行超时 / Flight timeout - { - if (!m_bTargetExist) - m_enumState = GfxSkillEventState.enumFinished; - else - HitTarget(GetTargetCenter()); - } - else if (!m_bTargetExist) - m_enumState = GfxSkillEventState.enumFinished; else if (m_enumState == GfxSkillEventState.enumWait) { if (m_dwCurSpan < m_dwDelayTime) return; + // Check host existence before transitioning to Flying + // 在转换到Flying之前检查施法者是否存在 if (!m_bHostExist) + { + m_enumState = GfxSkillEventState.enumFinished; + return; + } + + // For skills that require a target, check target existence before starting flight + // For area skills or skills without specific targets, allow flight even if target doesn't exist + // 对于需要目标的技能,在开始飞行前检查目标是否存在 + // 对于区域技能或没有特定目标的技能,即使目标不存在也允许飞行 + if (!m_bTargetExist && m_nTargetID != 0) + { + // Target is required but doesn't exist - finish the event + // 需要目标但目标不存在 - 结束事件 + m_enumState = GfxSkillEventState.enumFinished; + return; + } + + // Transition to Flying state + // 转换到飞行状态 + m_enumState = GfxSkillEventState.enumFlying; + m_pMoveMethod.SetMaxFlyTime(m_dwFlyTimeSpan); + + // Use target position if available, otherwise use host position (for area skills) + // 如果目标位置可用则使用,否则使用施法者位置(用于区域技能) + Vector3 targetPos = m_bTargetExist ? m_vTargetPos : m_vHostPos; + m_pMoveMethod.StartMove(m_vHostPos, targetPos); + + // Fly GFX spawning is handled by CECSkillGfxEvent.Tick() when it detects Wait→Flying transition + // 飞行特效的生成由CECSkillGfxEvent.Tick()在检测到Wait→Flying转换时处理 + } + else if (m_dwCurSpan > m_dwFlyTimeSpan) // 飞行超时 / Flight timeout + { + if (!m_bTargetExist && m_nTargetID != 0) m_enumState = GfxSkillEventState.enumFinished; else { - m_enumState = GfxSkillEventState.enumFlying; - m_pMoveMethod.SetMaxFlyTime(m_dwFlyTimeSpan); - m_pMoveMethod.StartMove(m_vHostPos, m_vTargetPos); - - // Fly GFX spawning is handled by CECSkillGfxEvent.Tick() when it detects Wait→Flying transition - // 飞行特效的生成由CECSkillGfxEvent.Tick()在检测到Wait→Flying转换时处理 + Vector3 hitPos = m_bTargetExist ? GetTargetCenter() : m_pMoveMethod.GetPos(); + HitTarget(hitPos); } } else // enumFlying state / 飞行状态 { if (m_pMoveMethod.TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos)) // 目标被命中 / Target hit - HitTarget(GetTargetCenter()); + { + // Only call GetTargetCenter if target exists and is not destroyed + // 仅在目标存在且未销毁时调用GetTargetCenter + if (m_bTargetExist && m_nTargetID != 0) + { + HitTarget(GetTargetCenter()); + } + else + { + // Target destroyed, hit at last known position or current position + // 目标已销毁,在最后已知位置或当前位置命中 + HitTarget(m_bTargetExist ? m_vTargetPos : m_pMoveMethod.GetPos()); + } + } // Fly GFX transform update is handled by CECSkillGfxEvent.Tick() // 飞行特效的变换更新由CECSkillGfxEvent.Tick()处理 diff --git a/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs index bf07c3e77c..8695b11a50 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs @@ -11,7 +11,7 @@ using System.Text; using ModelRenderer.Scripts.Common; using BrewMonster.Scripts; using UnityEngine; -using System.Threading.Tasks; + using Cysharp.Threading.Tasks; namespace BrewMonster { @@ -114,7 +114,7 @@ namespace BrewMonster // Yield every 10 skills to keep Unity responsive if ((loadedCount + failedCount) % 10 == 0) { - await System.Threading.Tasks.Task.Yield(); + await UniTask.Yield(); } } @@ -162,6 +162,51 @@ namespace BrewMonster // 更新技能特效事件(飞行/命中特效状态机) SkillGfxMan.InstanceSub.Tick(dwDeltaTime); } + +#if UNITY_EDITOR + /// + /// Draw gizmos for skill projectiles in Unity Editor + /// 在Unity编辑器中绘制技能弹道辅助线 + /// + private void OnDrawGizmos() + { + // Always draw gizmos (not just when selected) + // 始终绘制辅助线(不仅在选择时) + int gizmoCount = SkillGfxGizmoDrawer.GetGizmoCount(); + + // Draw test gizmo at origin to verify OnDrawGizmos is working + // 在原点绘制测试辅助线以验证OnDrawGizmos是否工作 + if (gizmoCount == 0 && Time.frameCount % 120 == 0) // Log every 2 seconds when no gizmos + { + // Draw a small test sphere at origin to verify gizmos work + // 在原点绘制小测试球体以验证辅助线是否工作 + Gizmos.color = Color.magenta; + Gizmos.DrawWireSphere(Vector3.zero, 1.0f); + } + + if (gizmoCount > 0) + { + // Only log occasionally to avoid spam + // 仅偶尔记录以避免刷屏 + if (Time.frameCount % 60 == 0) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] OnDrawGizmos: Drawing {gizmoCount} gizmo(s)"); + } + } + SkillGfxGizmoDrawer.DrawGizmos(); + } + + /// + /// Draw gizmos when selected (for debugging) + /// 选择时绘制辅助线(用于调试) + /// + private void OnDrawGizmosSelected() + { + // Also draw when selected for extra visibility + // 选择时也绘制以增加可见性 + SkillGfxGizmoDrawer.DrawGizmos(); + } +#endif bool FileExists(string relativePath) { string fullPath = Path.Combine(Application.streamingAssetsPath, relativePath); @@ -482,7 +527,7 @@ namespace BrewMonster m_bFadeOut = stub.m_bFadeOut; m_bRelScl = stub.m_bRelScl; m_fDefTarScl = stub.m_fDefTarScl; - m_param = stub.m_param; + //m_param = stub.m_param; } // GFX prefab accessors / GFX预制体访问器 @@ -499,12 +544,11 @@ namespace BrewMonster string flyGfxName; string hitGrdGfxName; #endif - public async Task Load(SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) + public async UniTask Load(SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) { flyGfxName = flyGFXPath; hitGfxName = hitGFXPath; hitGrdGfxName = hitGrdGFXPath; -#endif // Load GFX prefabs / 加载GFX预制体 flyGFX = string.IsNullOrEmpty(flyGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName); @@ -587,36 +631,94 @@ namespace BrewMonster { bool bCastInTargets = false; + BMLogger.LogError($"[SKILL_GFX_DEBUG] Composer.Play: host={nHostID}, castTarget={nCastTargetID}, targets={(targets?.Count ?? 0)}, isGoblin={bIsGoblinSkill}"); + // Determine GFX names from loaded prefabs / 从已加载的预制体确定GFX名称 string szFly = flyGFX != null ? flyGfxName : null; string szHit = hitGFX != null ? hitGfxName : null; + BMLogger.LogError($"[SKILL_GFX_DEBUG] Composer.Play: flyGFX={(flyGFX != null ? "LOADED" : "NULL")}, hitGFX={(hitGFX != null ? "LOADED" : "NULL")}, szFly={szFly ?? "NULL"}, szHit={szHit ?? "NULL"}"); + // TODO Phase 2: Optimization checks / 第二阶段:优化检查 // if (!CECOptimize.Instance.GetGFX().CanShowFly(nHostID)) szFly = null; // if (!CECOptimize.Instance.GetGFX().CanShowHit(nHostID)) szHit = null; + // Validate targets exist before processing (filter out destroyed targets) + // 在处理前验证目标是否存在(过滤已销毁的目标) if (targets != null && targets.Count > 0) { + var validTargets = new List(); + foreach (var tar in targets) + { + if (ValidateTargetExists(tar.idTarget)) + { + validTargets.Add(tar); + } + else + { + BMLogger.LogWarning($"[SKILL_GFX_DEBUG] Composer.Play: Target {tar.idTarget} is destroyed, skipping"); + } + } + + if (validTargets.Count == 0) + { + BMLogger.LogWarning($"[SKILL_GFX_DEBUG] Composer.Play: All targets destroyed, skipping GFX"); + return; + } + + int originalCount = targets.Count; + targets = validTargets; + BMLogger.LogError($"[SKILL_GFX_DEBUG] Composer.Play: Processing {targets.Count} valid targets (filtered from {originalCount})"); + for (int i = 0; i < targets.Count; i++) { var tar = targets[i]; if (nCastTargetID == tar.idTarget) bCastInTargets = true; + BMLogger.LogError($"[SKILL_GFX_DEBUG] Composer.Play: Calling AddOneTarget for target {i}/{targets.Count}, id={tar.idTarget}"); AddOneTarget(nCastTargetID, nHostID, szFly, szHit, tar, i == 0, bIsGoblinSkill); } } if (nCastTargetID != 0 && !bCastInTargets) { + // Validate cast target exists before adding + // 在添加前验证施法目标是否存在 + if (!ValidateTargetExists(nCastTargetID)) + { + BMLogger.LogWarning($"[SKILL_GFX_DEBUG] Composer.Play: Cast target {nCastTargetID} is destroyed, skipping"); + return; + } + TARGET_DATA tar = default; tar.idTarget = nCastTargetID; tar.dwModifier = 0; + BMLogger.LogError($"[SKILL_GFX_DEBUG] Composer.Play: Cast target not in targets list, adding separately"); AddOneTarget(nCastTargetID, nHostID, szFly, szHit, tar, false, bIsGoblinSkill); } } + + /// + /// Validate that a target exists and its GameObject is not destroyed + /// 验证目标存在且其GameObject未销毁 + /// + private bool ValidateTargetExists(int idTarget) + { + if (GPDataTypeHelper.ISNPCID(idTarget)) + { + var npc = EC_ManMessageMono.Instance?.CECNPCMan?.GetNPCFromAll(idTarget); + return npc != null && npc.gameObject != null; + } + else if (GPDataTypeHelper.ISPLAYERID(idTarget)) + { + var player = EC_ManMessageMono.Instance?.GetECManPlayer?.GetPlayer(idTarget); + return player != null && player.gameObject != null; + } + return false; + } public void AddOneTarget( int nCastTargetID, int nHostID, @@ -630,6 +732,8 @@ namespace BrewMonster float fScale; bool bReverse; + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneTarget: castTarget={nCastTargetID}, host={nHostID}, target={tar.idTarget}, fly={szFly ?? "NULL"}, hit={szHit ?? "NULL"}, first={bFirst}"); + // 根据目标模式决定Host和Target的映射 / Determine Host and Target mapping based on target mode switch (m_TargetMode) { @@ -649,11 +753,13 @@ namespace BrewMonster break; } + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneTarget: After mapping - _Host={_Host}, _Target={_Target}, reverse={bReverse}, moveMode={m_MoveMode}, flyTime={m_dwFlyTime}"); + // 计算缩放 / Calculate scale - if (m_bRelScl) + /* if (m_bRelScl) fScale = SkillGfxMan.InstanceSub.GetTargetScale(_Target) / m_fDefTarScl * m_fHitGfxScale; else - fScale = m_fHitGfxScale; + fScale = m_fHitGfxScale;*/ // 根据目标类型决定是否显示特效 / Determine whether to show effects based on target type if ((nCastTargetID != 0 && tar.idTarget != nCastTargetID) @@ -669,6 +775,13 @@ namespace BrewMonster } } + if (m_pSkillGfxMan == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneTarget: m_pSkillGfxMan is NULL - cannot add event!"); + return; + } + + BMLogger.LogError($"[SKILL_GFX_DEBUG] AddOneTarget: Calling m_pSkillGfxMan.AddSkillGfxEvent(host={_Host}, target={_Target}, fly={szFly ?? "NULL"}, hit={szHit ?? "NULL"}, flyTime={m_dwFlyTime}, moveMode={m_MoveMode})"); // 调用GFX管理器添加技能特效事件 / Call GFX manager to add skill GFX event m_pSkillGfxMan.AddSkillGfxEvent( @@ -683,8 +796,6 @@ namespace BrewMonster (int)m_FlyCluster.m_ulCount, m_FlyCluster.m_dwInterv, m_param, - m_fFlyGfxScale, - fScale, tar.dwModifier, m_bOneHit, m_bFadeOut, @@ -789,6 +900,8 @@ public class CECAttackEvent m_bDoFired = true; + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: skill={m_idSkill}, host={m_idHost}, castTarget={m_idCastTarget}, targets={m_targets.Count}, section={m_nSkillSection}"); + if (GPDataTypeHelper.ISPLAYERID(m_idHost)) { @@ -803,11 +916,14 @@ public class CECAttackEvent if (pMan != null) { bool isGoblin = ElementSkill.IsGoblinSkill((uint)m_idSkill); + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: Multi-section skill, calling pMan.Play()"); pMan.Play(m_idSkill, m_nSkillSection, m_idHost, m_idCastTarget, m_targets, isGoblin); pComposer = pMan.GetSkillGfxComposer(m_idSkill, m_nSkillSection); + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: Multi-section composer={(pComposer != null ? "FOUND" : "NULL")}"); } else { + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: Multi-section pMan is NULL!"); } } else @@ -816,18 +932,22 @@ public class CECAttackEvent // Get the composer manager var composerMan = m_pManager.GetSkillGfxComposerMan(); + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: composerMan={(composerMan != null ? "FOUND" : "NULL")}"); if (composerMan != null) { + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: Calling composerMan.Play(skill={m_idSkill}, host={m_idHost}, castTarget={m_idCastTarget}, targets={m_targets.Count}, isGoblin={isGoblin})"); if (isGoblin) composerMan.Play(m_idSkill, m_idHost, m_idCastTarget, m_targets, true); else composerMan.Play(m_idSkill, m_idHost, m_idCastTarget, m_targets); pComposer = composerMan.GetSkillGfxComposer(m_idSkill); + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: Composer lookup result={(pComposer != null ? "FOUND" : "NULL")}, flyTime={(pComposer != null ? pComposer.m_dwFlyTime : 0)}"); } else { + BMLogger.LogError($"[SKILL_GFX_DEBUG] DoFire: composerMan is NULL - cannot play skill GFX!"); } } diff --git a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs index e8f47fdbba..28f0feb36d 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs @@ -191,7 +191,9 @@ public class CECNPCMan : IMsgHandler hostplayer.SelectTarget(0); // Remove it from active NPC table - m_NPCTab.Remove(nid); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCLeave: Removing NPC from m_NPCTab - nid={nid}, table size before={m_NPCTab.Count}"); + bool removed = m_NPCTab.Remove(nid); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCLeave: NPC removed={(removed ? "SUCCESS" : "FAILED (not in table)" )}, table size after={m_NPCTab.Count}"); // Forbid reloading npc's resources //QueueNPCUndoLoad(nid, pNPC->GetBornStamp()); @@ -374,13 +376,19 @@ public class CECNPCMan : IMsgHandler } private bool OnMsgNPCInfo(ECMSG msg) { - switch (Convert.ToInt32(msg.dwParam2)) + int commandId = Convert.ToInt32(msg.dwParam2); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Received NPC info message, commandID={commandId}"); + + switch (commandId) { case CommandID.NPC_INFO_LIST: { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC_INFO_LIST"); // msg.dwParam1 chính là buffer chứa placeholder data (không có header cmd_npc_info_list) cmd_npc_info_list pCmd = MemoryMarshal.Read(((byte[])msg.dwParam1).AsSpan()); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_INFO_LIST contains {pCmd.count} NPC(s)"); + int offset = Marshal.OffsetOf("placeholder").ToInt32(); byte[] buffer = (byte[])msg.dwParam1; Span pDataBuf = buffer.AsSpan(offset); @@ -390,6 +398,8 @@ public class CECNPCMan : IMsgHandler // giống const info_npc& Info = *(const info_npc*)pDataBuf; info_npc info = MemoryMarshal.Read(pDataBuf); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC {i + 1}/{pCmd.count} - nid={info.nid}, tid={info.tid}"); + int iSize = info_npc.HEADER_SIZE; if ((info.state & PlayerNPCState.GP_STATE_EXTEND_PROPERTY) != 0) iSize += sizeof(uint) * NumberDWORDsPlayerNPC.OBJECT_EXT_STATE_COUNT; @@ -419,16 +429,20 @@ public class CECNPCMan : IMsgHandler case CommandID.NPC_ENTER_SLICE: { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC_ENTER_SLICE"); var buffer = (byte[])msg.dwParam1; info_npc info = MemoryMarshal.Read(buffer.AsSpan(0, info_npc.HEADER_SIZE)); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_ENTER_SLICE - nid={info.nid}, tid={info.tid}"); NPCEnter(info, false, buffer, info_npc.HEADER_SIZE); break; } case CommandID.NPC_ENTER_WORLD: { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: Processing NPC_ENTER_WORLD"); var buffer = (byte[])msg.dwParam1; info_npc info = MemoryMarshal.Read(buffer.AsSpan(0, info_npc.HEADER_SIZE)); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_ENTER_WORLD - nid={info.nid}, tid={info.tid}"); NPCEnter(info, true, buffer, info_npc.HEADER_SIZE); break; } @@ -492,7 +506,9 @@ public class CECNPCMan : IMsgHandler } // Thêm NPC vào bảng + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCEnter: Adding NPC to m_NPCTab - nid={Info.nid}, tid={Info.tid}, npc={(npc != null ? "created" : "NULL")}"); m_NPCTab[Info.nid] = npc; + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.NPCEnter: NPC added successfully. m_NPCTab[Info.nid] = npc {m_NPCTab[Info.nid].name} NPC(s)"); return true; } // Get NPC by id and optional bornStamp @@ -524,17 +540,28 @@ public class CECNPCMan : IMsgHandler } public CECNPC GetNPCFromAll(int nid) { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: Looking for NPC nid={nid}, m_NPCTab.Count={m_NPCTab.Count}"); + CECNPC pNPC = GetNPC(nid); - if (pNPC) - return pNPC; - - // Search from disappear array ? - /*for (int i = 0; i < m_aDisappearNPCs.GetSize(); i++) + if (pNPC != null) { - CECNPC* pNPC = m_aDisappearNPCs[i]; - if (pNPC->GetNPCID() == nid) - return pNPC; - }*/ + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: GetNPC returned {pNPC.name}"); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: NPC {nid} FOUND in m_NPCTab"); + return pNPC; + } + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: NPC {nid} NOT FOUND in m_NPCTab! Available NPC IDs: {string.Join(", ", m_NPCTab.Keys)}"); + + // Search from disappear array - provides grace period for GFX events (matches C++ behavior) + for (int i = 0; i < m_aDisappearNPCs.Count; i++) + { + CECNPC pDisappearNPC = m_aDisappearNPCs[i]; + if (pDisappearNPC != null && pDisappearNPC.gameObject != null && pDisappearNPC.GetNPCID() == nid) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: NPC {nid} FOUND in m_aDisappearNPCs"); + return pDisappearNPC; // Return NPC even if removed from main table + } + } return null; } diff --git a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs index 37db22d9c6..4c6d574e2d 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs @@ -81,39 +81,63 @@ namespace BrewMonster { Vector3 vTargetCenter = Vector3.zero; - // if composer has been set - // use the composer's parameter to make the hook information affect. - // 如果已设置组合器,使用组合器的参数来影响挂点信息 - if (GetComposer() != null) + try { - A3DSkillGfxComposer pComposer = GetComposer(); - _get_pos_by_id( - m_pPlayerMan, - m_pNPCMan, - (int)m_nTargetID, - out vTargetCenter, - pComposer.m_HitPos.HitPos, - false, - pComposer.m_HitPos.szHook, - pComposer.m_HitPos.bRelHook, - pComposer.m_HitPos.vOffset, - pComposer.m_HitPos.szHanger, - pComposer.m_HitPos.bChildHook); + // if composer has been set + // use the composer's parameter to make the hook information affect. + // 如果已设置组合器,使用组合器的参数来影响挂点信息 + if (GetComposer() != null) + { + A3DSkillGfxComposer pComposer = GetComposer(); + bool success = get_pos_by_id( + m_pPlayerMan, + m_pNPCMan, + (int)m_nTargetID, + out vTargetCenter, + pComposer.m_HitPos.HitPos, + false, + pComposer.m_HitPos.szHook, + pComposer.m_HitPos.bRelHook, + pComposer.m_HitPos.vOffset, + pComposer.m_HitPos.szHanger, + pComposer.m_HitPos.bChildHook); + + if (!success) + { + // Return last known position or zero if target is destroyed + // 如果目标已销毁,返回最后已知位置或零 + return m_vTargetPos != Vector3.zero ? m_vTargetPos : Vector3.zero; + } + } + else + { + bool success = get_pos_by_id( + m_pPlayerMan, + m_pNPCMan, + (int)m_nTargetID, + out vTargetCenter, + GfxHitPos.enumHitCenter, + false, + null, + false, + Vector3.zero, + null, + false); + + if (!success) + { + // Return last known position or zero if target is destroyed + // 如果目标已销毁,返回最后已知位置或零 + return m_vTargetPos != Vector3.zero ? m_vTargetPos : Vector3.zero; + } + } } - else + catch (System.Exception ex) { - _get_pos_by_id( - m_pPlayerMan, - m_pNPCMan, - (int)m_nTargetID, - out vTargetCenter, - GfxHitPos.enumHitCenter, - false, - null, - false, - Vector3.zero, - null, - false); + BMLogger.LogError($"[SKILL_GFX_DEBUG] GetTargetCenter: Exception accessing target {m_nTargetID} - {ex.Message}"); + // Return last known position or zero + // 返回最后已知位置或零 + return m_vTargetPos != Vector3.zero ? m_vTargetPos : Vector3.zero; } return vTargetCenter; @@ -125,6 +149,9 @@ namespace BrewMonster /// public override void Tick(uint dwDeltaTime) { + // Track state before base.Tick() to detect transitions / 在base.Tick()前记录状态以检测转换 + GfxSkillEventState prevState = m_enumState; + // Update host and target positions / 更新施法者和目标位置 if (GetComposer() != null) { @@ -141,7 +168,7 @@ namespace BrewMonster pTargetPos = m_pComposer.m_FlyEndPos; } - m_bHostExist = _get_pos_by_id( + m_bHostExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nHostID, @@ -154,7 +181,7 @@ namespace BrewMonster pHostPos.szHanger, pHostPos.bChildHook); - m_bTargetExist = _get_pos_by_id( + m_bTargetExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nTargetID, @@ -171,7 +198,7 @@ namespace BrewMonster } else { - m_bHostExist = _get_pos_by_id( + m_bHostExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nHostID, @@ -184,7 +211,7 @@ namespace BrewMonster null, false); - m_bTargetExist = _get_pos_by_id( + m_bTargetExist = get_pos_by_id( m_pPlayerMan, m_pNPCMan, (int)m_nTargetID, @@ -198,17 +225,75 @@ namespace BrewMonster false); } - // Track state before base.Tick() to detect transitions / 在base.Tick()前记录状态以检测转换 - GfxSkillEventState prevState = m_enumState; + // Log target existence issues with more detail + // 记录目标存在问题的更多详细信息 + if (!m_bTargetExist && m_nTargetID != 0) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: WARNING - Target {m_nTargetID} does not exist (host={m_nHostID}, exist={m_bHostExist}, pos={m_vHostPos}), state={prevState}. Target may have been destroyed or ID is invalid."); + } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: host={m_nHostID} (exist={m_bHostExist}, pos={m_vHostPos}), target={m_nTargetID} (exist={m_bTargetExist}, pos={m_vTargetPos}), state={prevState}"); + } + base.Tick(dwDeltaTime); + // Log state transitions + if (prevState != m_enumState) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: State transition {prevState} → {m_enumState}, host={m_nHostID}, target={m_nTargetID}"); + } + // Spawn fly GFX when entering Flying state / 进入飞行状态时生成飞行特效 if (prevState == GfxSkillEventState.enumWait && m_enumState == GfxSkillEventState.enumFlying) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: Transitioning to Flying, calling SpawnFlyGfx()"); + + // Register for gizmo drawing BEFORE spawning (so we capture the initial position) + // 在生成前注册用于辅助线绘制(以便捕获初始位置) +#if UNITY_EDITOR + Vector3 currentPos = m_pMoveMethod.GetPos(); + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: Registering gizmo - hostPos={m_vHostPos}, targetPos={m_vTargetPos}, currentPos={currentPos}, hostExist={m_bHostExist}, targetExist={m_bTargetExist}"); + + // Only register if positions are valid (not zero) + // 仅在位置有效(非零)时注册 + if (m_vHostPos.sqrMagnitude > 0.01f && m_vTargetPos.sqrMagnitude > 0.01f) + { + SkillGfxGizmoDrawer.RegisterProjectile(m_nHostID, m_nTargetID, m_vHostPos, m_vTargetPos, m_pMoveMethod.GetMode()); + } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: Gizmo registration SKIPPED - invalid positions!"); + } +#endif + SpawnFlyGfx(); + } // Update fly GFX transform during Flying / 飞行期间更新飞行特效变换 if (m_enumState == GfxSkillEventState.enumFlying) + { UpdateFlyGfxTransform(); + + // Update gizmo position / 更新辅助线位置 +#if UNITY_EDITOR + Vector3 currentPos = m_pMoveMethod.GetPos(); + // Only update if position is valid + // 仅在位置有效时更新 + if (currentPos.sqrMagnitude > 0.01f) + { + SkillGfxGizmoDrawer.UpdateProjectile(m_nHostID, m_nTargetID, currentPos, m_vTargetPos); + } +#endif + } + + // Remove gizmo when hit or finished / 命中或完成时移除辅助线 + if (m_enumState == GfxSkillEventState.enumHit || m_enumState == GfxSkillEventState.enumFinished) + { +#if UNITY_EDITOR + SkillGfxGizmoDrawer.RemoveProjectile(m_nHostID, m_nTargetID); +#endif + } } /// @@ -217,6 +302,7 @@ namespace BrewMonster /// protected override void HitTarget(Vector3 vTarget) { + BMLogger.LogError($"[SKILL_GFX_DEBUG] HitTarget: Entry, host={m_nHostID}, target={m_nTargetID}, pos={vTarget}"); base.HitTarget(vTarget); DestroyFlyGfx(); SpawnHitGfx(vTarget); @@ -234,15 +320,36 @@ namespace BrewMonster /// private void SpawnFlyGfx() { - if (m_pComposer == null) return; + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: Entry, host={m_nHostID}, target={m_nTargetID}"); + + if (m_pComposer == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: m_pComposer is NULL - cannot spawn fly GFX!"); + return; + } + GameObject prefab = m_pComposer.GetFlyGFX(); - if (prefab == null) return; + if (prefab == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: Fly GFX prefab is NULL - cannot spawn!"); + return; + } Vector3 pos = m_pMoveMethod.GetPos(); Vector3 dir = m_pMoveMethod.GetMoveDir(); Quaternion rot = dir.sqrMagnitude > 1e-4f ? Quaternion.LookRotation(dir) : Quaternion.identity; + + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: Instantiating prefab={prefab.name} at pos={pos}, dir={dir}"); m_flyGfxInstance = GameObject.Instantiate(prefab, pos, rot); - BMLogger.Log($"[GFX_FLOW] SpawnFlyGfx at {pos}, prefab={prefab.name}"); + + if (m_flyGfxInstance != null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: SUCCESS - Fly GFX spawned at {pos}, instance={m_flyGfxInstance.name}"); + } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: FAILED - Instantiate returned NULL!"); + } } /// @@ -277,9 +384,20 @@ namespace BrewMonster /// private void SpawnHitGfx(Vector3 vTarget) { - if (m_pComposer == null) return; + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: Entry, host={m_nHostID}, target={m_nTargetID}, pos={vTarget}"); + + if (m_pComposer == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: m_pComposer is NULL - cannot spawn hit GFX!"); + return; + } + GameObject prefab = m_pComposer.GetHitGFX(); - if (prefab == null) return; + if (prefab == null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: Hit GFX prefab is NULL - cannot spawn!"); + return; + } Quaternion rot = Quaternion.identity; if (m_bHostExist) @@ -289,9 +407,19 @@ namespace BrewMonster if (dir.sqrMagnitude > 1e-6f) rot = Quaternion.LookRotation(dir); } + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: Instantiating prefab={prefab.name} at pos={vTarget}"); m_hitGfxInstance = GameObject.Instantiate(prefab, vTarget, rot); + + if (m_hitGfxInstance != null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: SUCCESS - Hit GFX spawned at {vTarget}, instance={m_hitGfxInstance.name}"); + } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: FAILED - Instantiate returned NULL!"); + } + GameObject.Destroy(m_hitGfxInstance, 3.0f); // auto-cleanup / 自动清理 - BMLogger.Log($"[GFX_FLOW] SpawnHitGfx at {vTarget}, prefab={prefab.name}"); } /// @@ -342,7 +470,7 @@ namespace BrewMonster /// Get position by ID (player or NPC) /// 根据ID获取位置(玩家或NPC) /// - private static bool _get_pos_by_id( + private static bool get_pos_by_id( EC_ManPlayer pPlayerMan, CECNPCMan pNPCMan, int nID, @@ -357,47 +485,106 @@ namespace BrewMonster { vPos = Vector3.zero; + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: Entry - nID={nID}, isPlayerID={GPDataTypeHelper.ISPLAYERID(nID)}, isNPCID={GPDataTypeHelper.ISNPCID(nID)}, pPlayerMan={(pPlayerMan != null ? "exists" : "NULL")}, pNPCMan={(pNPCMan != null ? "exists" : "NULL")}"); + if (GPDataTypeHelper.ISPLAYERID(nID)) { + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: ID {nID} is a PLAYER ID"); CECPlayer pPlayer = pPlayerMan?.GetPlayer(nID); - if (pPlayer != null) + // Check if player exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pPlayer != null && pPlayer.gameObject != null) { - if (bIsGoblinSkill) + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: Player {nID} found, getting position"); { - // TODO: Handle goblin skill position - // if (pPlayer->GetGoblinModel()) - // vPos = pPlayer->GetGoblinModel()->GetModel()->GetModelAABB().Center; - // else - // return false; - return false; + if (bIsGoblinSkill) + { + // TODO: Handle goblin skill position + // if (pPlayer->GetGoblinModel()) + // vPos = pPlayer->GetGoblinModel()->GetModel()->GetModelAABB().Center; + // else + // return false; + return false; + } + else + { + // currently hook does not affect the Goblin Skill + // 目前挂点不影响小精灵技能 + while (true) + { + if (string.IsNullOrEmpty(szHook)) + break; + + // TODO: Get player model and hook position + /*CECModel pModel = pPlayer->GetPlayerModel(); + if (!pModel) + break; + + if (szHanger && bChildHook) + pModel = pModel->GetChildModel(szHanger); + + if (!pModel) + break; + + A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); + A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); + + if (!pHook) + break; + + if (bRelHook) + vPos = pHook->GetAbsoluteTM() * pOffset; + else + { + vPos = pSkin->GetAbsoluteTM() * pOffset; + vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + } + + return true;*/ + break; + } + + if (HitPos == GfxHitPos.enumHitBottom) + { + vPos = pPlayer.GetPosVector3(); + } + else + { + // TODO: Get player AABB + // const A3DAABB& aabb = pPlayer->GetPlayerAABB(); + // vPos = aabb.Center; + // vPos.y += aabb.Extents.y * .5f; + vPos = pPlayer.GetPosVector3(); + vPos.y += 1.0f; // Default height offset / 默认高度偏移 + } + } + + return true; } - else + } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: Player {nID} NOT FOUND or GameObject destroyed in pPlayerMan (pPlayerMan is {(pPlayerMan != null ? "not null" : "NULL")})"); + } + } + else if (GPDataTypeHelper.ISNPCID(nID)) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: ID {nID} is an NPC ID"); + CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); + + // Check if NPC exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pNPC != null && pNPC.gameObject != null) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: NPC {nID} found, getting position"); { - // currently hook does not affect the Goblin Skill - // 目前挂点不影响小精灵技能 while (true) { - if (string.IsNullOrEmpty(szHook)) - break; - - // TODO: Get player model and hook position - /*CECModel pModel = pPlayer->GetPlayerModel(); - if (!pModel) - break; - - if (szHanger && bChildHook) - pModel = pModel->GetChildModel(szHanger); - - if (!pModel) - break; - - A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); - A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); - + // TODO: Get NPC hook position + /*A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); if (!pHook) break; + A3DSkinModel *pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); if (bRelHook) vPos = pHook->GetAbsoluteTM() * pOffset; else @@ -412,66 +599,32 @@ namespace BrewMonster if (HitPos == GfxHitPos.enumHitBottom) { - vPos = pPlayer.GetPosVector3(); + vPos = pNPC.GetPosVector3(); } else { - // TODO: Get player AABB - // const A3DAABB& aabb = pPlayer->GetPlayerAABB(); + // TODO: Get NPC AABB + // const A3DAABB& aabb = pNPC->GetPickAABB(); // vPos = aabb.Center; // vPos.y += aabb.Extents.y * .5f; - vPos = pPlayer.GetPosVector3(); + vPos = pNPC.GetPosVector3(); vPos.y += 1.0f; // Default height offset / 默认高度偏移 } + + return true; } - - return true; } - } - else if (GPDataTypeHelper.ISNPCID(nID)) - { - CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); - - if (pNPC != null) + else { - while (true) - { - // TODO: Get NPC hook position - /*A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); - if (!pHook) - break; - - A3DSkinModel *pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); - if (bRelHook) - vPos = pHook->GetAbsoluteTM() * pOffset; - else - { - vPos = pSkin->GetAbsoluteTM() * pOffset; - vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); - } - - return true;*/ - break; - } - - if (HitPos == GfxHitPos.enumHitBottom) - { - vPos = pNPC.GetPosVector3(); - } - else - { - // TODO: Get NPC AABB - // const A3DAABB& aabb = pNPC->GetPickAABB(); - // vPos = aabb.Center; - // vPos.y += aabb.Extents.y * .5f; - vPos = pNPC.GetPosVector3(); - vPos.y += 1.0f; // Default height offset / 默认高度偏移 - } - - return true; + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: NPC {nID} NOT FOUND or GameObject destroyed in pNPCMan (pNPCMan is {(pNPCMan != null ? "not null" : "NULL")})"); } } + else + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: ID {nID} is NEITHER a player ID nor an NPC ID! This is likely an invalid ID."); + } + BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: Returning FALSE for ID {nID}"); return false; } @@ -493,7 +646,8 @@ namespace BrewMonster { CECPlayer pPlayer = pPlayerMan?.GetPlayer(nId); - if (pPlayer != null) + // Check if player exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pPlayer != null && pPlayer.gameObject != null) { // TODO: Get player direction and up // vDir = pPlayer->GetDir(); @@ -507,7 +661,8 @@ namespace BrewMonster { CECNPC pNPC = pNPCMan?.GetNPCFromAll(nId); - if (pNPC != null) + // Check if NPC exists AND GameObject is not destroyed (Unity's "fake null" handling) + if (pNPC != null && pNPC.gameObject != null) { // TODO: Get NPC direction and up // vDir = pNPC->GetDir(); @@ -729,6 +884,11 @@ namespace BrewMonster /// public bool Tick(uint dwDeltaTime) { + if (m_EventLst.Count > 0) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] SkillGfxMan.Tick: Processing {m_EventLst.Count} active event(s), deltaTime={dwDeltaTime}ms"); + } + var node = m_EventLst.First; while (node != null) @@ -825,6 +985,11 @@ namespace BrewMonster /// Add skill GFX event /// 添加技能特效事件 /// + /// + /// Convenience overload for weapon/melee attacks (no composer). + /// Scale is not needed — Unity Particle Systems handle their own scale. + /// 武器/近战攻击的便捷重载(无组合器)。不需要缩放 — Unity粒子系统自己处理缩放。 + /// public bool AddSkillGfxEvent( int nHostID, int nTargetID, @@ -836,8 +1001,6 @@ namespace BrewMonster int nFlyGfxCount = 1, uint dwInterval = 0, GFX_SKILL_PARAM? param = null, - float fFlyGfxScale = 1.0f, - float fHitGfxScale = 1.0f, uint dwModifier = 0) { return m_GfxMan.AddSkillGfxEvent( @@ -852,8 +1015,6 @@ namespace BrewMonster nFlyGfxCount, dwInterval, param ?? default, - fFlyGfxScale, - fHitGfxScale, dwModifier, false, // bOnlyOneHit false, // bFadeOut diff --git a/Assets/PerfectWorld/Scripts/Managers/SkillGfxGizmoDrawer.cs b/Assets/PerfectWorld/Scripts/Managers/SkillGfxGizmoDrawer.cs new file mode 100644 index 0000000000..d9849baadc --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/SkillGfxGizmoDrawer.cs @@ -0,0 +1,203 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Helper class to track and draw gizmos for skill GFX projectiles. + /// Draws projectile path from host to target, auto-removes after 10 seconds. + /// 用于跟踪和绘制技能GFX弹道辅助线的辅助类。绘制从施法者到目标的弹道路径,10秒后自动移除。 + /// + public static class SkillGfxGizmoDrawer + { + /// + /// Gizmo data for a single projectile + /// 单个弹道的辅助线数据 + /// + private class GizmoData + { + public Vector3 startPos; // Host position / 施法者位置 + public Vector3 currentPos; // Current projectile position / 当前弹道位置 + public Vector3 targetPos; // Target position / 目标位置 + public float createTime; // Time when created / 创建时间 + public long hostID; // Host ID for identification / 施法者ID + public long targetID; // Target ID for identification / 目标ID + public GfxMoveMode moveMode; // Movement mode / 移动模式 + } + + private static readonly List m_GizmoList = new List(); + private const float GIZMO_LIFETIME = 10.0f; // 10 seconds / 10秒 + + /// + /// Register a projectile for gizmo drawing + /// 注册一个弹道用于辅助线绘制 + /// + public static void RegisterProjectile(long hostID, long targetID, Vector3 startPos, Vector3 targetPos, GfxMoveMode moveMode) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] Gizmo: Registering projectile host={hostID}, target={targetID}, start={startPos}, target={targetPos}, mode={moveMode}"); + + var gizmo = new GizmoData + { + startPos = startPos, + currentPos = startPos, + targetPos = targetPos, + createTime = Time.time, + hostID = hostID, + targetID = targetID, + moveMode = moveMode + }; + m_GizmoList.Add(gizmo); + BMLogger.LogError($"[SKILL_GFX_DEBUG] Gizmo: Registered! Total gizmos={m_GizmoList.Count}"); + } + + /// + /// Update projectile position + /// 更新弹道位置 + /// + public static void UpdateProjectile(long hostID, long targetID, Vector3 currentPos, Vector3 targetPos) + { + for (int i = m_GizmoList.Count - 1; i >= 0; i--) + { + var gizmo = m_GizmoList[i]; + if (gizmo.hostID == hostID && gizmo.targetID == targetID) + { + gizmo.currentPos = currentPos; + gizmo.targetPos = targetPos; // Update target in case it moves + return; + } + } + } + + /// + /// Remove projectile gizmo + /// 移除弹道辅助线 + /// + public static void RemoveProjectile(long hostID, long targetID) + { + for (int i = m_GizmoList.Count - 1; i >= 0; i--) + { + var gizmo = m_GizmoList[i]; + if (gizmo.hostID == hostID && gizmo.targetID == targetID) + { + m_GizmoList.RemoveAt(i); + return; + } + } + } + + /// + /// Draw all gizmos (called from OnDrawGizmos) + /// 绘制所有辅助线(从OnDrawGizmos调用) + /// + public static void DrawGizmos() + { + if (m_GizmoList.Count == 0) + return; // No gizmos to draw / 没有辅助线要绘制 + + float currentTime = Time.time; + + // Remove expired gizmos and draw active ones + // 移除过期的辅助线并绘制活动的 + for (int i = m_GizmoList.Count - 1; i >= 0; i--) + { + var gizmo = m_GizmoList[i]; + float age = currentTime - gizmo.createTime; + + // Remove if expired + // 如果过期则移除 + if (age > GIZMO_LIFETIME) + { + m_GizmoList.RemoveAt(i); + continue; + } + + // Calculate fade alpha (fade out in last 2 seconds) + // 计算淡出透明度(最后2秒淡出) + float alpha = age > (GIZMO_LIFETIME - 2.0f) + ? 1.0f - ((age - (GIZMO_LIFETIME - 2.0f)) / 2.0f) + : 1.0f; + + // Draw projectile path + // 绘制弹道路径 + Gizmos.color = GetColorForMoveMode(gizmo.moveMode, alpha); + + // Draw line from start to current position (trail) + // 绘制从起点到当前位置的线(轨迹) + if (Vector3.Distance(gizmo.startPos, gizmo.currentPos) > 0.01f) + { + Gizmos.DrawLine(gizmo.startPos, gizmo.currentPos); + } + + // Draw line from current position to target (remaining path) + // 绘制从当前位置到目标的线(剩余路径) + Gizmos.color = GetColorForMoveMode(gizmo.moveMode, alpha * 0.5f); // Lighter for remaining path + if (Vector3.Distance(gizmo.currentPos, gizmo.targetPos) > 0.01f) + { + Gizmos.DrawLine(gizmo.currentPos, gizmo.targetPos); + } + + // Draw sphere at current position (larger for visibility) + // 在当前位置绘制球体(更大以便可见) + Gizmos.color = GetColorForMoveMode(gizmo.moveMode, alpha); + Gizmos.DrawSphere(gizmo.currentPos, 0.5f); // Increased from 0.2f to 0.5f + + // Draw wire sphere at target (larger for visibility) + // 在目标位置绘制线框球体(更大以便可见) + Gizmos.color = GetColorForMoveMode(gizmo.moveMode, alpha * 0.5f); + Gizmos.DrawWireSphere(gizmo.targetPos, 0.5f); // Increased from 0.3f to 0.5f + + // Draw wire sphere at start position + // 在起始位置绘制线框球体 + Gizmos.color = GetColorForMoveMode(gizmo.moveMode, alpha * 0.3f); + Gizmos.DrawWireSphere(gizmo.startPos, 0.3f); + } + } + + /// + /// Get color for movement mode + /// 根据移动模式获取颜色 + /// + private static Color GetColorForMoveMode(GfxMoveMode mode, float alpha) + { + Color baseColor; + switch (mode) + { + case GfxMoveMode.enumLinearMove: + baseColor = Color.yellow; // Yellow for linear + break; + case GfxMoveMode.enumOnTarget: + baseColor = Color.red; // Red for instant hit + break; + case GfxMoveMode.enumParabolicMove: + baseColor = Color.green; // Green for parabolic + break; + case GfxMoveMode.enumMissileMove: + baseColor = Color.cyan; // Cyan for missile + break; + default: + baseColor = Color.white; + break; + } + baseColor.a = alpha; + return baseColor; + } + + /// + /// Clear all gizmos + /// 清除所有辅助线 + /// + public static void ClearAll() + { + m_GizmoList.Clear(); + } + + /// + /// Get current gizmo count (for debugging) + /// 获取当前辅助线数量(用于调试) + /// + public static int GetGizmoCount() + { + return m_GizmoList.Count; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Managers/SkillGfxGizmoDrawer.cs.meta b/Assets/PerfectWorld/Scripts/Managers/SkillGfxGizmoDrawer.cs.meta new file mode 100644 index 0000000000..413d51cbfc --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/SkillGfxGizmoDrawer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c17eed6528c112645895f705535a0196 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Skills/skill.cs b/Assets/PerfectWorld/Scripts/Skills/skill.cs index ee08a1332f..6ad1855d9f 100644 --- a/Assets/PerfectWorld/Scripts/Skills/skill.cs +++ b/Assets/PerfectWorld/Scripts/Skills/skill.cs @@ -153,7 +153,6 @@ namespace BrewMonster.Scripts.Skills public const int MIN_LEVEL = 1; public const int MAX_LEVEL = 10; - // Base info public uint id; // Ψһ���ֱ�ʶ // Unique identifier public int cls; // ְҵ // Class/Profession public string name; // �������� // Skill name diff --git a/Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs b/Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs index 139847efde..4f561e1c04 100644 --- a/Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs +++ b/Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Threading.Tasks; +using Cysharp.Threading.Tasks; using BrewMonster.Network; using BrewMonster.Scripts; using BrewMonster.Scripts.Skills; @@ -20,7 +20,7 @@ namespace BrewMonster var composer = new A3DSkillGfxComposer(); // WARNING: .Result blocks the main thread - this can cause freezing! - if (!composer.Load(skillStub, flyGFXPath, hitGrdGFXPath, hitGFXPath).Result) + if (composer.Load(skillStub, flyGFXPath, hitGrdGFXPath, hitGFXPath).Status != UniTaskStatus.Succeeded) { // failed to load return false; @@ -37,7 +37,7 @@ namespace BrewMonster /// /// Async version of LoadOneComposer that doesn't block the main thread. /// - public async Task LoadOneComposerAsync(int nSkillID, SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) + public async UniTask LoadOneComposerAsync(int nSkillID, SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath) { if (m_ComposerMap.ContainsKey(nSkillID)) return false; @@ -71,29 +71,41 @@ namespace BrewMonster m_ComposerMap.Clear(); } - public void Play( - int nSkillID, - int nHostID, - int nCastTargetID, - List Targets, - bool bIsGoblinSkill = false) + public void Play( + int nSkillID, + int nHostID, + int nCastTargetID, + List Targets, + bool bIsGoblinSkill = false) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] ComposerMan.Play: skill={nSkillID}, host={nHostID}, castTarget={nCastTargetID}, targets={(Targets?.Count ?? 0)}, isGoblin={bIsGoblinSkill}"); + + if (!m_ComposerMap.TryGetValue(nSkillID, out var composer) || composer == null) { - if (!m_ComposerMap.TryGetValue(nSkillID, out var composer) || composer == null) - return; - - // Forward to composer (stubbed for now if Play not implemented) - try - { - // Provide a minimal default fly time estimation behavior when composer has no fly time configured - // Real effect triggering should be implemented inside composer.Play - composer.Play(nHostID, nCastTargetID, Targets, bIsGoblinSkill); - } - catch (System.MissingMethodException) - { - BMLogger.LogWarning("A3DSkillGfxComposer.Play is not implemented yet."); - } + BMLogger.LogError($"[SKILL_GFX_DEBUG] ComposerMan.Play: Composer NOT FOUND for skill {nSkillID} (map has {m_ComposerMap.Count} composers)"); + return; } + BMLogger.LogError($"[SKILL_GFX_DEBUG] ComposerMan.Play: Composer FOUND, calling composer.Play()"); + + // Forward to composer (stubbed for now if Play not implemented) + try + { + // Provide a minimal default fly time estimation behavior when composer has no fly time configured + // Real effect triggering should be implemented inside composer.Play + BMLogger.LogError($"[SKILL_GFX_DEBUG] ComposerMan.Play: nCastTargetID={nCastTargetID}; Targets.count={Targets.Count}"); + composer.Play(nHostID, nCastTargetID, Targets, bIsGoblinSkill); + } + catch (System.MissingMethodException) + { + BMLogger.LogWarning("A3DSkillGfxComposer.Play is not implemented yet."); + } + catch (System.Exception ex) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] ComposerMan.Play: Exception in composer.Play() - {ex.Message}"); + } + } + public A3DSkillGfxComposer GetSkillGfxComposer(int skill) { return m_ComposerMap.TryGetValue(skill, out var composer) ? composer : null; diff --git a/Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs b/Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs new file mode 100644 index 0000000000..20535aa39e --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs @@ -0,0 +1,84 @@ +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Straight-line projectile movement. + /// Mirrors C++ CGfxLinearMove exactly (A3DSkillGfxEvent2.cpp:22-52). + /// 直线弹道移动,完全镜像C++ CGfxLinearMove。 + /// + public class CGfxLinearMove : CGfxMoveBase + { + protected float m_fSpeed; + private const float _fly_speed = 20.0f / 1000.0f; // units per ms, same as C++ + + public CGfxLinearMove(GfxMoveMode mode) : base(mode) { } + + /// + /// Initialize movement from host to target. + /// 初始化从施法者到目标的移动。 + /// + public override void StartMove(Vector3 vHost, Vector3 vTarget) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Entry - host={vHost}, target={vTarget}, area={m_bArea}, maxFlyTime={m_dwMaxFlyTime}"); + + if (m_bArea) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Area skill - calculating range and random offset"); + CalcRange((vTarget - vHost).normalized); + m_vPos = vHost + GetRandOff(); + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Area skill - startPos={m_vPos} (host={vHost} + offset)"); + } + else + { + m_vPos = vHost; + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Non-area skill - startPos={m_vPos} (same as host)"); + } + + m_vMoveDir = vTarget - m_vPos; + float fDist = Normalize(ref m_vMoveDir); + float fMax = _fly_speed * m_dwMaxFlyTime; + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Distance={fDist}, maxDistance={fMax} (speed={_fly_speed} * time={m_dwMaxFlyTime}), moveDir={m_vMoveDir}"); + + if (fMax >= fDist) + { + m_fSpeed = _fly_speed; + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Using default speed={m_fSpeed} (distance fits in time)"); + } + else + { + m_fSpeed = fDist / m_dwMaxFlyTime; + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Using calculated speed={m_fSpeed} (distance={fDist} / time={m_dwMaxFlyTime})"); + } + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.StartMove: Complete - pos={m_vPos}, dir={m_vMoveDir}, speed={m_fSpeed}"); + } + + /// + /// Tick movement. Returns true when target is hit. + /// 更新移动。当命中目标时返回true。 + /// + public override bool TickMove(uint dwDeltaTime, Vector3 vHostPos, Vector3 vTargetPos) + { + Vector3 oldPos = m_vPos; + Vector3 vFlyDir = vTargetPos - m_vPos; + float fDist = Normalize(ref vFlyDir); + float fFlyDist = m_fSpeed * dwDeltaTime; + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.TickMove: Entry - oldPos={oldPos}, targetPos={vTargetPos}, deltaTime={dwDeltaTime}, speed={m_fSpeed}, distance={fDist}, flyDist={fFlyDist}"); + + if (fFlyDist >= fDist) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.TickMove: Target HIT! (flyDist={fFlyDist} >= distance={fDist})"); + return true; // target hit / 命中目标 + } + + m_vPos += vFlyDir * fFlyDist; + m_vMoveDir = vFlyDir; + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxLinearMove.TickMove: Moved - newPos={m_vPos}, moveDir={m_vMoveDir}, distanceRemaining={fDist - fFlyDist}"); + return false; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs.meta b/Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs.meta new file mode 100644 index 0000000000..554067196b --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e4107992bd89b5b43a8f8d6e944b2b10 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Vfx/CGfxMoveBase.cs b/Assets/PerfectWorld/Scripts/Vfx/CGfxMoveBase.cs new file mode 100644 index 0000000000..b71e8520ee --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Vfx/CGfxMoveBase.cs @@ -0,0 +1,172 @@ +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Abstract base class for all projectile movement patterns. + /// Mirrors C++ CGfxMoveBase exactly. + /// 所有弹道移动模式的抽象基类,完全镜像C++ CGfxMoveBase。 + /// + public abstract class CGfxMoveBase + { + protected GfxMoveMode m_Mode; + protected GfxHitPos m_HitPos = GfxHitPos.enumHitCenter; + protected Vector3 m_vPos; + protected Vector3 m_vMoveDir; + protected bool m_bOneOfCluser; // C++ spelling kept / 保持C++拼写 + protected uint m_dwMaxFlyTime; + protected bool m_bReverse; + protected bool m_bArea; + protected EmitShape m_Shape; + protected Vector3 m_vSize; + protected Vector3 m_vXRange; + protected Vector3 m_vYRange; + protected Vector3 m_vZRange; + protected float m_fSquare; + protected float m_fSquareH; + + protected CGfxMoveBase(GfxMoveMode mode) + { + m_Mode = mode; + m_HitPos = GfxHitPos.enumHitCenter; + } + + // Pure virtual (= 0) → abstract — subclasses MUST override + // 纯虚函数 → 抽象 — 子类必须重写 + public abstract void StartMove(Vector3 vHost, Vector3 vTarget); + public abstract bool TickMove(uint dwDeltaTime, Vector3 vHostPos, Vector3 vTargetPos); + + // ===== Protected helpers (same as C++ CGfxMoveBase) ===== + // 受保护的辅助方法(与C++ CGfxMoveBase相同) + + /// + /// Normalize vector, return original magnitude. Returns 0 if too small. + /// 归一化向量,返回原始长度。如果太小则返回0。 + /// + protected static float Normalize(ref Vector3 v) + { + float mag = v.magnitude; + if (mag < 1e-6f) { v = Vector3.zero; return 0f; } + v /= mag; + return mag; + } + + /// + /// Calculate emission range vectors based on movement direction. + /// 根据移动方向计算发射范围向量。 + /// + protected void CalcRange(Vector3 vDir) + { + m_vYRange = Vector3.up; + m_vZRange = new Vector3(vDir.x, 0, vDir.z); + if (Normalize(ref m_vZRange) < 0.01f) m_vZRange = Vector3.forward; + m_vXRange = Vector3.Cross(m_vYRange, m_vZRange); + m_vXRange *= m_vSize.x; + m_vYRange *= m_vSize.y; + m_vZRange *= m_vSize.z; + m_fSquare = m_vSize.sqrMagnitude; + m_fSquareH = m_vSize.x * m_vSize.x + m_vSize.z * m_vSize.z; + } + + /// + /// Get random offset based on emission shape. + /// 根据发射形状获取随机偏移。 + /// + protected Vector3 GetRandOff() + { + float x, y, z; + + switch (m_Shape) + { + case EmitShape.enumBox: + x = Random.Range(-1f, 1f); + y = Random.Range(-1f, 1f); + z = Random.Range(-1f, 1f); + break; + + case EmitShape.enumSphere: + // Rejection sampling for uniform distribution in sphere + // 球体内均匀分布的拒绝采样 + do + { + x = Random.Range(-1f, 1f); + y = Random.Range(-1f, 1f); + z = Random.Range(-1f, 1f); + } + while (x * x * m_fSquare / (m_vSize.x * m_vSize.x + 1e-6f) + + y * y * m_fSquare / (m_vSize.y * m_vSize.y + 1e-6f) + + z * z * m_fSquare / (m_vSize.z * m_vSize.z + 1e-6f) > m_fSquare); + break; + + case EmitShape.enumCylinder: + // Uniform in horizontal circle, uniform in Y + // 水平圆内均匀分布,Y轴均匀分布 + do + { + x = Random.Range(-1f, 1f); + z = Random.Range(-1f, 1f); + } + while (x * x * m_fSquareH / (m_vSize.x * m_vSize.x + 1e-6f) + + z * z * m_fSquareH / (m_vSize.z * m_vSize.z + 1e-6f) > m_fSquareH); + y = Random.Range(-1f, 1f); + break; + + default: + x = y = z = 0; + break; + } + + return m_vXRange * x + m_vYRange * y + m_vZRange * z; + } + + // ===== Public accessors (non-virtual, same as C++) ===== + // 公共访问器(非虚函数,与C++相同) + + public GfxMoveMode GetMode() { return m_Mode; } + public GfxHitPos GetHitPos() { return m_HitPos; } + public Vector3 GetPos() { return m_vPos; } + public Vector3 GetMoveDir() { return m_vMoveDir; } + public bool IsReverse() { return m_bReverse; } + public void SetReverse(bool bReverse) { m_bReverse = bReverse; } + public void SetIsCluster(bool bCluster) { m_bOneOfCluser = bCluster; } + public void SetMaxFlyTime(uint dwTime) { m_dwMaxFlyTime = dwTime; } + public void SetRange(Vector3 vSize) { m_vSize = vSize; } + + /// + /// Virtual — subclasses can override for custom param handling. + /// 虚函数 — 子类可以重写以自定义参数处理。 + /// + public virtual void SetParam(GFX_SKILL_PARAM param) + { + m_bArea = param.m_bArea; + m_Shape = param.m_Shape; + m_vSize = new Vector3(param.m_vSize.x, param.m_vSize.y, param.m_vSize.z); + } + + // ===== Factory method (same as C++ CGfxMoveBase::CreateMoveMethod) ===== + // 工厂方法(与C++ CGfxMoveBase::CreateMoveMethod相同) + + /// + /// Create movement method by mode. Only Linear and OnTarget implemented for Phase 1. + /// 根据模式创建移动方法。第一阶段仅实现Linear和OnTarget。 + /// + public static CGfxMoveBase CreateMoveMethod(GfxMoveMode mode) + { + switch (mode) + { + case GfxMoveMode.enumOnTarget: return new CGfxOnTargetMove(mode); + case GfxMoveMode.enumLinearMove: return new CGfxLinearMove(mode); + // TODO Phase 3: Add remaining movement modes + // case GfxMoveMode.enumParabolicMove: return new CGfxParabolicMove(mode); + // case GfxMoveMode.enumMissileMove: return new CGfxMissileMove(mode); + // case GfxMoveMode.enumMeteoricMove: return new CGfxMeteoricMove(mode); + // case GfxMoveMode.enumHelixMove: return new CGfxHelixMove(mode); + // case GfxMoveMode.enumCurvedMove: return new CGfxCurvedMove(mode); + // case GfxMoveMode.enumAccMove: return new CGfxAccMove(mode); + // case GfxMoveMode.enumLink: return new CGfxLinkMove(mode); + // case GfxMoveMode.enumRandMove: return new CGfxRandMove(mode); + default: return new CGfxLinearMove(mode); + } + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Vfx/CGfxMoveBase.cs.meta b/Assets/PerfectWorld/Scripts/Vfx/CGfxMoveBase.cs.meta new file mode 100644 index 0000000000..67372da788 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Vfx/CGfxMoveBase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5c18584a8969bcb47ba124d270aa8490 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs b/Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs new file mode 100644 index 0000000000..81b212d006 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs @@ -0,0 +1,80 @@ +using UnityEngine; + +namespace BrewMonster +{ + /// + /// Instant-hit movement (no flight). Used for melee/instant skills. + /// Mirrors C++ CGfxOnTargetMove exactly (A3DSkillGfxEvent2.cpp:297-322). + /// 瞬间命中移动(无飞行)。用于近战/瞬发技能。 + /// + public class CGfxOnTargetMove : CGfxMoveBase + { + protected float m_fRadius; + protected Vector3 m_vOffset; + + public CGfxOnTargetMove(GfxMoveMode mode) : base(mode) + { + m_HitPos = GfxHitPos.enumHitBottom; + m_fRadius = 0; + } + + /// + /// Set position to target immediately. Cluster offset adds random radius. + /// 立即将位置设置到目标。群集偏移添加随机半径。 + /// + public override void StartMove(Vector3 vHost, Vector3 vTarget) + { + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxOnTargetMove.StartMove: Entry - host={vHost}, target={vTarget}, cluster={m_bOneOfCluser}, radius={m_fRadius}"); + + m_vPos = vTarget; + m_vMoveDir = vTarget - vHost; + m_vMoveDir.y = 0; // C++: zero out Y before normalize + if (Normalize(ref m_vMoveDir) == 0) + { + m_vMoveDir = Vector3.forward; // _unit_z + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxOnTargetMove.StartMove: MoveDir was zero, using forward"); + } + + if (m_bOneOfCluser) + { + float fRandAng = Random.value * Mathf.PI * 2f; + float fRadius = Random.value * m_fRadius; + m_vOffset.x = Mathf.Cos(fRandAng) * fRadius; + m_vOffset.z = Mathf.Sin(fRandAng) * fRadius; + m_vOffset.y = 0; + m_vPos += m_vOffset; + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxOnTargetMove.StartMove: Cluster offset applied - offset={m_vOffset}, finalPos={m_vPos}"); + } + else + { + m_vOffset = Vector3.zero; + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxOnTargetMove.StartMove: No cluster offset, pos={m_vPos}"); + } + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxOnTargetMove.StartMove: Complete - pos={m_vPos}, dir={m_vMoveDir}"); + } + + /// + /// Always returns false — hit is triggered by fly time timeout, NOT by TickMove. + /// 始终返回false — 命中由飞行时间超时触发,而非TickMove。 + /// + public override bool TickMove(uint dwDeltaTime, Vector3 vHostPos, Vector3 vTargetPos) + { + Vector3 oldPos = m_vPos; + m_vPos = vTargetPos + m_vOffset; + + BMLogger.LogError($"[SKILL_GFX_DEBUG] CGfxOnTargetMove.TickMove: Updated pos from {oldPos} to {m_vPos} (target={vTargetPos}, offset={m_vOffset}), returning false (hit by timeout)"); + return false; // C++ returns false — hit triggered by fly time timeout + } + + /// + /// Override to also read radius from param value. + /// 重写以从参数值中读取半径。 + /// + public override void SetParam(GFX_SKILL_PARAM param) + { + base.SetParam(param); + m_fRadius = param.value.fVal; // C# union access: param.value.fVal + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs.meta b/Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs.meta new file mode 100644 index 0000000000..031fad7e11 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0257990f0ce2fad439f013b71b86e3f3 \ No newline at end of file diff --git a/C_SHARP_VS_CPP_COMPARISON.md b/C_SHARP_VS_CPP_COMPARISON.md new file mode 100644 index 0000000000..e0c65f0948 --- /dev/null +++ b/C_SHARP_VS_CPP_COMPARISON.md @@ -0,0 +1,272 @@ +# C# vs C++ Comparison: Why Monster Destroy Error Occurs in C# but Not C++ + +## Key Differences + +### 1. **Memory Management Model** + +#### C++ (Raw Pointers) +```cpp +// C++: EC_ManNPC.cpp line 774-787 +void CECNPCMan::ReleaseNPC(CECNPC* pNPC) +{ + if (pNPC) + { + pNPC->Release(); + delete pNPC; // ← Object immediately destroyed, pointer becomes invalid + pNPC = NULL; + } +} +``` + +**Behavior:** +- `delete pNPC` immediately destroys the object +- Pointer becomes **invalid** (dangling pointer) +- Accessing deleted object = **immediate crash** (predictable) +- Null check (`if (pNPC)`) is **sufficient** to prevent crashes + +#### C# Unity (Managed References) +```csharp +// C#: CECNPCMan.cs line 214-226 +void ReleaseNPC(CECNPC pNPC) +{ + if (pNPC) + { + pNPC.Release(); + pNPC.DestroySelf(); // ← Calls Destroy(gameObject) + } +} + +// CECNPC.cs line 578-581 +public void DestroySelf() +{ + Destroy(gameObject); // ← Unity marks for destruction, but reference persists +} +``` + +**Behavior:** +- `Destroy(gameObject)` **marks** GameObject for destruction at end of frame +- C# reference (`pNPC`) **still exists** and is NOT set to null +- Unity's `==` operator is **overloaded** - `pNPC == null` returns `true` for destroyed objects +- But **direct property access** (`pNPC.transform`, `pNPC.GetPosVector3()`) **still throws error** +- Null check (`if (pNPC != null)`) is **NOT sufficient** - need GameObject check too + +### 2. **GetNPCFromAll Implementation** + +#### C++ Version +```cpp +// C++: EC_ManNPC.cpp line 908-923 +CECNPC* CECNPCMan::GetNPCFromAll(int nid) +{ + CECNPC* pNPC = GetNPC(nid); + if (pNPC) + return pNPC; + + // Search from disappear array - NPCs still exist here! + for (int i=0; i < m_aDisappearNPCs.GetSize(); i++) + { + CECNPC* pNPC = m_aDisappearNPCs[i]; + if (pNPC->GetNPCID() == nid) + return pNPC; // ← Returns NPC even if removed from main table + } + + return NULL; +} +``` + +**Key Points:** +- Searches **disappear array** (`m_aDisappearNPCs`) after main table +- NPCs in disappear array are **still valid** (not deleted yet) +- Provides a **grace period** for GFX events to complete +- Returns `NULL` only when NPC is truly gone + +#### C# Version +```csharp +// C#: CECNPCMan.cs line 541-564 +public CECNPC GetNPCFromAll(int nid) +{ + CECNPC pNPC = GetNPC(nid); + if (pNPC != null) + return pNPC; + + // Search from disappear array - COMMENTED OUT! + /*for (int i = 0; i < m_aDisappearNPCs.GetSize(); i++) + { + CECNPC* pNPC = m_aDisappearNPCs[i]; + if (pNPC->GetNPCID() == nid) + return pNPC; + }*/ + + return null; // ← Returns null immediately if not in main table +} +``` + +**Key Points:** +- **Disappear array search is COMMENTED OUT** (lines 555-561) +- Returns `null` immediately if NPC not in main table +- **No grace period** - NPC removed from table = immediately unavailable +- This is a **porting issue** - the safety mechanism was removed + +### 3. **get_pos_by_id Implementation** + +#### C++ Version +```cpp +// C++: EC_ManSkillGfx.cpp line 89-122 +inline bool _get_pos_by_id(..., int nID, A3DVECTOR3& vPos, ...) +{ + if (ISNPCID(nID)) + { + CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID); + + if (pNPC){ // ← Simple null check is sufficient + // Access pNPC methods directly + vPos = pNPC->GetPos(); // ← Safe: if pNPC is null, we wouldn't be here + return true; + } + } + return false; +} +``` + +**Why it works:** +- `GetNPCFromAll()` may return NPC from disappear array (still valid) +- If `pNPC` is null, we return false - **no access attempted** +- If `pNPC` is non-null, it's **guaranteed valid** (C++ pointer semantics) +- No need to check if object is "destroyed" - either it exists or pointer is null + +#### C# Version +```csharp +// C#: CECSkillGfxMan.cs line 545-589 +private static bool get_pos_by_id(..., int nID, out Vector3 vPos, ...) +{ + if (GPDataTypeHelper.ISNPCID(nID)) + { + CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); + + if (pNPC != null) // ← This check passes even for destroyed GameObjects! + { + vPos = pNPC.GetPosVector3(); // ← ERROR: GameObject destroyed! + // OR + vPos = pNPC.transform.position; // ← ERROR: GameObject destroyed! + } + } + return false; +} +``` + +**Why it fails:** +- `GetNPCFromAll()` returns `null` immediately (disappear array not checked) +- But even if it returned a reference, Unity's destroyed objects are "fake null" +- `pNPC != null` returns `true` even for destroyed GameObjects +- Need **additional check**: `pNPC.gameObject != null` to detect destroyed objects + +### 4. **Object Lifecycle Differences** + +#### C++ Lifecycle +``` +1. NPC created → Pointer stored in m_NPCTab +2. NPC dies → Moved to m_aDisappearNPCs (still valid) +3. Disappear timer expires → delete pNPC (immediate destruction) +4. Pointer becomes invalid → NULL check prevents access +``` + +**Timeline:** +- NPC removed from main table → **Still accessible via disappear array** +- NPC deleted → **Pointer immediately invalid** (predictable crash if accessed) + +#### C# Unity Lifecycle +``` +1. NPC created → GameObject + Component stored in m_NPCTab +2. NPC dies → Removed from m_NPCTab immediately +3. Destroy(gameObject) called → Marked for destruction +4. End of frame → GameObject destroyed, but C# reference persists +5. Reference is "fake null" → == null works, but property access throws +``` + +**Timeline:** +- NPC removed from main table → **Immediately unavailable** (disappear array not checked) +- GameObject destroyed → **Reference persists but is "fake null"** +- Property access → **Throws "object has been destroyed" error** + +### 5. **Null Check Behavior** + +#### C++ Null Check +```cpp +CECNPC* pNPC = GetNPCFromAll(nid); +if (pNPC) // ← True only if pointer is valid +{ + vPos = pNPC->GetPos(); // ← Safe: pointer guaranteed valid +} +``` + +**Semantics:** +- `if (pNPC)` checks if pointer is **not NULL** +- If true, pointer is **guaranteed valid** (C++ doesn't have "fake null") +- Access is **safe** + +#### C# Unity Null Check +```csharp +CECNPC pNPC = GetNPCFromAll(nid); +if (pNPC != null) // ← True even for destroyed GameObjects! +{ + vPos = pNPC.GetPosVector3(); // ← ERROR: GameObject may be destroyed +} +``` + +**Semantics:** +- `if (pNPC != null)` uses Unity's **overloaded == operator** +- Returns `true` even for **destroyed GameObjects** (Unity's "fake null") +- Need **additional check**: `pNPC.gameObject != null` + +### 6. **Why C++ Doesn't Have This Problem** + +1. **Disappear Array Safety Net** + - C++ searches `m_aDisappearNPCs` which provides grace period + - NPCs remain accessible even after removal from main table + - GFX events can complete before NPC is actually deleted + +2. **Immediate Destruction** + - When `delete` is called, object is **immediately destroyed** + - Pointer becomes invalid, null check prevents access + - No "fake null" state - either valid or null + +3. **Predictable Behavior** + - Accessing deleted object = immediate crash (predictable) + - Null check is sufficient protection + - No need for GameObject existence checks + +### 7. **Why C# Has This Problem** + +1. **Disappear Array Not Used** + - C# version has disappear array search **commented out** + - NPCs become unavailable immediately when removed from main table + - No grace period for GFX events + +2. **Delayed Destruction** + - `Destroy(gameObject)` marks for destruction at **end of frame** + - C# reference persists in "fake null" state + - Property access throws error even though `== null` returns true + +3. **Unity's "Fake Null"** + - Unity overloads `==` operator to handle destroyed objects + - But direct property access doesn't use the overloaded operator + - Need explicit GameObject check: `gameObject != null` + +## Summary: Root Causes + +| Aspect | C++ | C# Unity | Impact | +|--------|-----|----------|--------| +| **Disappear Array** | ✅ Searched | ❌ Commented out | C# has no grace period | +| **Null Check** | ✅ Sufficient | ❌ Not sufficient | C# needs GameObject check | +| **Destruction** | Immediate (`delete`) | Delayed (`Destroy`) | C# has "fake null" state | +| **Pointer/Reference** | Raw pointer (invalid when deleted) | Managed reference (persists when destroyed) | C# reference can point to destroyed object | + +## The Fix + +To match C++ behavior, C# needs: + +1. **Re-enable disappear array search** in `GetNPCFromAll()` +2. **Add GameObject checks** in `get_pos_by_id()`: `pNPC != null && pNPC.gameObject != null` +3. **Early termination** of GFX events when target is destroyed +4. **Target validation** before creating GFX events + +These changes will restore the safety mechanisms that existed in C++ but were lost during porting. diff --git a/MONSTER_DESTROY_ERROR_ANALYSIS.md b/MONSTER_DESTROY_ERROR_ANALYSIS.md new file mode 100644 index 0000000000..067f8fdf11 --- /dev/null +++ b/MONSTER_DESTROY_ERROR_ANALYSIS.md @@ -0,0 +1,304 @@ +# Monster Destroy Error Analysis + +## Error Message +``` +[SKILL_GFX_DEBUG] ComposerMan.Play: Exception in composer.Play() - The object of type 'CECMonster' has been destroyed but you are still trying to access it. +``` + +## Root Cause + +This is a **race condition** between skill casting and monster destruction. Here's the flow: + +### The Problem Flow + +1. **Skill is Cast** → `CECAttacksMan.AddSkillAttack()` creates a `CECAttackEvent` +2. **Attack Event Queued** → Event is added to `m_targets` linked list in `CECAttacksMan` +3. **Monster Destroyed** → Monster dies/disappears, removed from `m_NPCTab`, GameObject destroyed +4. **Attack Event Fires** → `CECAttackEvent.DoFire()` is called (after delay) +5. **GFX Event Created** → `A3DSkillGfxComposer.Play()` creates `CECSkillGfxEvent` with target ID +6. **GFX Event Ticks** → `CECSkillGfxEvent.Tick()` tries to get target position +7. **ERROR** → `get_pos_by_id()` calls `GetNPCFromAll()` which returns null or destroyed object +8. **Access Attempt** → Code tries to access `pNPC.GetPosVector3()` or `pNPC.transform` on destroyed GameObject + +## Monster Destruction Flow (C#) + +### Entry Points for Monster Destruction: + +1. **`CECNPCMan.OnMsgNPCDied()`** (line 291) + - Receives `MSG_NM_NPCDIED` message + - Calls `pNPC.Killed(bDelay)` + - May call `NPCDisappear()` later + +2. **`CECNPCMan.OnMsgNPCDisappear()`** (line 149) + - Receives `MSG_NM_NPCDISAPPEAR` message + - Calls `NPCDisappear(nid)` + +3. **`CECNPCMan.OnMsgNPCOutOfView()`** (line 90) + - Receives `MSG_NM_NPCOUTOFVIEW` message + - Calls `NPCLeave(nid)` + +4. **`CECNPCMan.OnMsgInvalidObject()`** (line 80) + - Receives `MSG_NM_INVALIDOBJECT` message + - Calls `NPCLeave(nid)` + +### Destruction Process: + +```csharp +// CECNPCMan.NPCDisappear() - line 156 +void NPCDisappear(int nid) +{ + CECNPC pNPC = GetNPC(nid); + if (pNPC) + { + pNPC.Disappear(); + NPCLeave(nid, true, false); // Remove from active table + m_aDisappearNPCs.Add(pNPC); // Add to disappear table + } +} + +// CECNPCMan.NPCLeave() - line 177 +void NPCLeave(int nid, bool bUpdateMMArray = true, bool bRelease = true) +{ + CECNPC pNPC = GetNPC(nid); + if (!pNPC) return; + + // Remove from active NPC table + m_NPCTab.Remove(nid); // ← Monster removed from dictionary here + + if (bRelease) + ReleaseNPC(pNPC); // ← Calls DestroySelf() +} + +// CECNPCMan.ReleaseNPC() - line 214 +void ReleaseNPC(CECNPC pNPC) +{ + if (pNPC) + { + pNPC.Release(); + pNPC.DestroySelf(); // ← Calls Destroy(gameObject) + } +} + +// CECNPC.DestroySelf() - line 578 +public void DestroySelf() +{ + Destroy(gameObject); // ← Unity destroys the GameObject +} +``` + +## Skill Casting Flow (C#) + +### When Skill is Cast: + +```csharp +// CECAttacksMan.AddSkillAttack() - line 270 +public CECAttackEvent AddSkillAttack(int idHost, int idCastTarget, int idTarget, ...) +{ + var newEvent = new CECAttackEvent(...); + m_targets.AddLast(newEvent); // ← Event queued with target ID + return newEvent; +} + +// CECAttacksMan.Update() - line 142 +private void Update() +{ + var node = m_targets.First; + while (node != null) + { + if (!node.Value.m_bFinished) + node.Value.Tick(dwDeltaTime); // ← Event ticks every frame + node = next; + } +} + +// CECAttackEvent.DoFire() - line 845 +bool DoFire() +{ + // ... skill logic ... + composerMan.Play(m_idSkill, m_idHost, m_idCastTarget, m_targets); + // ← Creates GFX event with target IDs +} + +// A3DSkillGfxComposer.Play() - line 630 +public void Play(int nHostID, int nCastTargetID, List targets, ...) +{ + // Creates CECSkillGfxEvent for each target + AddOneTarget(nCastTargetID, nHostID, szFly, szHit, tar, ...); +} + +// A3DSkillGfxMan.AddOneSkillGfxEvent() - line 117 +public bool AddOneSkillGfxEvent(...) +{ + A3DSkillGfxEvent pEvent = SkillGfxMan.InstanceSub.GetEmptyEvent(mode); + pEvent.SetTargetID(nTargetID); // ← Stores target ID + PushEvent(pEvent); // ← Event added to active list +} + +// CECSkillGfxEvent.Tick() - line 126 +public override void Tick(uint dwDeltaTime) +{ + // Updates host and target positions every frame + m_bTargetExist = get_pos_by_id(..., (int)m_nTargetID, out m_vTargetPos, ...); + // ← Tries to get position of target (monster) + + base.Tick(dwDeltaTime); // ← May call GetTargetCenter() +} + +// CECSkillGfxEvent.GetTargetCenter() - line 80 +public override Vector3 GetTargetCenter() +{ + get_pos_by_id(..., (int)m_nTargetID, out vTargetCenter, ...); + // ← Tries to access destroyed monster here! + return vTargetCenter; +} + +// CECSkillGfxEvent.get_pos_by_id() - line 449 +private static bool get_pos_by_id(..., int nID, out Vector3 vPos, ...) +{ + if (GPDataTypeHelper.ISNPCID(nID)) + { + CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); // ← Returns null if destroyed + + if (pNPC != null) + { + vPos = pNPC.GetPosVector3(); // ← ERROR: GameObject destroyed! + // OR + vPos = pNPC.transform.position; // ← ERROR: GameObject destroyed! + } + } +} +``` + +## Why This Happens in C# + +### Unity's Destroy Behavior: + +In Unity, when you call `Destroy(gameObject)`: +1. The GameObject is marked for destruction +2. At the end of the frame, Unity destroys it +3. **However**, C# references to the component (`CECNPC`) are NOT set to `null` immediately +4. The reference still exists, but accessing any property throws: "The object has been destroyed" + +### The Race Condition: + +``` +Frame N: Skill cast → CECAttackEvent created → Queued (delay: 200ms) +Frame N+1: Monster dies → Removed from m_NPCTab → GameObject destroyed +Frame N+2: CECAttackEvent.DoFire() called → Creates CECSkillGfxEvent +Frame N+3: CECSkillGfxEvent.Tick() → Tries to access destroyed monster → ERROR +``` + +### Why C++ Didn't Have This Issue: + +In C++, when an object is deleted: +- The pointer becomes invalid immediately +- Accessing it causes a crash (but predictable) +- The code likely had better null checks or the timing was different + +In C# Unity: +- Destroyed objects are "fake null" - `== null` returns true, but the reference isn't actually null +- Unity's `==` operator is overloaded to handle destroyed objects +- But direct property access still throws the error + +## The Bug in GetNPCFromAll + +There's also a **secondary bug** in `CECNPCMan.GetNPCFromAll()`: + +```csharp +// Line 545-546 - BUG! +CECNPC pNPC = GetNPC(nid); +BMLogger.LogError($"... GetNPC returned {(pNPC.name)}"); // ← Crashes if pNPC is null! +if (pNPC != null) // ← Check happens AFTER accessing pNPC.name +``` + +This will crash if `pNPC` is null. + +## Solutions + +### Solution 1: Check for Destroyed Objects (Recommended) + +In `CECSkillGfxEvent.get_pos_by_id()`, check if the GameObject is destroyed: + +```csharp +private static bool get_pos_by_id(..., int nID, out Vector3 vPos, ...) +{ + if (GPDataTypeHelper.ISNPCID(nID)) + { + CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); + + // Check if NPC exists AND GameObject is not destroyed + if (pNPC != null && pNPC.gameObject != null) + { + vPos = pNPC.GetPosVector3(); + return true; + } + } + return false; +} +``` + +### Solution 2: Early Termination of GFX Events + +When target is destroyed, mark the GFX event as finished: + +```csharp +// In CECSkillGfxEvent.Tick() +if (!m_bTargetExist && m_nTargetID != 0) +{ + // Target was destroyed, finish the event + m_enumState = GfxSkillEventState.enumFinished; + return; +} +``` + +### Solution 3: Fix GetNPCFromAll Bug + +```csharp +public CECNPC GetNPCFromAll(int nid) +{ + CECNPC pNPC = GetNPC(nid); + + // Check null BEFORE accessing properties + if (pNPC != null) + { + BMLogger.LogError($"... GetNPC returned {pNPC.name}"); + return pNPC; + } + + BMLogger.LogError($"... NPC {nid} NOT FOUND"); + return null; +} +``` + +### Solution 4: Validate Targets Before Creating GFX Events + +In `A3DSkillGfxComposer.Play()`, validate targets exist before creating events: + +```csharp +public void Play(int nHostID, int nCastTargetID, List targets, ...) +{ + // Validate targets exist before creating GFX events + if (targets != null) + { + for (int i = targets.Count - 1; i >= 0; i--) + { + var tar = targets[i]; + if (!ValidateTargetExists(tar.idTarget)) + { + targets.RemoveAt(i); // Remove invalid target + } + } + } + // ... rest of Play() +} +``` + +## Recommended Fix + +Apply **all four solutions**: +1. Fix the `GetNPCFromAll` null check bug +2. Add destroyed object checks in `get_pos_by_id` +3. Early terminate GFX events when target is destroyed +4. Validate targets before creating GFX events + +This provides defense in depth and handles the race condition at multiple levels. diff --git a/SKILL_GFX_CONVERSION_PLAN.md b/SKILL_GFX_CONVERSION_PLAN.md index d438200bfb..d5f3fe8f72 100644 --- a/SKILL_GFX_CONVERSION_PLAN.md +++ b/SKILL_GFX_CONVERSION_PLAN.md @@ -5,8 +5,8 @@ This document provides a comprehensive plan to convert the Perfect World skill GFX (graphical effects) system from C++ to Unity C#. The system handles visual effects when players cast skills, including projectile flight, hit effects, and various movement patterns. **Date Created:** 2026-02-11 -**Last Updated:** 2026-02-12 -**Status:** Implementation Phase +**Last Updated:** 2026-02-24 +**Status:** Phase 1 Complete — Movement system created, scale removed (Particle Systems) **Complexity:** Medium - Core structure exists, needs wiring up --- @@ -50,11 +50,11 @@ A3DSkillGfxComposer.Play() - Iterates targets, calls AddOneTarget ✅ ↓ A3DSkillGfxMan.AddSkillGfxEvent() - Creates GFX events with clustering ✅ ↓ -A3DSkillGfxEvent.Tick() - State machine (Wait → Flying → Hit → Finished) ⚠️ COMMENTED OUT +A3DSkillGfxEvent.Tick() - State machine (Wait → Flying → Hit → Finished) ✅ WORKING ↓ -CGfxMoveBase - Movement calculation ❌ NOT CREATED +CGfxMoveBase - Movement calculation ✅ CREATED (Linear + OnTarget) ↓ -GFX Spawning (fly/hit) - Instantiate prefabs ❌ NOT IMPLEMENTED +GFX Spawning (fly/hit) - Instantiate prefabs ✅ IMPLEMENTED (CECSkillGfxEvent) ``` ### 1.3 Key Components @@ -65,11 +65,11 @@ GFX Spawning (fly/hit) - Instantiate prefabs ❌ NOT IMPLEMENTED | `CECAttackEvent` | `CECAttacksMan.cs` | ✅ Working (full DoFire with Player/NPC/weapon branches) | | `A3DSkillGfxComposerMan` | `A3DSkillGfxComposerMan.cs` | ✅ Complete (load, play, lookup) | | `A3DSkillGfxComposer` | `CECAttacksMan.cs` (partial) + `CECSkillGfxMan.cs` (partial) | ✅ Mostly complete (Load from SkillStub, Play, AddOneTarget) | -| `A3DSkillGfxMan` | `A3DSkillGfxMan.cs` | ⚠️ Structure exists but Tick() logic commented out | -| `A3DSkillGfxEvent` | `A3DSkillGfxMan.cs` | ⚠️ State machine structure exists, core logic commented out | -| `CECSkillGfxEvent` | `CECSkillGfxMan.cs` | ⚠️ Helpers done, Tick/HitTarget commented out | +| `A3DSkillGfxMan` | `A3DSkillGfxMan.cs` | ✅ Working (AddSkillGfxEvent, clustering, scale removed) | +| `A3DSkillGfxEvent` | `A3DSkillGfxMan.cs` | ✅ State machine working (Wait→Flying→Hit→Finished) | +| `CECSkillGfxEvent` | `CECSkillGfxMan.cs` | ✅ Working (Tick, SpawnFlyGfx, SpawnHitGfx, HitTarget) | | `SkillGfxMan` | `CECSkillGfxMan.cs` | ✅ Event pool, Tick loop, GetEmptyEvent | -| `CGfxMoveBase` (Movement) | N/A | ❌ Not created | +| `CGfxMoveBase` (Movement) | `CGfxMoveBase.cs` | ✅ Created (Linear + OnTarget, factory method) | | `CECMultiSectionSkillMan` | `CECAttacksMan.cs` | ⚠️ Structure done, LoadConfig not implemented | | `SkillStub` | `skill.cs` | ✅ All GFX params stored per-skill | | `BaseVfxObject` | `BaseVfxObject.cs` | ✅ Full VFX lifecycle | @@ -536,9 +536,9 @@ public class CGfxOnTargetMove : CGfxMoveBase string szHit = hitGFX != null ? hitGfxName : null; ``` -2. Add `m_fFlyGfxScale` and `m_fHitGfxScale` fields (default 1.0f) +2. ~~Add `m_fFlyGfxScale` and `m_fHitGfxScale` fields~~ **REMOVED** — Unity Particle Systems handle their own scale (see `agent-skills/11-gfx-to-particle-system.md`) -3. Un-comment scale calculation in `AddOneTarget()` +3. ~~Un-comment scale calculation in `AddOneTarget()`~~ **REMOVED** — not needed for Particle Systems 4. Make GFX prefab references accessible to events: ```csharp @@ -952,8 +952,16 @@ BMLogger.Log($"[GFX_FLOW] HitTarget at {vTarget}"); **End of Document** -This updated plan reflects the actual codebase state as of 2026-02-12. The core structure is ~60% done. The main work remaining is: -1. **Create** the movement system (new files) -2. **Un-comment** the state machine and GFX logic -3. **Fix** the GFX string/scale issues -4. **Remove** the SpawnGFX temp hack +This plan was last updated 2026-02-24. **Phase 1 is now complete (~95%)**: +1. ✅ Movement system created (`CGfxMoveBase`, `CGfxLinearMove`, `CGfxOnTargetMove`) +2. ✅ State machine working (Wait→Flying→Hit→Finished) +3. ✅ GFX spawning via `CECSkillGfxEvent` (SpawnFlyGfx, SpawnHitGfx) +4. ✅ Scale parameters removed — Unity Particle Systems handle their own scale +5. ✅ SkillGfxMan.Tick() wired into Update loop +6. ✅ NPC skill GFX path wired up + +**Remaining Phase 2+ work:** +- Additional movement modes (Parabolic, Missile, Helix, etc.) +- Hook/bone attachment for GFX positions +- Multi-section skill LoadConfig +- Optimization (LOD, culling) diff --git a/agent-skills/11-gfx-to-particle-system.md b/agent-skills/11-gfx-to-particle-system.md new file mode 100644 index 0000000000..89f8d93593 --- /dev/null +++ b/agent-skills/11-gfx-to-particle-system.md @@ -0,0 +1,60 @@ +# GFX to Particle System Conversion Notes + +## Key Difference: Scale Handling + +### C++ (Original) +In the original C++ engine, GFX files (`.gfx`) were custom Angelica 3D effect objects (`A3DGFXEx`). +Scale was applied **programmatically** via code: + +```cpp +// C++ — scale set by code on each GFX instance +pGfx->SetScale(fFlyGfxScale); +pGfx->SetActualScale(pHitGfx->GetScale() * 2.0f); // e.g. critical hit 2x +``` + +Two scale fields existed per composer: +- `m_fFlyGfxScale` — fly GFX scale multiplier +- `m_fHitGfxScale` — hit GFX scale multiplier + +These were also used with `m_bRelScl` and `m_fDefTarScl` for relative-to-target scaling: +```cpp +if (m_bRelScl) + fScale = GetTargetScale(target) / m_fDefTarScl * m_fHitGfxScale; +else + fScale = m_fHitGfxScale; +``` + +### Unity (Current) +In Unity, all `.gfx` files have been **converted to Particle Systems** (prefabs with `ParticleSystem` components). + +**Scale is NOT needed in code because:** +1. Particle Systems define their own size/scale in the prefab inspector +2. Visual size is controlled by Start Size, Size Over Lifetime, etc. within the particle system +3. The particle system already looks correct at its authored scale when instantiated + +### What Was Removed +- `m_fFlyGfxScale` and `m_fHitGfxScale` fields from `A3DSkillGfxComposer` +- `fFlyGfxScale` and `fHitGfxScale` parameters from `AddSkillGfxEvent()` and `AddOneSkillGfxEvent()` +- Scale calculation in `AddOneTarget()` (the `if (m_bRelScl)` block) +- `pGfx.SetScale()` calls (were already commented out) + +### What Was Kept +- `m_bRelScl` and `m_fDefTarScl` — kept in `SkillStub` and `A3DSkillGfxComposer` for potential future use + (e.g., if we ever want relative scaling on huge bosses) + +## Rule for Future Development + +> **DO NOT add GFX scale parameters back into the code pipeline.** +> If scale adjustment is needed for specific skills (e.g., boss skills should be bigger), +> use Unity's built-in Transform scale on the instantiated prefab, or create a variant prefab +> with different particle sizes. Do not try to replicate the C++ `SetScale()` pattern. + +## GFX Loading Path + +``` +C++: A3DGFXEx* pGfx = LoadGfx(pDevice, "gfx/path/to/effect.gfx"); +Unity: GameObject prefab = await Addressables.LoadAssetAsync("gfx/path/to/effect"); + GameObject instance = Instantiate(prefab, position, rotation); +``` + +The Addressable address uses the same path as C++ (minus the `.gfx` extension), prefixed with `gfx/`. diff --git a/agent-skills/12-prefer-unitask-over-task.md b/agent-skills/12-prefer-unitask-over-task.md new file mode 100644 index 0000000000..8af0b3a54d --- /dev/null +++ b/agent-skills/12-prefer-unitask-over-task.md @@ -0,0 +1,283 @@ +# Prefer UniTask Over Task for Unity Async Operations + +## Why UniTask? + +**UniTask** (Cysharp.Threading.Tasks) is a high-performance async/await library designed specifically for Unity. It provides significant advantages over standard .NET `Task`: + +### Performance Benefits +- **Zero allocation** — No GC allocations for async operations (Task allocates ~200 bytes per await) +- **Unity-optimized** — Built on Unity's PlayerLoop, integrates with Unity lifecycle +- **Better performance** — Up to 10x faster than Task in Unity + +### Unity Integration +- **Cancellation tokens** — Automatic cancellation when GameObject is destroyed +- **PlayerLoop integration** — Runs on Unity's main thread by default +- **Coroutine-like behavior** — Can await Unity operations (WaitForSeconds, etc.) +- **Better error handling** — More Unity-friendly exception handling + +### Codebase Status +The project currently has **mixed usage**: +- ✅ `LitModelHolder.cs` — Already uses UniTask +- ⚠️ `CECAttacksMan.cs` — Uses `System.Threading.Tasks` +- ⚠️ `A3DSkillGfxComposerMan.cs` — Uses `System.Threading.Tasks` +- ⚠️ `A3DSkillGfxMan.cs` — Uses `System.Threading.Tasks` + +**Goal:** Migrate all async code to UniTask over time. + +--- + +## When to Use UniTask vs Task + +### ✅ Use UniTask For: +- **All Unity async operations** (loading assets, waiting, coroutines) +- **MonoBehaviour async methods** (Start, Update, etc.) +- **Addressable loading** (async asset loading) +- **Any code that runs in Unity's main thread** + +### ⚠️ Use Task Only For: +- **Pure .NET library code** (no Unity dependencies) +- **Third-party APIs** that return Task (wrap with `ToUniTask()`) +- **Thread pool operations** (use `UniTask.RunOnThreadPool()` instead) + +--- + +## Migration Patterns + +### Pattern 1: Method Signatures + +**❌ Old (Task):** +```csharp +using System.Threading.Tasks; + +public async Task Load(string path) +{ + await Task.Delay(100); + return true; +} +``` + +**✅ New (UniTask):** +```csharp +using Cysharp.Threading.Tasks; + +public async UniTask Load(string path) +{ + await UniTask.Delay(100); + return true; +} +``` + +### Pattern 2: Yield to Keep Unity Responsive + +**❌ Old (Task):** +```csharp +// CECAttacksMan.cs line 117 +await System.Threading.Tasks.Task.Yield(); +``` + +**✅ New (UniTask):** +```csharp +await UniTask.Yield(); // Yields to Unity's main thread +``` + +### Pattern 3: Addressable Loading + +**❌ Old (Task):** +```csharp +public async Task LoadPrefabAsync(string path) +{ + var handle = Addressables.LoadAssetAsync(path); + await handle.Task; // Blocks, can cause freezing + return handle.Result; +} +``` + +**✅ New (UniTask):** +```csharp +public async UniTask LoadPrefabAsync(string path) +{ + var handle = Addressables.LoadAssetAsync(path); + await handle.ToUniTask(); // Non-blocking, Unity-friendly + return handle.Result; +} +``` + +### Pattern 4: Cancellation Tokens + +**❌ Old (Task):** +```csharp +public async Task LoadAsync(CancellationToken ct) +{ + await Task.Delay(1000, ct); +} +``` + +**✅ New (UniTask):** +```csharp +public async UniTask LoadAsync(CancellationToken ct = default) +{ + // Auto-cancels when GameObject is destroyed if using GetCancellationTokenOnDestroy() + await UniTask.Delay(1000, cancellationToken: ct); +} + +// In MonoBehaviour: +private async void Start() +{ + var ct = this.GetCancellationTokenOnDestroy(); // Auto-cancels on destroy + await LoadAsync(ct); +} +``` + +### Pattern 5: Converting Task to UniTask + +**When calling third-party APIs that return Task:** +```csharp +// If you must call a Task-returning method: +Task result = SomeThirdPartyLibrary.DoSomethingAsync(); +bool value = await result.ToUniTask(); // Convert to UniTask +``` + +--- + +## Codebase-Specific Examples + +### Example 1: CECAttacksMan.cs + +**Current (line 74-121):** +```csharp +public async void LoadAllSkillGfxAsync() +{ + // ... + await System.Threading.Tasks.Task.Yield(); // ❌ Should be UniTask.Yield() +} +``` + +**Should be:** +```csharp +using Cysharp.Threading.Tasks; // Add at top + +public async void LoadAllSkillGfxAsync() +{ + // ... + await UniTask.Yield(); // ✅ Better performance, Unity-integrated +} +``` + +### Example 2: A3DSkillGfxComposerMan.cs + +**Current (line 40):** +```csharp +public async Task LoadOneComposerAsync(...) +{ + if (!await composer.Load(...)) // Task + return false; +} +``` + +**Should be:** +```csharp +using Cysharp.Threading.Tasks; // Add at top + +public async UniTask LoadOneComposerAsync(...) +{ + if (!await composer.Load(...)) // UniTask + return false; +} +``` + +### Example 3: A3DSkillGfxComposer.Load() + +**Current (line 502):** +```csharp +public async Task Load(SkillStub skillStub, ...) +{ + flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName); +} +``` + +**Should be:** +```csharp +using Cysharp.Threading.Tasks; // Add at top + +public async UniTask Load(SkillStub skillStub, ...) +{ + flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName); + // If LoadPrefabAsync returns Task, convert: + // flyGFX = await AddressableManager.Instance.LoadPrefabAsync(...).ToUniTask(); +} +``` + +--- + +## Rules for New Code + +1. **Always use UniTask** for new async methods in Unity code +2. **Add `using Cysharp.Threading.Tasks;`** at the top of files with async code +3. **Use `UniTask.Yield()`** instead of `Task.Yield()` to keep Unity responsive +4. **Use `UniTask.Delay()`** instead of `Task.Delay()` for delays +5. **Use `GetCancellationTokenOnDestroy()`** in MonoBehaviour for auto-cancellation +6. **Convert Task to UniTask** when calling third-party APIs: `.ToUniTask()` + +--- + +## Migration Priority + +When refactoring existing code: + +1. **High Priority** — Methods called frequently (Update loops, loading) +2. **Medium Priority** — One-time initialization methods +3. **Low Priority** — Rarely-called utility methods + +**Don't break working code** — migrate incrementally, test after each change. + +--- + +## Common Mistakes to Avoid + +### ❌ Don't Mix Task and UniTask +```csharp +// BAD: Mixing Task and UniTask +public async Task Load() +{ + await UniTask.Delay(100); // ❌ Task method using UniTask + return true; +} +``` + +### ❌ Don't Use Task.Yield() in Unity +```csharp +// BAD: Task.Yield() doesn't integrate with Unity's main thread +await Task.Yield(); // ❌ May not yield to Unity properly +``` + +### ✅ Do Use UniTask Consistently +```csharp +// GOOD: Consistent UniTask usage +public async UniTask Load() +{ + await UniTask.Delay(100); // ✅ Proper Unity integration + return true; +} +``` + +--- + +## References + +- **UniTask GitHub:** https://github.com/Cysharp/UniTask +- **UniTask Documentation:** https://github.com/Cysharp/UniTask#readme +- **Performance Comparison:** UniTask is 10x faster than Task in Unity benchmarks + +--- + +## Quick Reference + +| Task API | UniTask Equivalent | +|----------|-------------------| +| `Task` | `UniTask` | +| `Task` | `UniTask` | +| `Task.Yield()` | `UniTask.Yield()` | +| `Task.Delay(ms)` | `UniTask.Delay(ms)` | +| `Task.Run(action)` | `UniTask.RunOnThreadPool(action)` | +| `CancellationToken` | `CancellationToken` (same, but use `GetCancellationTokenOnDestroy()`) | +| `task.ToUniTask()` | Convert Task → UniTask | diff --git a/agent-skills/README.md b/agent-skills/README.md index f893d1296f..2892c2a948 100644 --- a/agent-skills/README.md +++ b/agent-skills/README.md @@ -21,6 +21,8 @@ This folder contains comprehensive skills and guidelines for AI agents working o ### Reference 9. **[C++ Source Reference](./09-cpp-source-reference.md)** - Key C++ files and their locations 10. **[Unity C# Reference](./10-unity-csharp-reference.md)** - Key Unity C# files and their locations +11. **[GFX to Particle System](./11-gfx-to-particle-system.md)** - GFX→ParticleSystem conversion, why scale is not needed +12. **[Prefer UniTask Over Task](./12-prefer-unitask-over-task.md)** - Use UniTask for all Unity async operations ## Quick Start @@ -29,6 +31,7 @@ When starting a conversion task: 2. Check [Type Mappings](./02-type-mappings.md) for type conversions 3. Review [Common Pitfalls](./07-common-pitfalls.md) before coding 4. Use [Testing & Validation](./06-testing-validation.md) to verify your work +5. For async code: Use [UniTask Over Task](./12-prefer-unitask-over-task.md) - Prefer UniTask for Unity async operations ## Priority Rules