30 KiB
C++ Hook System for GFX Attack Spawning - Investigation Report
Overview
This investigation documents the complete flow of a skill in Perfect World C++:
- Player presses a skill button on the client
- Client sends a request to the server
- Server validates and broadcasts the skill result back
- Client receives server response and executes visual effects
- GFX objects are spawned at hook positions on character skeletons
Key Files
| File | Purpose |
|---|---|
EC_HostPlayer.cpp |
Player input handling, sends c2s_CmdCastSkill to server |
EC_HostMsg.cpp |
Receives server responses (OBJECT_CAST_SKILL, HOST_SKILL_ATTACK_RESULT) |
Network/EC_GameDataPrtc.cpp |
Routes incoming packets to message handlers |
EC_Player.cpp |
PlayAttackEffect() → AddSkillAttack() |
EC_ManAttacks.cpp |
CECAttackEvent::DoFire() → triggers GFX composer |
EC_ManSkillGfx.cpp |
Hook lookup and position calculation |
A3DSkillGfxComposer2.h |
Composer structure with hook parameters |
A3DSkillGfxEvent2.cpp |
GFX event state machine and spawning |
Part 1: Complete Client → Server → Client → GFX Flow
Full Flow Diagram
flowchart TD
subgraph client1 [CLIENT - Input]
A[Player presses skill hotkey] --> B[ApplySkillShortcut]
B --> C{Validate: range\ncooldown\nwork state}
C -->|OK| D[m_pPrepSkill = skill]
D --> E[CastSkill called]
E --> F[c2s_CmdCastSkill sent to server]
end
subgraph server [SERVER - Processing]
G[Server receives c2s_CmdCastSkill] --> H[Validate skill usage\ncost MP\ncheck range]
H --> I[Broadcast OBJECT_CAST_SKILL\nto all nearby clients]
I --> J[Calculate damage\nand targets]
J --> K[Send HOST_SKILL_ATTACK_RESULT\nto caster]
J --> L[Send HOST_SKILL_ATTACKED\nto target]
J --> M[Send OBJECT_SKILL_ATTACK_RESULT\nto observers]
end
subgraph client2 [CLIENT - Server Response Phase 1: Casting]
N[EC_GameDataPrtc receives\nOBJECT_CAST_SKILL] --> O[PostMessage MSG_PM_CASTSKILL]
O --> P[OnMsgPlayerCastSkill]
P --> Q[Set m_pCurSkill]
P --> R[Create CECHPWorkSpell]
P --> S[PlaySkillCastAction\nplay cast animation]
P --> T[Start incantation timer]
end
subgraph client3 [CLIENT - Server Response Phase 2: Attack Result]
U[EC_GameDataPrtc receives\nHOST_SKILL_ATTACK_RESULT] --> V[PostMessage MSG_HST_SKILLRESULT]
V --> W[OnMsgHstSkillResult]
W --> X[PlayAttackEffect\nwith skillID and damage]
X --> Y[AddSkillAttack to AttacksMan]
Y --> Z[CECAttackEvent created\nwith timeToBeFired delay]
end
subgraph client4 [CLIENT - GFX Execution]
AA[AttacksMan.Tick every frame] --> AB{timeToBeFired\nreached?}
AB -->|Yes| AC[CECAttackEvent.DoFire]
AC --> AD[ComposerMan.Play\nwith skillID and targets]
AD --> AE[AddOneSkillGfxEvent\nloads GFX resources]
AE --> AF[LoadFlyGfx\nLoadHitGfx]
AF --> AG[GFX Event State Machine\nspawns and animates GFX\nat hook positions]
end
F --> G
I --> N
K --> U
Z --> AA
Part 2: Detailed Step-by-Step
Step 1: Player Input (EC_HostPlayer.cpp)
Function: CECHostPlayer::ApplySkillShortcut() → CastSkill()
// EC_HostPlayer.cpp ~line 2715
m_pPrepSkill = pSkill; // Store which skill we want to cast
CastSkill(m_PlayerInfo.cid, bForceAttack);
Function: CECHostPlayer::CastSkill() → sends packet
// EC_HostPlayer.cpp ~line 6300
// Send cast skill request to server
g_pGame->GetGameSession()->c2s_CmdCastSkill(prepSkillID, byPVPMask, 1, &idTarget);
prepSkillID- The skill to castidTarget- The target's client IDbyPVPMask- PVP attack permission flags
Step 2: Server Processes and Responds
The server receives c2s_CmdCastSkill, validates it (MP, range, cooldown), then broadcasts back two separate messages:
| Server Packet | Direction | Purpose |
|---|---|---|
OBJECT_CAST_SKILL |
Server → All nearby clients | Tells everyone "this player is casting" |
HOST_SKILL_ATTACK_RESULT |
Server → Caster only | Tells caster the damage result |
HOST_SKILL_ATTACKED |
Server → Target only | Tells target they were hit |
OBJECT_SKILL_ATTACK_RESULT |
Server → Observers | Tells bystanders the result |
Step 3: Client Receives Phase 1 — Cast Confirmation (EC_GameDataPrtc.cpp + EC_HostMsg.cpp)
Network router (EC_GameDataPrtc.cpp:1323-1331):
case OBJECT_CAST_SKILL:
case OBJECT_CAST_INSTANT_SKILL:
case OBJECT_CAST_POS_SKILL:
{
if (ISPLAYERID(pCmd->caster))
pGameRun->PostMessage(MSG_PM_CASTSKILL, MAN_PLAYER, -1, (DWORD)pDataBuf, pCmdHeader->cmd);
else if (ISNPCID(pCmd->caster))
pGameRun->PostMessage(MSG_NM_NPCCASTSKILL, MAN_NPC, 0, (DWORD)pDataBuf, pCmdHeader->cmd);
break;
}
Handler (EC_HostMsg.cpp:5878):
case OBJECT_CAST_SKILL:
{
cmd_object_cast_skill* pCmd = (cmd_object_cast_skill*)Msg.dwParam1;
m_pCurSkill = GetPositiveSkillByID(pCmd->skill); // Find skill object
// Create work to handle the casting animation
CECHPWorkSpell* pWork = (CECHPWorkSpell*)m_pWorkMan->CreateWork(CECHPWork::WORK_SPELLOBJECT);
pWork->PrepareCast(pCmd->target, m_pCurSkill, iWaitTime);
m_pWorkMan->StartWork_p1(pWork);
// Play the casting animation
PlaySkillCastAction(m_pCurSkill->GetSkillID());
// Start incantation timer UI
m_IncantCnt.SetPeriod(iTime);
m_IncantCnt.Reset();
}
Step 4: Client Receives Phase 2 — Attack Result (EC_GameDataPrtc.cpp + EC_HostMsg.cpp)
Network router (EC_GameDataPrtc.cpp:1540):
case HOST_SKILL_ATTACK_RESULT:
pGameRun->PostMessage(MSG_HST_SKILLRESULT, MAN_PLAYER, 0, (DWORD)pDataBuf, pCmdHeader->cmd);
break;
Handler (EC_HostMsg.cpp:947):
void CECHostPlayer::OnMsgHstSkillResult(const ECMSG& Msg)
{
cmd_host_skill_attack_result* pCmd = (cmd_host_skill_attack_result*)Msg.dwParam1;
// This triggers the GFX and attack event creation
PlayAttackEffect(pCmd->idTarget, pCmd->idSkill, 0, pCmd->iDamage,
pCmd->attack_flag, pCmd->attack_speed * 50, NULL, pCmd->section);
}
Step 5: PlayAttackEffect Creates Attack Event (EC_Player.cpp)
Function: CECPlayer::PlayAttackEffect() (EC_Player.cpp:3414)
void CECPlayer::PlayAttackEffect(int idTarget, int idSkill, int skillLevel,
int nDamage, DWORD dwModifier, int nAttackSpeed,
int* piAttackTime, int nSection)
{
if (!idSkill)
{
// Melee attack: creates melee attack event
CECAttackEvent* pAttack = GetAttacksMan()->AddMeleeAttack(
GetPlayerInfo().cid, idTarget, idWeapon, dwModifier, nDamage, nTimeFly);
}
else
{
// Skill attack: creates skill attack event
CECAttackEvent* pAttack = GetAttacksMan()->AddSkillAttack(
GetPlayerInfo().cid, m_idCurSkillTarget, idTarget,
GetWeaponID(), idSkill, skillLevel, dwModifier, nDamage);
pAttack->SetSkillSection(nSection);
PlaySkillAttackAction(idSkill, nAttackSpeed, NULL, nSection, &pAttack->m_bSignaled);
}
}
Step 6: Attack Event Waits and Fires (EC_ManAttacks.cpp)
AddSkillAttack() creates a CECAttackEvent with a delay (timeToBeFired = 200ms):
CECAttackEvent * CECAttacksMan::AddSkillAttack(...)
{
// timeToBeFired=200ms, timeToDoDamage=1000ms
CECAttackEvent newEvent(this, idHost, idCastTarget, idTarget, idWeapon,
idSkill, nSkillLevel, dwModifier, nDamage, 200, 1000);
m_AttackList.AddTail(newEvent);
return ...;
}
Each frame CECAttacksMan::Tick() updates events:
// EC_ManAttacks.cpp ~line 476
if (m_timeToBeFired)
{
if (m_timeToBeFired <= dwDeltaTime)
{
m_timeToBeFired = 0;
DoFire(); // ← GFX is triggered here
}
else
m_timeToBeFired -= dwDeltaTime;
}
Step 7: DoFire Triggers GFX System (EC_ManAttacks.cpp)
Function: CECAttackEvent::DoFire() (EC_ManAttacks.cpp:111):
bool CECAttackEvent::DoFire()
{
if (ISPLAYERID(m_idHost))
{
if (m_idSkill != 0)
{
// Use skill GFX composer to play skill effect
if (m_nSkillSection > 0) // Multi-section skill
{
CECMultiSectionSkillMan* pMan = m_pManager->GetMultiSkillGfxComposerMan();
pMan->Play(m_idSkill, m_nSkillSection, m_idHost, m_idCastTarget, m_targets, ...);
}
else
{
// Single-section skill
m_pManager->GetSkillGfxComposerMan()->Play(
m_idSkill, m_idHost, m_idCastTarget, m_targets);
}
}
else if (m_idWeapon != 0)
{
// Weapon-based attack: load fly/hit GFX from weapon essence data
PROJECTILE_ESSENCE* pProjectile = ...;
szFlyGFX = pProjectile->file_firegfx + 4; // skip "gfx/" prefix
szHitGFX = pProjectile->file_hitgfx + 4;
// Directly add to GFX man (bypasses composer)
GetSkillGfxMan()->AddSkillGfxEvent(m_idHost, data.idTarget,
pszFlyGFX, pszHitGFX, m_timeToDoDamage, ...);
}
}
}
Step 8: Composer Plays GFX (A3DSkillGfxComposer2.cpp)
A3DSkillGfxComposer::Play() iterates over targets and calls AddOneSkillGfxEvent:
ComposerMan.Play(skillID, hostID, castTargetID, targets)
↓
A3DSkillGfxComposer.Play()
↓
For each target: AddOneTarget(...)
↓
A3DSkillGfxMan.AddSkillGfxEvent(...)
↓
A3DSkillGfxMan.AddOneSkillGfxEvent(...)
↓
LoadFlyGfx() + LoadHitGfx() ← GFX resources loaded
↓
Event pushed to active list
↓
Event.Tick() every frame → hook lookup → spawn/update GFX
Part 3: Architecture Flow (GFX Spawn Detail)
flowchart TD
A[Skill Attack Triggered] --> B[A3DSkillGfxComposer.Play]
B --> C[Load Composer from File]
C --> D[Composer Contains SGC_POS_INFO]
D --> E[m_FlyPos - Host Hook Info]
D --> F[m_FlyEndPos - Target Hook Info]
D --> G[m_HitPos - Hit Hook Info]
E --> H[AddOneSkillGfxEvent Called]
F --> H
G --> H
H --> I[LoadFlyGfx - Load Resource]
H --> J[LoadHitGfx - Load Resource]
I --> K[SetFlyGfx - Store Reference]
J --> L[SetHitGfx - Store Reference]
K --> M[CECSkillGfxEvent Created]
L --> M
M --> N[Event.Tick Called Every Frame]
N --> O{State?}
O -->|Wait| P[Count Down Delay]
P --> Q[State: Wait → Flying]
Q --> R[Get Position from Hook]
R --> S[_get_pos_by_id Function]
S --> T{Is Player?}
T -->|Yes| U[Get Player Model]
T -->|No| V[Get NPC Model]
U --> W{Has Hanger?}
V --> W
W -->|Yes| X[Get Child Model]
W -->|No| Y[Use Main Model]
X --> Z[GetSkeletonHook Lookup]
Y --> Z
Z --> AA[Calculate Position with Offset]
AA --> AB[SetParentTM - Set Transform]
AB --> AC[Start - Activate Fly GFX]
AC --> AD[TickAnimation - Update Every Frame]
O -->|Flying| AE[Update Position Every Frame]
AE --> AF[SetParentTM - Update Transform]
AF --> AG[TickAnimation - Update Animation]
AG --> AH{Target Reached?}
AH -->|Yes| AI[HitTarget Called]
AH -->|No| AE
AI --> AJ[ReleaseFlyGfx - Destroy]
AJ --> AK[Get Hit Position from Hook]
AK --> AL[SetParentTM - Set Hit Transform]
AL --> AM[Start - Activate Hit GFX]
AM --> AN[TickAnimation - Update Every Frame]
O -->|Hit| AO[Update Hit Position if Tracing]
AO --> AP[SetParentTM - Update Transform]
AP --> AQ[TickAnimation - Update Animation]
AQ --> AR{Hit GFX Finished?}
AR -->|Yes| AS[ReleaseHitGfx - Cleanup]
AR -->|No| AO
Part 4: Hook Parameter Storage
SGC_POS_INFO Structure
Defined in A3DSkillGfxComposer2.h:36-54:
struct SGC_POS_INFO
{
GfxHitPos HitPos; // Fallback hit position mode
char szHook[80]; // Hook name (e.g., "weapon", "hand")
char szHanger[80]; // Child model name (weapon/pet)
A3DVECTOR3 vOffset; // Position offset
bool bRelHook; // true = relative to hook, false = absolute
bool bChildHook; // true = search in child models
};
Composer Storage
A3DSkillGfxComposer class stores three SGC_POS_INFO structures:
m_FlyPos(line 70) - Fly GFX spawn position (host)m_FlyEndPos(line 71) - Fly GFX end position (target)m_HitPos(line 90) - Hit GFX position (target)
Part 5b: Hook Lookup Method: _get_pos_by_id
Function Signature
EC_ManSkillGfx.cpp:10-22:
inline bool _get_pos_by_id(
CECPlayerMan* pPlayerMan,
CECNPCMan* pNPCMan,
int nID,
A3DVECTOR3& vPos,
GfxHitPos HitPos,
bool bIsGoblinSkill = false,
const char* szHook = NULL, // Hook name
bool bRelHook = false, // Relative vs absolute offset
const A3DVECTOR3* pOffset = NULL, // Position offset
const char* szHanger = NULL, // Child model name
bool bChildHook = false) // Search in child models
Player Hook Lookup Flow
EC_ManSkillGfx.cpp:24-87:
-
Get Player Model:
CECPlayer* pPlayer = pPlayerMan->GetPlayer(nID); CECModel* pModel = pPlayer->GetPlayerModel(); -
Handle Child Model (Hanger):
if (szHanger && bChildHook) pModel = pModel->GetChildModel(szHanger); // Get weapon/pet model -
Get Skeleton Hook:
A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); // true = non-recursive -
Calculate Position:
-
Relative Offset (
bRelHook = true):vPos = pHook->GetAbsoluteTM() * (*pOffset);- Transforms offset from hook's local space to world space
-
Absolute Offset (
bRelHook = false):vPos = pSkin->GetAbsoluteTM() * (*pOffset); vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);- Transforms offset in model's world space, then translates to hook position
-
-
Fallback (if hook not found):
if (HitPos == enumHitBottom) vPos = pPlayer->GetPos(); else vPos = aabb.Center + aabb.Extents.y * 0.5f; // Center top
NPC Hook Lookup Flow
EC_ManSkillGfx.cpp:89-118:
-
Get NPC Hook:
CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID); A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); -
Get Skin Model:
A3DSkinModel* pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); -
Calculate Position (same logic as player)
Part 5: Hook Usage in Event System
Event Tick Method
CECSkillGfxEvent::Tick() (EC_ManSkillGfx.cpp:307-374) updates positions every frame:
-
Check for Composer:
if (A3DSkillGfxComposer* pComposer = GetComposer()) { // Use composer's hook parameters const SGC_POS_INFO *pHostPos, *pTargetPos; if (m_pMoveMethod->IsReverse()) { pHostPos = &m_pComposer->m_FlyEndPos; // Reversed pTargetPos = &m_pComposer->m_FlyPos; } else { pHostPos = &m_pComposer->m_FlyPos; // Normal pTargetPos = &m_pComposer->m_FlyEndPos; } -
Update Host Position:
m_bHostExist = _get_pos_by_id( m_pPlayerMan, m_pNPCMan, m_nHostID, m_vHostPos, pHostPos->HitPos, m_bIsGoblinSkill, pHostPos->szHook, // Hook name from composer pHostPos->bRelHook, // Relative flag &pHostPos->vOffset, // Offset vector pHostPos->szHanger, // Hanger name pHostPos->bChildHook); // Child hook flag -
Update Target Position (similar call with
pTargetPos)
Get Target Center
CECSkillGfxEvent::GetTargetCenter() (EC_ManSkillGfx.cpp:274-305) uses hit position hook:
if (A3DSkillGfxComposer* pComposer = GetComposer())
{
_get_pos_by_id(...,
pComposer->m_HitPos.szHook,
pComposer->m_HitPos.bRelHook,
&pComposer->m_HitPos.vOffset,
pComposer->m_HitPos.szHanger,
pComposer->m_HitPos.bChildHook);
}
Part 6: GFX Spawning/Instantiation (Equivalent to Unity's Instantiate)
Overview
In C++, GFX objects are loaded (like loading a prefab), assigned to the event (like storing a reference), and started (like activating a GameObject). Unlike Unity's Instantiate() which creates a new instance immediately, C++ loads the GFX resource and activates it when needed.
GFX Creation Flow
Step 1: Load GFX Resource (Like Loading Prefab)
Location: A3DSkillGfxMan::AddOneSkillGfxEvent() (A3DSkillGfxEvent2.cpp:647-669)
Fly GFX Loading:
if (szFlyGfx)
{
// Load GFX resource (equivalent to Resources.Load or Addressables.LoadAssetAsync)
A3DGFXEx* pGfx = pEvent->LoadFlyGfx(m_pDevice, szFlyGfx);
if (pGfx)
{
// Configure GFX properties (like setting GameObject properties)
pGfx->SetScale(fFlyGfxScale);
pGfx->SetDisableCamShake(pEvent->GetDisableCamShake());
pGfx->SetCreatedByGFXECM(pEvent->GetHostModelCreatedByGfx());
pGfx->SetUseLOD(pEvent->GetGfxUseLod());
pGfx->SetId(pEvent->GetHostID());
// Assign to event (like storing GameObject reference)
pEvent->SetFlyGfx(pGfx);
}
}
Hit GFX Loading (similar):
if (szHitGfx)
{
A3DGFXEx* pGfx = pEvent->LoadHitGfx(m_pDevice, szHitGfx);
if (pGfx)
{
pGfx->SetScale(fHitGfxScale);
pEvent->SetHitGfx(pGfx);
}
}
Step 2: LoadGfx Implementation
Location: A3DSkillGfxEvent2.h:545-546
virtual A3DGFXEx* LoadFlyGfx(A3DDevice* pDev, const char* szPath)
{
return AfxGetGFXExMan()->LoadGfx(pDev, szPath);
}
AfxGetGFXExMan()- Gets the GFX manager (like a resource manager)LoadGfx()- Loads GFX from file path (likeResources.Load<GameObject>(path))- Returns
A3DGFXEx*- Pointer to GFX object (like GameObject reference)
Step 3: Fly GFX Activation (Like GameObject.SetActive(true))
Location: A3DSkillGfxEvent::Tick() (A3DSkillGfxEvent2.cpp:530-547)
Fly GFX is started when event state changes from enumWait to enumFlying:
if (m_enumState == enumWait)
{
if (m_dwCurSpan < m_dwDelayTime) return;
// State change: Wait → Flying
m_enumState = enumFlying;
m_pMoveMethod->StartMove(m_vHostPos, m_vTargetPos);
if (m_pFlyGfx)
{
// Calculate transform matrix from hook position
A3DVECTOR3 vDir, vUp;
if (m_pMoveMethod->GetMode() == enumOnTarget && m_pMoveMethod->IsReverse() && GetTargetDirAndUp(vDir, vUp))
m_pFlyGfx->SetParentTM(a3d_TransformMatrix(vDir, vUp, m_pMoveMethod->GetPos()));
else
m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos()));
// START the GFX (equivalent to GameObject.SetActive(true) + Play())
m_pFlyGfx->Start(true);
// Update parameters (like setting component properties)
m_pMoveMethod->UpdateGfxParam(m_pFlyGfx, m_vHostPos, m_vTargetPos);
// Initial animation tick (like calling Update on first frame)
m_pFlyGfx->TickAnimation(0);
}
}
Step 4: Fly GFX Update (Every Frame)
Location: A3DSkillGfxEvent::Tick() (A3DSkillGfxEvent2.cpp:554-565)
While flying, GFX position is updated every frame:
else // enumFlying state
{
if (m_pMoveMethod->TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos))
HitTarget(GetTargetCenter()); // Reached target
else if (m_pFlyGfx)
{
// Update transform every frame (like updating Transform.position)
A3DVECTOR3 vDir, vUp;
if (m_pMoveMethod->GetMode() == enumOnTarget && m_pMoveMethod->IsReverse() && GetTargetDirAndUp(vDir, vUp))
m_pFlyGfx->SetParentTM(a3d_TransformMatrix(vDir, vUp, m_pMoveMethod->GetPos()));
else
m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos()));
// Update parameters (like updating component values)
m_pMoveMethod->UpdateGfxParam(m_pFlyGfx, m_vHostPos, m_vTargetPos);
// Tick animation (like calling Update() every frame)
m_pFlyGfx->TickAnimation(dwDeltaTime);
}
}
Step 5: Hit GFX Activation (When Target is Hit)
Location: A3DSkillGfxEvent::HitTarget() (A3DSkillGfxEvent2.cpp:570-601)
Hit GFX is started when fly GFX reaches target:
void A3DSkillGfxEvent::HitTarget(const A3DVECTOR3& vTarget)
{
m_enumState = enumHit;
ReleaseFlyGfx(); // Destroy fly GFX (like Destroy(flyGfxInstance))
if (m_pHitGfx)
{
m_bHitGfxInfinite = m_pHitGfx->IsInfinite();
A3DMATRIX4 matTran;
// Calculate hit position transform (from hook or default)
if (m_bHostExist)
{
A3DVECTOR3 vDir = vTarget - m_vHostPos;
vDir.y = 0;
if (vDir.Normalize() < 1e-3)
vDir = A3DVECTOR3(0, 0, 1.0f);
matTran = _build_matrix(vDir, vTarget);
}
else
{
matTran = IdentityMatrix();
matTran.SetRow(3, vTarget);
}
// Set transform (like transform.position = vTarget)
m_pHitGfx->SetParentTM(matTran);
// START the hit GFX (equivalent to Instantiate + SetActive)
m_pHitGfx->Start(true);
// Initial animation tick
m_pHitGfx->TickAnimation(0);
}
}
Step 6: Hit GFX Update (Every Frame While Active)
Location: A3DSkillGfxEvent::Tick() (A3DSkillGfxEvent2.cpp:492-512)
Hit GFX is updated every frame while active:
else if (m_enumState == enumHit) // Hit state
{
if (!m_pHitGfx || m_pHitGfx->GetState() == ST_STOP)
m_enumState = enumFinished;
else
{
if (!m_bTargetExist || m_bHitGfxInfinite && m_pHitGfx->GetTimeElapse() > HIT_GFX_MAX_TIMESPAN)
m_enumState = enumFinished;
else
{
// If tracing target, update position every frame (like following a moving target)
if (m_bTraceTarget)
{
A3DMATRIX4 matTran;
matTran.Identity();
matTran.SetRow(3, GetTargetCenter()); // Get position from hook
m_pHitGfx->SetParentTM(matTran);
}
// Tick animation (like Update() every frame)
m_pHitGfx->TickAnimation(dwDeltaTime);
}
}
}
GFX Cleanup (Like Destroy)
Location: A3DSkillGfxEvent2.h:474-493
void ReleaseFlyGfx()
{
if (m_pFlyGfx)
{
if (m_bFadeOut)
AfxGetGFXExMan()->QueueFadeOutGfx(m_pFlyGfx, 1000); // Fade out
else
{
m_pFlyGfx->Release();
delete m_pFlyGfx; // Destroy (like Destroy(gameObject))
}
m_pFlyGfx = NULL;
}
}
void ReleaseHitGfx()
{
if (m_pHitGfx)
{
AfxGetGFXExMan()->CacheReleasedGfx(m_pHitGfx); // Return to pool
m_pHitGfx = NULL;
}
}
C++ vs Unity Comparison
| C++ Operation | Unity Equivalent | When It Happens |
|---|---|---|
LoadFlyGfx(device, path) |
Resources.Load<GameObject>(path) or Addressables.LoadAssetAsync<GameObject>(path) |
Event creation (AddOneSkillGfxEvent) |
SetFlyGfx(pGfx) |
m_flyGfxInstance = prefab |
Event creation (store reference) |
m_pFlyGfx->Start(true) |
GameObject.SetActive(true) + ParticleSystem.Play() |
State change: Wait → Flying |
m_pFlyGfx->SetParentTM(matrix) |
transform.position = pos + transform.rotation = rot |
Every frame during flight |
m_pFlyGfx->TickAnimation(deltaTime) |
Update() method called |
Every frame |
ReleaseFlyGfx() |
Destroy(flyGfxInstance) |
When target is hit |
m_pHitGfx->Start(true) |
Instantiate(hitPrefab, position, rotation) + SetActive(true) |
When target is hit |
m_pHitGfx->SetParentTM(matrix) |
transform.position = targetPos |
Every frame (if bTraceTarget) |
ReleaseHitGfx() |
Destroy(hitGfxInstance) |
When hit GFX finishes |
Key Differences from Unity
- No Immediate Instantiation: C++ loads GFX resource upfront but doesn't "spawn" it until
Start()is called - Resource Management: GFX objects are managed by
A3DGFXExMan(like an object pool) - Transform Updates: Uses matrix transforms (
SetParentTM) instead of Transform component - State-Based Activation: GFX is activated based on event state machine, not immediately on creation
- Manual Animation Ticks: Must call
TickAnimation()every frame (Unity does this automatically)
Complete Spawning Timeline
Event Creation (AddOneSkillGfxEvent)
↓
LoadFlyGfx() - Load resource (like prefab)
↓
SetFlyGfx() - Store reference
↓
[Event added to queue, Tick() called every frame]
↓
Wait State - Delay time counting down
↓
State Change: Wait → Flying
↓
SetParentTM() - Set initial position (from hook)
↓
Start(true) - Activate GFX (like Instantiate + SetActive)
↓
TickAnimation(0) - Initial update
↓
[Every Frame]
↓
SetParentTM() - Update position (from movement)
↓
TickAnimation(deltaTime) - Update animation
↓
Target Reached
↓
ReleaseFlyGfx() - Destroy fly GFX
↓
HitTarget() called
↓
SetParentTM() - Set hit position (from hook)
↓
Start(true) - Activate hit GFX
↓
[Every Frame]
↓
SetParentTM() - Update position (if tracing target)
↓
TickAnimation(deltaTime) - Update animation
↓
Hit GFX Finished
↓
ReleaseHitGfx() - Cleanup
Part 7: Skeleton Hook Lookup Implementation
GetSkeletonHook Method
A3DSkinModel::GetSkeletonHook(const char* szName, bool bNoChild):
bNoChild = true: Non-recursive - only searches main skeletonbNoChild = false: Recursive - searches child models (weapons, pets)
Note: The C++ code uses bNoChild = true (non-recursive) when looking up hooks for GFX positioning.
Hook Transform
A3DSkeletonHook::GetAbsoluteTM() returns world-space transform matrix:
- Automatically updates when skeleton animates
- Includes bone transform + hook local transform
- Used for position calculation
Part 8: Key Implementation Details
Position Calculation Modes
-
Relative Offset (
bRelHook = true):- Offset is in hook's local coordinate space
- Formula:
hookWorldMatrix * offset - Example: Offset (0, 0, 0.5) relative to "weapon" hook
-
Absolute Offset (
bRelHook = false):- Offset is in model's world coordinate space
- Formula:
(modelWorldMatrix * offset) - modelPos + hookPos - Example: Offset (0, 1, 0) in world space, then translated to hook
Child Model Support
- Hanger: Name of child model (e.g., "weapon", "pet")
- bChildHook: If true, searches hook in child model instead of main model
- Used for weapon-mounted effects or pet-attached effects
Reverse Mode
When m_pMoveMethod->IsReverse() is true:
- Fly GFX travels from target to host
m_FlyPosbecomes target positionm_FlyEndPosbecomes host position
Part 9: Data Flow Summary
Composer File (GFX config)
↓
A3DSkillGfxComposer.Load()
↓
SGC_POS_INFO structures populated
↓
A3DSkillGfxComposer.Play()
↓
CECSkillGfxEvent created with composer reference
↓
LoadFlyGfx() / LoadHitGfx() - Load GFX resources
↓
Event.Tick() called every frame
↓
_get_pos_by_id() called with hook parameters
↓
GetSkeletonHook() finds hook by name
↓
Position calculated with offset
↓
GFX activated (Start()) and updated at calculated position
Part 10: Critical Code Locations
| Function | File | Lines | Purpose |
|---|---|---|---|
_get_pos_by_id |
EC_ManSkillGfx.cpp |
10-122 | Main hook lookup and position calculation |
CECSkillGfxEvent::Tick |
EC_ManSkillGfx.cpp |
307-374 | Updates host/target positions using hooks |
GetTargetCenter |
EC_ManSkillGfx.cpp |
274-305 | Gets hit position using hit hook |
SGC_POS_INFO |
A3DSkillGfxComposer2.h |
36-54 | Hook parameter structure |
GetSkeletonHook |
A3DSkinModel |
N/A | Skeleton hook lookup (referenced) |
AddOneSkillGfxEvent |
A3DSkillGfxEvent2.cpp |
603-676 | Creates event and loads GFX resources |
LoadFlyGfx / LoadHitGfx |
A3DSkillGfxEvent2.h |
545-546 | Loads GFX from file (like Resources.Load) |
A3DSkillGfxEvent::Tick |
A3DSkillGfxEvent2.cpp |
487-568 | State machine, activates/updates GFX |
HitTarget |
A3DSkillGfxEvent2.cpp |
570-601 | Activates hit GFX when target reached |
ReleaseFlyGfx / ReleaseHitGfx |
A3DSkillGfxEvent2.h |
474-493 | Destroys GFX (like Destroy) |
Part 11: Important Notes
- Non-Recursive Search: C++ uses
GetSkeletonHook(szHook, true)- only searches main skeleton, not child models recursively - Frame-by-Frame Updates: Positions are recalculated every frame in
Tick()method - Fallback Behavior: If hook not found, falls back to AABB center or bottom position
- Composer Dependency: Hook system only works when
A3DSkillGfxComposeris set; otherwise uses default hit position modes - Reverse Mode: When reversed, fly/hit positions are swapped for reverse-direction skills
- GFX Loading vs Spawning: C++ loads GFX resources upfront but doesn't activate them until
Start()is called - this is different from Unity's immediateInstantiate() - Resource Management: GFX objects are managed by
A3DGFXExManwhich handles pooling and cleanup