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

945 lines
30 KiB
Markdown

# 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<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`:
```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<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