fix skill can't move linear

This commit is contained in:
VDH
2026-02-24 18:45:24 +07:00
parent b56718d56b
commit 9d4dfbc6ba
20 changed files with 2073 additions and 249 deletions
@@ -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()处理
@@ -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
/// <summary>
/// Draw gizmos for skill projectiles in Unity Editor
/// 在Unity编辑器中绘制技能弹道辅助线
/// </summary>
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();
}
/// <summary>
/// Draw gizmos when selected (for debugging)
/// 选择时绘制辅助线(用于调试)
/// </summary>
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<bool> Load(SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath)
public async UniTask<bool> 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<TARGET_DATA>();
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);
}
}
/// <summary>
/// Validate that a target exists and its GameObject is not destroyed
/// 验证目标存在且其GameObject未销毁
/// </summary>
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!");
}
}
@@ -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<cmd_npc_info_list>(((byte[])msg.dwParam1).AsSpan());
BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.OnMsgNPCInfo: NPC_INFO_LIST contains {pCmd.count} NPC(s)");
int offset = Marshal.OffsetOf<cmd_npc_info_list>("placeholder").ToInt32();
byte[] buffer = (byte[])msg.dwParam1;
Span<byte> 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<info_npc>(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<info_npc>(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<info_npc>(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;
}
@@ -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
/// </summary>
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
}
}
/// <summary>
@@ -217,6 +302,7 @@ namespace BrewMonster
/// </summary>
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
/// </summary>
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!");
}
}
/// <summary>
@@ -277,9 +384,20 @@ namespace BrewMonster
/// </summary>
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}");
}
/// <summary>
@@ -342,7 +470,7 @@ namespace BrewMonster
/// Get position by ID (player or NPC)
/// 根据ID获取位置(玩家或NPC)
/// </summary>
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
/// </summary>
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
/// 添加技能特效事件
/// </summary>
/// <summary>
/// Convenience overload for weapon/melee attacks (no composer).
/// Scale is not needed — Unity Particle Systems handle their own scale.
/// 武器/近战攻击的便捷重载(无组合器)。不需要缩放 — Unity粒子系统自己处理缩放。
/// </summary>
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
@@ -0,0 +1,203 @@
using System.Collections.Generic;
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// 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秒后自动移除。
/// </summary>
public static class SkillGfxGizmoDrawer
{
/// <summary>
/// Gizmo data for a single projectile
/// 单个弹道的辅助线数据
/// </summary>
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<GizmoData> m_GizmoList = new List<GizmoData>();
private const float GIZMO_LIFETIME = 10.0f; // 10 seconds / 10秒
/// <summary>
/// Register a projectile for gizmo drawing
/// 注册一个弹道用于辅助线绘制
/// </summary>
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}");
}
/// <summary>
/// Update projectile position
/// 更新弹道位置
/// </summary>
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;
}
}
}
/// <summary>
/// Remove projectile gizmo
/// 移除弹道辅助线
/// </summary>
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;
}
}
}
/// <summary>
/// Draw all gizmos (called from OnDrawGizmos)
/// 绘制所有辅助线(从OnDrawGizmos调用)
/// </summary>
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);
}
}
/// <summary>
/// Get color for movement mode
/// 根据移动模式获取颜色
/// </summary>
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;
}
/// <summary>
/// Clear all gizmos
/// 清除所有辅助线
/// </summary>
public static void ClearAll()
{
m_GizmoList.Clear();
}
/// <summary>
/// Get current gizmo count (for debugging)
/// 获取当前辅助线数量(用于调试)
/// </summary>
public static int GetGizmoCount()
{
return m_GizmoList.Count;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c17eed6528c112645895f705535a0196
@@ -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
@@ -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
/// <summary>
/// Async version of LoadOneComposer that doesn't block the main thread.
/// </summary>
public async Task<bool> LoadOneComposerAsync(int nSkillID, SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath)
public async UniTask<bool> 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<TARGET_DATA> Targets,
bool bIsGoblinSkill = false)
public void Play(
int nSkillID,
int nHostID,
int nCastTargetID,
List<TARGET_DATA> 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;
@@ -0,0 +1,84 @@
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// Straight-line projectile movement.
/// Mirrors C++ CGfxLinearMove exactly (A3DSkillGfxEvent2.cpp:22-52).
/// 直线弹道移动,完全镜像C++ CGfxLinearMove。
/// </summary>
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) { }
/// <summary>
/// Initialize movement from host to target.
/// 初始化从施法者到目标的移动。
/// </summary>
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}");
}
/// <summary>
/// Tick movement. Returns true when target is hit.
/// 更新移动。当命中目标时返回true。
/// </summary>
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;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e4107992bd89b5b43a8f8d6e944b2b10
@@ -0,0 +1,172 @@
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// Abstract base class for all projectile movement patterns.
/// Mirrors C++ CGfxMoveBase exactly.
/// 所有弹道移动模式的抽象基类,完全镜像C++ CGfxMoveBase。
/// </summary>
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相同)
/// <summary>
/// Normalize vector, return original magnitude. Returns 0 if too small.
/// 归一化向量,返回原始长度。如果太小则返回0。
/// </summary>
protected static float Normalize(ref Vector3 v)
{
float mag = v.magnitude;
if (mag < 1e-6f) { v = Vector3.zero; return 0f; }
v /= mag;
return mag;
}
/// <summary>
/// Calculate emission range vectors based on movement direction.
/// 根据移动方向计算发射范围向量。
/// </summary>
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;
}
/// <summary>
/// Get random offset based on emission shape.
/// 根据发射形状获取随机偏移。
/// </summary>
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; }
/// <summary>
/// Virtual — subclasses can override for custom param handling.
/// 虚函数 — 子类可以重写以自定义参数处理。
/// </summary>
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相同)
/// <summary>
/// Create movement method by mode. Only Linear and OnTarget implemented for Phase 1.
/// 根据模式创建移动方法。第一阶段仅实现Linear和OnTarget。
/// </summary>
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);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5c18584a8969bcb47ba124d270aa8490
@@ -0,0 +1,80 @@
using UnityEngine;
namespace BrewMonster
{
/// <summary>
/// Instant-hit movement (no flight). Used for melee/instant skills.
/// Mirrors C++ CGfxOnTargetMove exactly (A3DSkillGfxEvent2.cpp:297-322).
/// 瞬间命中移动(无飞行)。用于近战/瞬发技能。
/// </summary>
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;
}
/// <summary>
/// Set position to target immediately. Cluster offset adds random radius.
/// 立即将位置设置到目标。群集偏移添加随机半径。
/// </summary>
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}");
}
/// <summary>
/// Always returns false — hit is triggered by fly time timeout, NOT by TickMove.
/// 始终返回false — 命中由飞行时间超时触发,而非TickMove。
/// </summary>
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
}
/// <summary>
/// Override to also read radius from param value.
/// 重写以从参数值中读取半径。
/// </summary>
public override void SetParam(GFX_SKILL_PARAM param)
{
base.SetParam(param);
m_fRadius = param.value.fVal; // C# union access: param.value.fVal
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0257990f0ce2fad439f013b71b86e3f3
+272
View File
@@ -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.
+304
View File
@@ -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<TARGET_DATA> 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<TARGET_DATA> 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.
+24 -16
View File
@@ -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)
+60
View File
@@ -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<GameObject>("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/`.
+283
View File
@@ -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<bool> Load(string path)
{
await Task.Delay(100);
return true;
}
```
**✅ New (UniTask):**
```csharp
using Cysharp.Threading.Tasks;
public async UniTask<bool> 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<GameObject> LoadPrefabAsync(string path)
{
var handle = Addressables.LoadAssetAsync<GameObject>(path);
await handle.Task; // Blocks, can cause freezing
return handle.Result;
}
```
**✅ New (UniTask):**
```csharp
public async UniTask<GameObject> LoadPrefabAsync(string path)
{
var handle = Addressables.LoadAssetAsync<GameObject>(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<bool> 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<bool> LoadOneComposerAsync(...)
{
if (!await composer.Load(...)) // Task<bool>
return false;
}
```
**Should be:**
```csharp
using Cysharp.Threading.Tasks; // Add at top
public async UniTask<bool> LoadOneComposerAsync(...)
{
if (!await composer.Load(...)) // UniTask<bool>
return false;
}
```
### Example 3: A3DSkillGfxComposer.Load()
**Current (line 502):**
```csharp
public async Task<bool> Load(SkillStub skillStub, ...)
{
flyGFX = await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName);
}
```
**Should be:**
```csharp
using Cysharp.Threading.Tasks; // Add at top
public async UniTask<bool> 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<bool> 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<bool> 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<T>` | `UniTask<T>` |
| `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 |
+3
View File
@@ -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