Files
test/Documentation/Skill Flow Documentation.md
T
2026-03-13 16:03:47 +07:00

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++:

  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

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 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):

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:

  1. Get Player Model:

    CECPlayer* pPlayer = pPlayerMan->GetPlayer(nID);
    CECModel* pModel = pPlayer->GetPlayerModel();
    
  2. Handle Child Model (Hanger):

    if (szHanger && bChildHook)
        pModel = pModel->GetChildModel(szHanger);  // Get weapon/pet model
    
  3. Get Skeleton Hook:

    A3DSkinModel* pSkin = pModel->GetA3DSkinModel();
    A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true);  // true = non-recursive
    
  4. 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
  5. 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:

  1. Get NPC Hook:

    CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID);
    A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook);
    
  2. Get Skin Model:

    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:

    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:

    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:

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 (like Resources.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

  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