fix skill can't move linear
This commit is contained in:
@@ -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,9 +45,7 @@ namespace BrewMonster
|
||||
A3DSkillGfxComposer pComposer,
|
||||
long nHostID,
|
||||
long nTargetID,
|
||||
|
||||
string szFlyGfx,
|
||||
|
||||
string szHitGfx,
|
||||
uint dwFlyTimeSpan,
|
||||
bool bTraceTarget,
|
||||
@@ -56,20 +53,14 @@ namespace BrewMonster
|
||||
int nFlyGfxCount,
|
||||
uint dwInterval,
|
||||
GFX_SKILL_PARAM param,
|
||||
|
||||
float fFlyGfxScale,
|
||||
|
||||
float fHitGfxScale,
|
||||
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,
|
||||
@@ -98,50 +94,63 @@ namespace BrewMonster
|
||||
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,
|
||||
|
||||
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;
|
||||
else
|
||||
{
|
||||
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);
|
||||
m_pMoveMethod.StartMove(m_vHostPos, m_vTargetPos);
|
||||
|
||||
// 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
|
||||
{
|
||||
Vector3 hitPos = m_bTargetExist ? GetTargetCenter() : m_pMoveMethod.GetPos();
|
||||
HitTarget(hitPos);
|
||||
}
|
||||
}
|
||||
else // enumFlying state / 飞行状态
|
||||
{
|
||||
if (m_pMoveMethod.TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos)) // 目标被命中 / Target hit
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
CECNPC pNPC = GetNPC(nid);
|
||||
if (pNPC)
|
||||
return pNPC;
|
||||
BMLogger.LogError($"[SKILL_GFX_DEBUG] CECNPCMan.GetNPCFromAll: Looking for NPC nid={nid}, m_NPCTab.Count={m_NPCTab.Count}");
|
||||
|
||||
// Search from disappear array ?
|
||||
/*for (int i = 0; i < m_aDisappearNPCs.GetSize(); i++)
|
||||
CECNPC pNPC = GetNPC(nid);
|
||||
if (pNPC != null)
|
||||
{
|
||||
CECNPC* pNPC = m_aDisappearNPCs[i];
|
||||
if (pNPC->GetNPCID() == nid)
|
||||
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,13 +81,15 @@ namespace BrewMonster
|
||||
{
|
||||
Vector3 vTargetCenter = Vector3.zero;
|
||||
|
||||
try
|
||||
{
|
||||
// if composer has been set
|
||||
// use the composer's parameter to make the hook information affect.
|
||||
// 如果已设置组合器,使用组合器的参数来影响挂点信息
|
||||
if (GetComposer() != null)
|
||||
{
|
||||
A3DSkillGfxComposer pComposer = GetComposer();
|
||||
_get_pos_by_id(
|
||||
bool success = get_pos_by_id(
|
||||
m_pPlayerMan,
|
||||
m_pNPCMan,
|
||||
(int)m_nTargetID,
|
||||
@@ -99,10 +101,17 @@ namespace BrewMonster
|
||||
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
|
||||
{
|
||||
_get_pos_by_id(
|
||||
bool success = get_pos_by_id(
|
||||
m_pPlayerMan,
|
||||
m_pNPCMan,
|
||||
(int)m_nTargetID,
|
||||
@@ -114,6 +123,21 @@ namespace BrewMonster
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
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,11 +485,17 @@ 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)
|
||||
{
|
||||
BMLogger.LogError($"[SKILL_GFX_DEBUG] _get_pos_by_id: Player {nID} found, getting position");
|
||||
{
|
||||
if (bIsGoblinSkill)
|
||||
{
|
||||
@@ -428,11 +562,20 @@ namespace BrewMonster
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
if (pNPC != null)
|
||||
// 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");
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
@@ -471,7 +614,17 @@ namespace BrewMonster
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
@@ -78,20 +78,32 @@ namespace BrewMonster
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
@@ -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/`.
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user