945 lines
30 KiB
Markdown
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
|