# C++ Hook System for GFX Attack Spawning - Investigation Report ## Overview This investigation documents the **complete flow** of a skill in Perfect World C++: 1. Player presses a skill button on the client 2. Client sends a request to the server 3. Server validates and broadcasts the skill result back 4. Client receives server response and executes visual effects 5. 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 ```mermaid 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()` ```cpp // 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 ```cpp // EC_HostPlayer.cpp ~line 6300 // Send cast skill request to server g_pGame->GetGameSession()->c2s_CmdCastSkill(prepSkillID, byPVPMask, 1, &idTarget); ``` - `prepSkillID` - The skill to cast - `idTarget` - The target's client ID - `byPVPMask` - 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`): ```cpp 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`): ```cpp 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`): ```cpp case HOST_SKILL_ATTACK_RESULT: pGameRun->PostMessage(MSG_HST_SKILLRESULT, MAN_PLAYER, 0, (DWORD)pDataBuf, pCmdHeader->cmd); break; ``` **Handler** (`EC_HostMsg.cpp:947`): ```cpp 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`) ```cpp 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`): ```cpp 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: ```cpp // 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`): ```cpp 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) ```mermaid 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`: ```cpp 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`: ```cpp 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`: 1. **Get Player Model**: ```cpp CECPlayer* pPlayer = pPlayerMan->GetPlayer(nID); CECModel* pModel = pPlayer->GetPlayerModel(); ``` 2. **Handle Child Model (Hanger)**: ```cpp if (szHanger && bChildHook) pModel = pModel->GetChildModel(szHanger); // Get weapon/pet model ``` 3. **Get Skeleton Hook**: ```cpp A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); // true = non-recursive ``` 4. **Calculate Position**: - **Relative Offset** (`bRelHook = true`): ```cpp vPos = pHook->GetAbsoluteTM() * (*pOffset); ``` - Transforms offset from hook's local space to world space - **Absolute Offset** (`bRelHook = false`): ```cpp 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 5. **Fallback** (if hook not found): ```cpp 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`: 1. **Get NPC Hook**: ```cpp CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID); A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); ``` 2. **Get Skin Model**: ```cpp A3DSkinModel* pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); ``` 3. **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: 1. **Check for Composer**: ```cpp 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; } ``` 2. **Update Host Position**: ```cpp 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 ``` 3. **Update Target Position** (similar call with `pTargetPos`) ### Get Target Center `CECSkillGfxEvent::GetTargetCenter()` (`EC_ManSkillGfx.cpp:274-305`) uses hit position hook: ```cpp 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**: ```cpp 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): ```cpp 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` ```cpp 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 (like `Resources.Load(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`: ```cpp 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: ```cpp 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: ```cpp 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: ```cpp 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` ```cpp 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(path)` or `Addressables.LoadAssetAsync(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 1. **No Immediate Instantiation**: C++ loads GFX resource upfront but doesn't "spawn" it until `Start()` is called 2. **Resource Management**: GFX objects are managed by `A3DGFXExMan` (like an object pool) 3. **Transform Updates**: Uses matrix transforms (`SetParentTM`) instead of Transform component 4. **State-Based Activation**: GFX is activated based on event state machine, not immediately on creation 5. **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 skeleton - **`bNoChild = 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 1. **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 2. **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_FlyPos` becomes target position - `m_FlyEndPos` becomes 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 1. **Non-Recursive Search**: C++ uses `GetSkeletonHook(szHook, true)` - only searches main skeleton, not child models recursively 2. **Frame-by-Frame Updates**: Positions are recalculated every frame in `Tick()` method 3. **Fallback Behavior**: If hook not found, falls back to AABB center or bottom position 4. **Composer Dependency**: Hook system only works when `A3DSkillGfxComposer` is set; otherwise uses default hit position modes 5. **Reverse Mode**: When reversed, fly/hit positions are swapped for reverse-direction skills 6. **GFX Loading vs Spawning**: C++ loads GFX resources upfront but doesn't activate them until `Start()` is called - this is different from Unity's immediate `Instantiate()` 7. **Resource Management**: GFX objects are managed by `A3DGFXExMan` which handles pooling and cleanup