skill behavior system plan

This commit is contained in:
VDH
2026-02-12 18:39:25 +07:00
parent 592a2f01db
commit c8e46a034a
2 changed files with 1322 additions and 0 deletions
+873
View File
@@ -0,0 +1,873 @@
# Skill GFX System Conversion Plan (C++ to Unity C#)
## Executive Summary
This document provides a comprehensive plan to convert the Perfect World skill GFX (graphical effects) system from C++ to Unity C#. The system handles visual effects when players cast skills, including projectile flight, hit effects, and various movement patterns.
**Date Created:** 2026-02-11
**Last Updated:** 2026-02-12
**Status:** Implementation Phase
**Complexity:** Medium - Core structure exists, needs wiring up
---
## Table of Contents
1. [System Overview](#system-overview)
2. [Current State Analysis](#current-state-analysis)
3. [Architecture Comparison](#architecture-comparison)
4. [Implementation Phases](#implementation-phases)
5. [File-by-File Status](#file-by-file-status)
6. [Key Technical Challenges](#key-technical-challenges)
7. [Testing Strategy](#testing-strategy)
---
## 1. System Overview
### 1.1 What is the Skill GFX System?
The Skill GFX system manages all visual effects when skills are cast in Perfect World:
- **Fly GFX**: Projectile/missile effects traveling from caster to target
- **Hit GFX**: Impact effects when skill hits the target
- **Hit Ground GFX**: Ground impact effects for area skills
### 1.2 Flow Diagram
```
Player Casts Skill
CECAttacksMan.AddSkillAttack() - Creates attack event ✅
CECAttackEvent.Tick() - Timing system ✅
CECAttackEvent.DoFire() - Triggers when skill fires ✅
A3DSkillGfxComposerMan.Play() - Finds composer by skill ID ✅
A3DSkillGfxComposer.Play() - Iterates targets, calls AddOneTarget ✅
A3DSkillGfxMan.AddSkillGfxEvent() - Creates GFX events with clustering ✅
A3DSkillGfxEvent.Tick() - State machine (Wait → Flying → Hit → Finished) ⚠️ COMMENTED OUT
CGfxMoveBase / IGfxMovement - Movement calculation ❌ NOT CREATED
GFX Spawning (fly/hit) - Instantiate prefabs ❌ NOT IMPLEMENTED
```
### 1.3 Key Components
| Component | File | Status |
|-----------|------|--------|
| `CECAttacksMan` | `CECAttacksMan.cs` | ✅ Working (attack events, timing, DoFire, DoDamage) |
| `CECAttackEvent` | `CECAttacksMan.cs` | ✅ Working (full DoFire with Player/NPC/weapon branches) |
| `A3DSkillGfxComposerMan` | `A3DSkillGfxComposerMan.cs` | ✅ Complete (load, play, lookup) |
| `A3DSkillGfxComposer` | `CECAttacksMan.cs` (partial) + `CECSkillGfxMan.cs` (partial) | ✅ Mostly complete (Load from SkillStub, Play, AddOneTarget) |
| `A3DSkillGfxMan` | `A3DSkillGfxMan.cs` | ⚠️ Structure exists but Tick() logic commented out |
| `A3DSkillGfxEvent` | `A3DSkillGfxMan.cs` | ⚠️ State machine structure exists, core logic commented out |
| `CECSkillGfxEvent` | `CECSkillGfxMan.cs` | ⚠️ Helpers done, Tick/HitTarget commented out |
| `SkillGfxMan` | `CECSkillGfxMan.cs` | ✅ Event pool, Tick loop, GetEmptyEvent |
| `CGfxMoveBase` (Movement) | N/A | ❌ Not created |
| `CECMultiSectionSkillMan` | `CECAttacksMan.cs` | ⚠️ Structure done, LoadConfig not implemented |
| `SkillStub` | `skill.cs` | ✅ All GFX params stored per-skill |
| `BaseVfxObject` | `BaseVfxObject.cs` | ✅ Full VFX lifecycle |
| `CECGFXCaster` | `CECGFXCaster.cs` | ✅ Async GFX loading |
---
## 2. Current State Analysis
### 2.1 What's Fully Working ✅
1. **Attack Event Pipeline** (`CECAttacksMan.cs`)
- `AddSkillAttack()` / `AddMeleeAttack()` - creates events
- `Tick()` - timing system (timeToBeFired → DoFire → timeToDoDamage → DoDamage)
- `DoDamage()` - applies damage to NPC/Player targets
- `Signal()` / `FindAttackByAttacker()` - event signaling
2. **DoFire() Implementation** (`CECAttackEvent.DoFire()`)
- Player skill branch: calls `composerMan.Play()` → GFX pipeline ✅
- Player weapon branch: loads weapon/projectile essence, spawns hit GFX via `ShowVfx()`
- Player fist branch: uses default fist hit GFX ✅
- NPC skill branch (multi-section): calls `pMan.Play()`
- NPC melee branch: loads monster/pet essence GFX (stub) ⚠️
- Fly time estimation based on distance ✅
3. **GFX Composer Loading** (`A3DSkillGfxComposerMan.cs`)
- `LoadOneComposerAsync()` - async loading with Addressables ✅
- Parameters read from `SkillStub` (MoveMode, TargetMode, FlyTime, clusters, etc.) ✅
- Placeholder fallback `Resources.Load("GFX/PlaceHolder")` for missing fly GFX ✅
4. **Composer Play Pipeline** (`A3DSkillGfxComposer.Play()`)
- Iterates targets, calls `AddOneTarget()`
- `AddOneTarget()` determines host/target based on `TargetMode`
- Calls `m_pSkillGfxMan.AddSkillGfxEvent()` with all parameters ✅
5. **Event Manager** (`A3DSkillGfxMan.AddSkillGfxEvent()`)
- Clustering support (multiple fly GFX per skill) ✅
- `AddOneSkillGfxEvent()` sets up event properties ✅
- Pushes event to `SkillGfxMan.m_EventLst`
6. **Event Pool** (`SkillGfxMan`)
- Pre-allocated event pool per `GfxMoveMode`
- `GetEmptyEvent()` / `Resume()` reuse ✅
- `Tick()` loop iterates events, recycles finished ones ✅
7. **Position Helpers** (`CECSkillGfxEvent`)
- `_get_pos_by_id()` - Player/NPC position with hook stubs ✅
- `_get_dir_and_up_by_id()`
- `_get_scale_by_id()`
- `_get_ecm_property_by_id()`
- `GetTargetCenter()` with composer HitPos ✅
8. **Data Layer** (`SkillStub`)
- All GFX parameters per skill: MoveMode, TargetMode, FlyTime, clusters, etc. ✅
- GFX paths: `m_szFlyGfxPath`, `m_szHitGfxPath`, `m_szHitGrndGfxPath`
- Example: Skill1Stub (虎击) has `m_MoveMode = enumOnTarget`, `m_dwFlyTime = 0`
9. **VFX System** (`BaseVfxObject.cs`, `CECGFXCaster.cs`)
- ParticleSystem lifecycle management ✅
- Scale, position, rotation control ✅
- Lifetime tracking and auto-stop ✅
- Async prefab loading via Addressables ✅
### 2.2 What's Partially Done ⚠️ (Structure Exists, Logic Commented Out)
1. **A3DSkillGfxEvent.Tick() State Machine** (`A3DSkillGfxMan.cs:389-468`)
- State enum exists: `enumWait`, `enumFlying`, `enumHit`, `enumFinished`
- State transitions coded but **all movement and GFX logic commented out**:
- Wait→Flying: `m_pMoveMethod.StartMove()` commented out (line 434)
- Flying: `m_pMoveMethod.TickMove()` commented out (line 454)
- Hit: `m_pHitGfx` lifecycle commented out (lines 396-414)
- Fly GFX `SetParentTM` / `TickAnimation` commented out (lines 437-448, 456-467)
2. **CECSkillGfxEvent.Tick()** (`CECSkillGfxMan.cs:123-198`)
- Position update logic fully written but **commented out** (lines 125-195)
- Currently just calls `base.Tick(dwDeltaTime)` (line 197)
3. **CECSkillGfxEvent.HitTarget()** (`CECSkillGfxMan.cs:204-240`)
- Calls `base.HitTarget()`
- Special hit effects (rune, magic rune) **commented out** (lines 210-239)
4. **A3DSkillGfxComposer.Play() GFX Strings** (`CECAttacksMan.cs:565-610`)
- Currently passes `""` (empty string) for `szFly` and `szHit` instead of actual GFX paths
- Optimization check (`CECOptimize.CanShowFly/CanShowHit`) commented out
5. **A3DSkillGfxComposer.AddOneTarget() Scale** (`CECAttacksMan.cs:611-686`)
- Scale calculation commented out (lines 644-647)
- `m_fFlyGfxScale` and `fScale` hardcoded to `0` (lines 677-678)
6. **SpawnGFX() Temp Hack** (`CECAttacksMan.cs:540-551`)
- Currently just instantiates flyGFX at target position as child (wrong behavior)
- Should not exist - GFX spawning belongs in the event state machine
7. **NPC Skill GFX** (`CECAttacksMan.cs:980-986`)
- Regular NPC skill composerMan.Play() call is **commented out**
### 2.3 What's Completely Missing ❌
1. **Movement System (CGfxMoveBase)**
- No `IGfxMovement` interface
- No movement implementations (Linear, Parabolic, Missile, etc.)
- `m_pMoveMethod` referenced in comments but never created
- All movement-related calls are commented out
2. **GFX Instance Management in Events**
- No `GameObject m_flyGfxInstance` / `m_hitGfxInstance` fields
- No fly GFX instantiation during Wait→Flying transition
- No fly GFX position/rotation updates during Flying state
- No hit GFX instantiation on HitTarget()
- No GFX cleanup on event finish
3. **Multi-Section Skill Config Loading** (`CECMultiSectionSkillMan.LoadConfig()`)
- Returns `false` with TODO comment
- Format documented but not parsed
4. **Hook System Implementation**
- `_get_pos_by_id()` has stubs for skeleton hooks
- No actual bone lookups (TODOs only)
5. **Scale Fields** in `A3DSkillGfxComposer`
- `m_fFlyGfxScale` and `m_fHitGfxScale` are not defined
- Referenced in `AddOneTarget()` as hardcoded `0`
---
## 3. Architecture Comparison
### 3.1 Current C# Architecture
```
CECAttacksMan (MonoSingleton, Update → Tick)
├── LinkedList<CECAttackEvent> ✅ Working
│ ├── DoFire() → Player skill / weapon / fist ✅ Working
│ ├── DoFire() → NPC skill / melee ⚠️ Partially
│ └── DoDamage() ✅ Working
├── A3DSkillGfxComposerMan ✅ Working
│ ├── LoadOneComposerAsync() ✅ Working
│ ├── Play() → composer.Play() ✅ Working
│ └── Dictionary<int, A3DSkillGfxComposer> ✅ Working
│ ├── Load(SkillStub, paths) ✅ Working
│ ├── Play() → AddOneTarget() ✅ Working (szFly/szHit empty ⚠️)
│ ├── AddOneTarget() → m_pSkillGfxMan.Add() ✅ Working (scale = 0 ⚠️)
│ └── SpawnGFX() temp hack ⚠️ Wrong behavior
├── A3DSkillGfxMan (singleton) ✅ Structure
│ ├── AddSkillGfxEvent() ✅ Working
│ └── AddOneSkillGfxEvent() ✅ Working
├── SkillGfxMan extends A3DSkillGfxMan ✅ Working
│ ├── Event pool (FreeLst[]) ✅ Working
│ ├── m_EventLst ✅ Working
│ ├── Tick() loop ✅ Working
│ └── GetEmptyEvent() / Resume() ✅ Working
├── CECSkillGfxEvent extends A3DSkillGfxEvent ⚠️ Partially
│ ├── Position helpers ✅ Working
│ ├── GetTargetCenter() ✅ Working
│ ├── Tick() override ⚠️ Commented out
│ └── HitTarget() override ⚠️ Commented out
├── A3DSkillGfxEvent (base class) ⚠️ Partially
│ ├── State machine skeleton ✅ Exists
│ ├── Movement (m_pMoveMethod) ❌ Not created
│ ├── Fly GFX instance ❌ Not created
│ ├── Hit GFX instance ❌ Not created
│ └── Tick() state machine logic ⚠️ Commented out
└── CECMultiSectionSkillMan ⚠️ Structure only
├── Play() / GetSkillGfxComposer() ✅ Working
└── LoadConfig() ❌ Not implemented
```
### 3.2 What Needs to Change
```
CURRENTLY: SHOULD BE:
═══════════════════ ═══════════════════
DoFire() → composerMan.Play() (same) ✅
↓ ↓
composer.Play(szFly="", szHit="") composer.Play(flyGFX, hitGFX objects)
↓ ↓
AddOneTarget(scale=0) AddOneTarget(scale from SkillStub)
↓ ↓
AddSkillGfxEvent() → push event (same) ✅
↓ ↓
composer.SpawnGFX() [WRONG!] Remove SpawnGFX hack
↓ ↓
event.Tick() → mostly no-op event.Tick() → full state machine:
Wait → StartMove, SpawnFlyGfx
Flying → TickMove, UpdateTransform
Hit → SpawnHitGfx, track target
Finished → cleanup
```
---
## 4. Implementation Phases
### Phase 1: MVP - Wire Up Core Pipeline (3-5 days)
**Goal:** Get the fly GFX → move → hit GFX pipeline working end-to-end with linear movement
#### Task 1.1: Create Movement System (1 day)
**New Files:**
**`Assets/PerfectWorld/Scripts/Vfx/IGfxMovement.cs`**
```csharp
public interface IGfxMovement
{
void SetMaxFlyTime(uint dwMaxFlyTime);
void StartMove(Vector3 vHost, Vector3 vTarget);
bool TickMove(float deltaTimeMs, Vector3 vHostPos, Vector3 vTargetPos);
Vector3 GetPos();
Vector3 GetMoveDir();
GfxMoveMode GetMode();
GfxHitPos GetHitPos();
bool IsReverse();
void SetReverse(bool bReverse);
void SetParam(GFX_SKILL_PARAM param);
void SetIsCluster(bool bCluster);
}
```
**`Assets/PerfectWorld/Scripts/Vfx/GfxLinearMove.cs`** (C++ ref: A3DSkillGfxEvent2.cpp:22-52)
```csharp
public class GfxLinearMove : IGfxMovement
{
private Vector3 m_vPos;
private Vector3 m_vMoveDir;
private float m_fSpeed;
private uint m_dwMaxFlyTime;
private bool m_bReverse;
private const float _fly_speed = 20.0f; // units/sec
public void SetMaxFlyTime(uint dwMaxFlyTime) { m_dwMaxFlyTime = dwMaxFlyTime; }
public void StartMove(Vector3 vHost, Vector3 vTarget)
{
m_vPos = vHost;
m_vMoveDir = vTarget - m_vPos;
float fDist = m_vMoveDir.magnitude;
if (fDist < 1e-4f) { m_vMoveDir = Vector3.forward; m_fSpeed = _fly_speed; return; }
m_vMoveDir /= fDist;
float fMax = _fly_speed / 1000f * m_dwMaxFlyTime;
m_fSpeed = fMax >= fDist ? _fly_speed / 1000f : fDist / m_dwMaxFlyTime;
}
public bool TickMove(float deltaTimeMs, Vector3 vHostPos, Vector3 vTargetPos)
{
Vector3 vFlyDir = vTargetPos - m_vPos;
float fDist = vFlyDir.magnitude;
if (fDist < 1e-4f) return true;
vFlyDir /= fDist;
float fFlyDist = m_fSpeed * deltaTimeMs;
if (fFlyDist >= fDist) return true;
m_vPos += vFlyDir * fFlyDist;
m_vMoveDir = vFlyDir;
return false;
}
public Vector3 GetPos() => m_vPos;
public Vector3 GetMoveDir() => m_vMoveDir;
public GfxMoveMode GetMode() => GfxMoveMode.enumLinearMove;
public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter;
public bool IsReverse() => m_bReverse;
public void SetReverse(bool b) { m_bReverse = b; }
public void SetParam(GFX_SKILL_PARAM param) { }
public void SetIsCluster(bool b) { }
}
```
**`Assets/PerfectWorld/Scripts/Vfx/GfxOnTargetMove.cs`** (for instant-hit skills like 虎击)
```csharp
public class GfxOnTargetMove : IGfxMovement
{
private Vector3 m_vPos;
private Vector3 m_vMoveDir;
private bool m_bReverse;
public void SetMaxFlyTime(uint dwMaxFlyTime) { }
public void StartMove(Vector3 vHost, Vector3 vTarget)
{
m_vPos = vTarget; // Instantly at target
m_vMoveDir = (vTarget - vHost);
if (m_vMoveDir.magnitude > 1e-4f) m_vMoveDir.Normalize();
else m_vMoveDir = Vector3.forward;
}
public bool TickMove(float deltaTimeMs, Vector3 vHostPos, Vector3 vTargetPos)
{
m_vPos = vTargetPos;
return true; // Instantly hits
}
public Vector3 GetPos() => m_vPos;
public Vector3 GetMoveDir() => m_vMoveDir;
public GfxMoveMode GetMode() => GfxMoveMode.enumOnTarget;
public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter;
public bool IsReverse() => m_bReverse;
public void SetReverse(bool b) { m_bReverse = b; }
public void SetParam(GFX_SKILL_PARAM param) { }
public void SetIsCluster(bool b) { }
}
```
**`Assets/PerfectWorld/Scripts/Vfx/GfxMoveFactory.cs`**
```csharp
public static class GfxMoveFactory
{
public static IGfxMovement Create(GfxMoveMode mode)
{
switch (mode)
{
case GfxMoveMode.enumLinearMove: return new GfxLinearMove();
case GfxMoveMode.enumOnTarget: return new GfxOnTargetMove();
// Phase 2: add more modes
default: return new GfxLinearMove();
}
}
}
```
#### Task 1.2: Un-comment & Wire Up Event State Machine (1-2 days)
**Changes to `A3DSkillGfxMan.cs` — `A3DSkillGfxEvent` class:**
1. Add `m_pMoveMethod` field (un-comment):
```csharp
protected IGfxMovement m_pMoveMethod;
```
2. In constructor, create movement:
```csharp
m_pMoveMethod = GfxMoveFactory.Create(mode);
```
3. Un-comment the state machine in `Tick()`:
- Wait→Flying: call `m_pMoveMethod.SetMaxFlyTime()` + `StartMove()`
- Flying: call `m_pMoveMethod.TickMove()`, on true → `HitTarget(GetTargetCenter())`
- Hit: check hit GFX lifetime → Finished
4. In `HitTarget()`: set state to Hit (already done), spawning handled by derived class
**Changes to `CECSkillGfxMan.cs` — `CECSkillGfxEvent` class:**
1. Add GFX instance fields:
```csharp
private GameObject m_flyGfxInstance;
private GameObject m_hitGfxInstance;
```
2. Un-comment `Tick()` position update logic (lines 125-195)
3. Override key methods for GFX spawning:
- `SpawnFlyGfx()` — instantiate `m_pComposer.flyGFX` at move position
- `UpdateFlyGfxTransform()` — update fly GFX position/rotation each tick
- `HitTarget()` — destroy fly GFX, instantiate hit GFX
**Changes to `A3DSkillGfxMan.cs` — `AddOneSkillGfxEvent()`:**
1. Remove the temporary `pComposer.SpawnGFX(nTargetID)` call (line 193)
#### Task 1.3: Fix Composer Play() GFX References (0.5 day)
**Changes to `CECAttacksMan.cs` — `A3DSkillGfxComposer.Play()`:**
1. Pass actual GFX names instead of empty strings:
```csharp
string szFly = flyGFX != null ? flyGfxName : null;
string szHit = hitGFX != null ? hitGfxName : null;
```
2. Add `m_fFlyGfxScale` and `m_fHitGfxScale` fields (default 1.0f)
3. Un-comment scale calculation in `AddOneTarget()`
4. Make GFX prefab references accessible to events:
```csharp
public GameObject GetFlyGFX() => flyGFX;
public GameObject GetHitGFX() => hitGFX;
public GameObject GetHitGrdGFX() => hitGrdGFX;
```
#### Task 1.4: Wire Up NPC Skill GFX (0.5 day)
**Changes to `CECAttacksMan.cs` — `DoFire()` NPC branch (line 980-986):**
1. Un-comment the composerMan.Play() call for regular NPC skills
#### Task 1.5: Testing & Debug Logging (1 day)
- Test with instant-hit skills (虎击, m_MoveMode = enumOnTarget)
- Test with projectile skills (skills with m_dwFlyTime > 0)
- Add flow logging at each state transition
- Verify damage timing matches GFX arrival
**Phase 1 Deliverables:**
- ✅ Movement system (Linear + OnTarget)
- ✅ Fly GFX spawns at caster, moves toward target
- ✅ Hit GFX spawns when projectile arrives
- ✅ Instant-hit skills work (no fly, immediate hit GFX)
- ✅ Timing synchronized (DoDamage matches GFX hit)
---
### Phase 2: Complete Movement & Visual Polish (1-2 weeks)
**Goal:** All 10 movement modes, proper GFX lifecycle, special effects
#### Task 2.1: Implement Remaining Movement Modes (3-5 days)
| Mode | Class | C++ Ref | Complexity | Priority |
|------|-------|---------|------------|----------|
| Linear | `GfxLinearMove` | lines 22-52 | ✅ Done in Phase 1 | — |
| OnTarget | `GfxOnTargetMove` | — | ✅ Done in Phase 1 | — |
| Parabolic | `GfxParabolicMove` | lines 54-92 | Medium | High |
| Missile | `GfxMissileMove` | lines 94-142 | High | High |
| Meteoric | `GfxMeteoricMove` | lines 144-188 | Low | Medium |
| Accelerated | `GfxAccMove` | lines 319-370 | Low | Medium |
| Helix | `GfxHelixMove` | lines 190-256 | Medium | Low |
| Curved | `GfxCurvedMove` | lines 258-317 | High | Low |
| Link | `GfxLinkMove` | lines 372-420 | Medium | Low |
| Random | `GfxRandMove` | lines 422-458 | Medium | Low |
**Pattern:** Each implements `IGfxMovement`, added to `GfxMoveFactory.Create()`
#### Task 2.2: GFX Lifecycle Management (2-3 days)
1. **Object Pooling** — Replace `Instantiate/Destroy` with pool:
```csharp
public class GfxPool : MonoSingleton<GfxPool>
{
public GameObject Spawn(GameObject prefab, Vector3 pos, Quaternion rot);
public void Despawn(GameObject obj, float delay = 0f);
}
```
2. **Fade-out Support** — When `m_bFadeOut` is true, fade fly GFX over 1s instead of instant destroy
3. **Hit GFX Infinite/Loop Detection** — Check if ParticleSystem is looping, cap at 5s
4. **GFX Scale** — Apply `fFlyGfxScale` / `fHitGfxScale` to spawned instances
5. **Trace Target** — When `m_bTraceTarget` is true, update hit GFX position each frame
#### Task 2.3: Hook System (2-3 days)
**Update `_get_pos_by_id()` in `CECSkillGfxMan.cs`:**
```csharp
// Replace the TODO skeleton hook stubs:
// 1. Get player/NPC model
// 2. Find bone Transform by hook name
// 3. Apply relative/absolute offset
// 4. Cache Transform lookups for performance
```
Requires:
- `CECModel.GetHook(string hookName)` — find bone Transform by name
- `CECModel.GetChildModel(string hangerName)` — for weapon/pet sub-models
#### Task 2.4: Special Hit Effects (1-2 days)
**Un-comment in `CECSkillGfxEvent.HitTarget()`:**
```csharp
// Rune effects (physical/magic attack/defense)
if ((m_dwModifier & MOD.MOD_PHYSIC_ATTACK_RUNE) != 0)
CECGameRun.Instance.ShowVfx("程序联入/符石/物攻符石特效", vTarget, null, 1f);
// Critical strike (scale up hit GFX)
if ((m_dwModifier & MOD.MOD_CRITICAL_STRIKE) != 0)
if (m_hitGfxInstance != null)
m_hitGfxInstance.transform.localScale *= 2.0f;
// Nullity (show miss effect instead)
if ((m_dwModifier & MOD.MOD_NULLITY) != 0)
CECGameRun.Instance.ShowVfx("程序联入/击中/无效攻击击中", vTarget, null, 1f);
```
#### Task 2.5: Optimization Checks (1 day)
**Un-comment in `A3DSkillGfxComposer.Play()`:**
```csharp
if (!CECOptimize.Instance.GetGFX().CanShowFly(nHostID))
szFly = null;
if (!CECOptimize.Instance.GetGFX().CanShowHit(nHostID))
szHit = null;
```
Implement `CECOptimize.GFXOptimize`:
- Distance-based culling (don't show GFX for far players)
- Per-player GFX count limit
- Total active GFX cap
**Phase 2 Deliverables:**
- ✅ All 10 movement modes
- ✅ Object pooling
- ✅ GFX scale and fade-out
- ✅ Hook system (skeleton bone attachment)
- ✅ Special effects (rune, critical, nullity)
- ✅ Distance/count optimization
---
### Phase 3: Advanced Features & Production (1-2 weeks)
**Goal:** Multi-section skills, goblin skills, full production readiness
#### Task 3.1: Multi-Section Skill Config Loading (2-3 days)
**Implement `CECMultiSectionSkillMan.LoadConfig()`:**
- Parse config file format (skill_id, section, suffix, sgc)
- Load or reuse `A3DSkillGfxComposer` per section
- Wire to `Play()` which already works
#### Task 3.2: Goblin (Pet) Skill Position (1 day)
**Update `_get_pos_by_id()` goblin handling:**
```csharp
if (bIsGoblinSkill)
{
CECGoblin goblin = pPlayer.GetGoblin();
if (goblin != null)
{
vPos = goblin.transform.position;
vPos.y += 0.5f;
return true;
}
}
```
#### Task 3.3: Skill State Action System (1-2 days)
```csharp
// In CECAttacksMan:
private List<SkillStateAction> m_SkillStateActionVec;
public bool LoadSkillStateActionConfig(string szFile) { ... }
public bool GetSkillStateActionName(int skill, int state, out string name1, out string name2) { ... }
```
#### Task 3.4: Debug Visualization (1 day)
```csharp
#if UNITY_EDITOR
public class GfxDebugVisualizer : MonoBehaviour
{
void OnDrawGizmos()
{
// Draw host → target lines
// Draw projectile positions
// Show state labels
}
}
#endif
```
#### Task 3.5: NPC Melee GFX Completion (1 day)
Complete the NPC melee branch in `DoFire()`:
- Load monster/pet essence GFX paths
- Call `AddSkillGfxEvent()` properly
#### Task 3.6: Production Testing & Polish (2-3 days)
- Test all skill types across all classes
- Edge cases: target dies mid-flight, caster dies, out of range
- Stress test: 50+ simultaneous skill casts
- Performance profiling (Unity Profiler)
- Memory leak check (object pool verification)
**Phase 3 Deliverables:**
- ✅ Multi-section skills (combo stages)
- ✅ Goblin/pet skill positioning
- ✅ Skill state actions
- ✅ Debug tools
- ✅ Production-ready performance
---
## 5. File-by-File Status
### 5.1 Files — No Changes Needed ✅
| File | Lines | Notes |
|------|-------|-------|
| `A3DSkillGfxComposerMan.cs` | 97 | Complete: Load, Play, GetComposer |
| `BaseVfxObject.cs` | 307 | Complete VFX lifecycle |
| `CECGFXCaster.cs` | 89 | Async GFX loading |
| `skill.cs` (SkillStub) | 369 | All GFX params defined |
| `SkillStubs1/*.cs` (514 files) | — | Per-skill GFX params set |
### 5.2 Files — Need Edits ⚠️
| File | Lines | What Needs Changing |
|------|-------|---------------------|
| `A3DSkillGfxMan.cs` | 516 | Un-comment movement + state machine in Tick(), add IGfxMovement field, remove SpawnGFX call |
| `CECSkillGfxMan.cs` | 878 | Un-comment Tick() position updates, implement GFX spawning, un-comment HitTarget() |
| `CECAttacksMan.cs` | 1354 | Fix Play() GFX strings, add scale fields, un-comment NPC skill branch, remove SpawnGFX hack |
### 5.3 Files — Need Creation ❌
| File | Phase | Purpose |
|------|-------|---------|
| `IGfxMovement.cs` | Phase 1 | Movement interface |
| `GfxLinearMove.cs` | Phase 1 | Straight-line projectile |
| `GfxOnTargetMove.cs` | Phase 1 | Instant-hit (no flight) |
| `GfxMoveFactory.cs` | Phase 1 | Factory for movement modes |
| `GfxParabolicMove.cs` | Phase 2 | Arc trajectory |
| `GfxMissileMove.cs` | Phase 2 | Homing missile |
| `GfxMeteoricMove.cs` | Phase 2 | Falls from sky |
| `GfxAccMove.cs` | Phase 2 | Accelerated flight |
| `GfxHelixMove.cs` | Phase 2 | Spiral path |
| `GfxCurvedMove.cs` | Phase 2 | Bezier curve |
| `GfxLinkMove.cs` | Phase 2 | Lightning chain |
| `GfxRandMove.cs` | Phase 2 | Random walk |
| `GfxPool.cs` | Phase 2 | Object pooling |
---
## 6. Key Technical Challenges
### 6.1 Challenge: Movement Time Units
**Issue:** C++ uses milliseconds throughout. Unity typically uses seconds.
**Solution:** Keep milliseconds internally (matching C++ data), convert at boundaries:
```csharp
// In CECSkillGfxEvent.Tick():
// dwDeltaTime is in milliseconds (from CECAttacksMan.Update: Time.deltaTime * 1000)
// Pass to movement as milliseconds to match C++ math
m_pMoveMethod.TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos);
```
### 6.2 Challenge: GFX Prefab vs Path References
**Issue:** C++ used string paths to load GFX at runtime. Unity has prefab references.
**Current State:** `A3DSkillGfxComposer` stores both:
- `GameObject flyGFX` / `hitGFX` / `hitGrdGFX` — loaded prefabs
- `string flyGfxName` / `hitGfxName` / `hitGrdGfxName` — original paths
**Solution:** Events should reference the `A3DSkillGfxComposer` which holds the loaded prefabs. Access via `m_pComposer.GetFlyGFX()` etc.
### 6.3 Challenge: SpawnGFX Hack Removal
**Issue:** `A3DSkillGfxComposer.SpawnGFX()` (line 540) instantiates fly GFX at target position, which is wrong.
**Solution:** Remove SpawnGFX call from `AddOneSkillGfxEvent()`. GFX spawning should happen inside `CECSkillGfxEvent.Tick()` when transitioning from Wait→Flying state.
### 6.4 Challenge: GFX String vs Prefab in AddSkillGfxEvent
**Issue:** `AddSkillGfxEvent` receives `string szFlyGfx` / `string szHitGfx`, but the actual prefab is on the composer.
**Solution:** The event already receives `pComposer` reference. Use `pComposer.GetFlyGFX()` / `pComposer.GetHitGFX()` for instantiation. The string params can be used for null-check (if empty, skip fly/hit).
### 6.5 Challenge: Timing Synchronization
**Current Flow:**
```
DoFire():
composerMan.Play() → creates GFX event (starts flying)
m_timeToDoDamage = estimated from distance
event.Tick():
Wait → Flying → Hit (when projectile arrives)
DoDamage():
Applied based on m_timeToDoDamage countdown
```
**Issue:** DoDamage timing is independent of GFX arrival. They may not match.
**Acceptable for now:** The estimation `distance / 20.0f * 1000.0f` roughly matches the default fly speed. Perfect sync would require the event to notify when it hits, which adds coupling.
---
## 7. Testing Strategy
### 7.1 Phase 1 Testing
**Test Skills:**
1. **虎击 (Skill 1)** — `enumOnTarget`, `m_dwFlyTime = 0` → instant hit, should show hit GFX only
2. Find a skill with `m_dwFlyTime > 0` and `enumLinearMove` → should show fly → hit
**Verify:**
- [ ] Fly GFX spawns at caster position
- [ ] Fly GFX moves toward target
- [ ] Hit GFX spawns at target when projectile arrives
- [ ] Fly GFX destroyed on hit
- [ ] Damage numbers appear around same time as hit GFX
- [ ] No GFX for instant skills without fly/hit paths
**Debug Logging Points:**
```csharp
// CECAttackEvent.DoFire()
BMLogger.Log($"[GFX_FLOW] DoFire: skill={m_idSkill}, host={m_idHost}");
// A3DSkillGfxComposer.Play()
BMLogger.Log($"[GFX_FLOW] Composer.Play: targets={targets.Count}, flyGFX={flyGFX != null}");
// A3DSkillGfxEvent.Tick() state changes
BMLogger.Log($"[GFX_FLOW] Event state: {m_enumState} → {newState}");
// CECSkillGfxEvent — GFX spawn/destroy
BMLogger.Log($"[GFX_FLOW] SpawnFlyGfx at {position}");
BMLogger.Log($"[GFX_FLOW] HitTarget at {vTarget}");
```
### 7.2 Phase 2 Testing
- Test each movement mode with dedicated test skill
- Verify hook attachments (weapon tip, chest, etc.)
- Performance: 50+ active GFX events at 60 FPS
- Object pool: verify objects are reused, not leaked
### 7.3 Phase 3 Testing
- Multi-section skills (combo attack with different GFX per section)
- Goblin/pet skills
- Edge cases: target dies mid-flight, caster disconnects
- Stress test: 100+ players in PvP scenario
---
## 8. Estimated Timeline
| Phase | Duration | What's Done | What's Left |
|-------|----------|-------------|-------------|
| **Phase 1** | **3-5 days** | Core structure exists | Create movement, un-comment state machine, fix GFX refs |
| **Phase 2** | **1-2 weeks** | — | 8 more movement modes, pooling, hooks, effects |
| **Phase 3** | **1-2 weeks** | Structure exists | Config loading, goblin, polish, testing |
| **Total** | **3-5 weeks** | ~60% structural | ~40% implementation + testing |
---
## 9. Quick Reference: What to Un-comment
### A3DSkillGfxMan.cs — `A3DSkillGfxEvent`
| Line(s) | What | Action |
|---------|------|--------|
| 205-206 | `m_pMoveMethod` field | Un-comment, use IGfxMovement |
| 251 | `m_pMoveMethod = CGfxMoveBase.CreateMoveMethod(mode)` | Replace with `GfxMoveFactory.Create(mode)` |
| 336-337 | `GetMoveMethod()`, `GetMode()`, `GetHitPos()` | Un-comment |
| 343 | `SetReverse()` | Un-comment |
| 344-345 | `SetParam()`, `SetIsCluster()` | Un-comment |
| 396-414 | Hit state: hit GFX lifecycle | Un-comment, adapt to Unity GFX |
| 420-421 | Flight timeout → HitTarget | Un-comment |
| 434-448 | Wait→Flying: StartMove + fly GFX start | Un-comment, adapt |
| 454-467 | Flying: TickMove + fly GFX update | Un-comment, adapt |
### CECSkillGfxMan.cs — `CECSkillGfxEvent`
| Line(s) | What | Action |
|---------|------|--------|
| 37-53 | `GetOriginalHost()` / `GetOriginalTarget()` | Un-comment |
| 125-195 | `Tick()` position update logic | Un-comment |
| 210-239 | `HitTarget()` special effects | Un-comment (Phase 2) |
### CECAttacksMan.cs — `A3DSkillGfxComposer`
| Line(s) | What | Action |
|---------|------|--------|
| 540-551 | `SpawnGFX()` | **DELETE** this temp hack |
| 569-583 | Play() szFly/szHit | Fix to use actual GFX names |
| 644-647 | Scale calculation | Un-comment |
| 677-678 | fFlyGfxScale / fScale | Use actual values |
### CECAttacksMan.cs — `A3DSkillGfxMan.AddOneSkillGfxEvent()`
| Line | What | Action |
|------|------|--------|
| 150 | `SetReverse(bReverse)` | Un-comment |
| 193 | `pComposer.SpawnGFX(nTargetID)` | **DELETE** |
### CECAttacksMan.cs — `CECAttackEvent.DoFire()` NPC branch
| Line(s) | What | Action |
|---------|------|--------|
| 980-986 | NPC skill composerMan.Play() | Un-comment |
---
**End of Document**
This updated plan reflects the actual codebase state as of 2026-02-12. The core structure is ~60% done. The main work remaining is:
1. **Create** the movement system (new files)
2. **Un-comment** the state machine and GFX logic
3. **Fix** the GFX string/scale issues
4. **Remove** the SpawnGFX temp hack
+449
View File
@@ -0,0 +1,449 @@
# Skill GFX System - Quick Start Guide
## TL;DR - What You Need to Know
### Current Status (Updated 2026-02-12)
-**Core pipeline exists** — Attack events, timing, DoFire, DoDamage, composer loading all work
-**SkillStub has all GFX params** — MoveMode, FlyTime, TargetMode, clusters per skill
-**Composer Play() wired** — Iterates targets, calls AddOneTarget → AddSkillGfxEvent
- ⚠️ **State machine commented out** — Tick() logic exists but is commented out
- ⚠️ **GFX strings empty** — Play() passes `""` instead of actual GFX names
- ⚠️ **SpawnGFX temp hack** — Instantiates flyGFX at target pos (wrong, needs removal)
-**Movement system missing** — No IGfxMovement, no GfxLinearMove, etc.
-**GFX spawning missing** — No fly/hit GFX instantiation in event Tick()
### What's the Skill GFX System?
When a player casts a skill, you see:
1. **Fly GFX** — Projectile traveling from caster to target (e.g., fireball flying)
2. **Hit GFX** — Impact effect when it hits (e.g., explosion)
### The Flow (What's Working vs Not)
```
Player casts skill
CECAttacksMan.AddSkillAttack() ✅ Working
CECAttackEvent.Tick() → timing ✅ Working
CECAttackEvent.DoFire() ✅ Working
A3DSkillGfxComposerMan.Play() ✅ Working
A3DSkillGfxComposer.Play() ⚠️ Passes empty GFX strings
AddOneTarget() → AddSkillGfxEvent() ✅ Working (scale=0 ⚠️)
SpawnGFX() temp hack ⚠️ WRONG - needs removal
A3DSkillGfxEvent.Tick() ⚠️ State machine COMMENTED OUT
IGfxMovement.TickMove() ❌ NOT CREATED
Spawn/update fly GFX ❌ NOT IMPLEMENTED
HitTarget() → spawn hit GFX ❌ NOT IMPLEMENTED
CECAttackEvent.DoDamage() ✅ Working
```
---
## Phase 1 Implementation Guide (3-5 days)
### Step 1: Create Movement System (1 day)
Create 4 new files in `Assets/PerfectWorld/Scripts/Vfx/`:
**`IGfxMovement.cs`** — Movement interface
```csharp
namespace BrewMonster
{
public interface IGfxMovement
{
void SetMaxFlyTime(uint dwMaxFlyTime);
void StartMove(UnityEngine.Vector3 vHost, UnityEngine.Vector3 vTarget);
bool TickMove(float deltaTimeMs, UnityEngine.Vector3 vHostPos, UnityEngine.Vector3 vTargetPos);
UnityEngine.Vector3 GetPos();
UnityEngine.Vector3 GetMoveDir();
GfxMoveMode GetMode();
GfxHitPos GetHitPos();
bool IsReverse();
void SetReverse(bool bReverse);
void SetParam(GFX_SKILL_PARAM param);
void SetIsCluster(bool bCluster);
}
}
```
**`GfxLinearMove.cs`** — Straight-line projectile
```csharp
using UnityEngine;
namespace BrewMonster
{
public class GfxLinearMove : IGfxMovement
{
private Vector3 m_vPos, m_vMoveDir;
private float m_fSpeed;
private uint m_dwMaxFlyTime;
private bool m_bReverse;
private const float _fly_speed = 20.0f; // units/sec → 0.02 units/ms
public void SetMaxFlyTime(uint t) { m_dwMaxFlyTime = t; }
public void StartMove(Vector3 vHost, Vector3 vTarget)
{
m_vPos = vHost;
m_vMoveDir = vTarget - vHost;
float fDist = m_vMoveDir.magnitude;
if (fDist < 1e-4f) { m_vMoveDir = Vector3.forward; m_fSpeed = _fly_speed / 1000f; return; }
m_vMoveDir /= fDist;
float fMax = (_fly_speed / 1000f) * m_dwMaxFlyTime;
m_fSpeed = fMax >= fDist ? _fly_speed / 1000f : fDist / m_dwMaxFlyTime;
}
public bool TickMove(float dtMs, Vector3 vHostPos, Vector3 vTargetPos)
{
Vector3 dir = vTargetPos - m_vPos;
float dist = dir.magnitude;
if (dist < 1e-4f) return true;
dir /= dist;
float fly = m_fSpeed * dtMs;
if (fly >= dist) return true;
m_vPos += dir * fly;
m_vMoveDir = dir;
return false;
}
public Vector3 GetPos() => m_vPos;
public Vector3 GetMoveDir() => m_vMoveDir;
public GfxMoveMode GetMode() => GfxMoveMode.enumLinearMove;
public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter;
public bool IsReverse() => m_bReverse;
public void SetReverse(bool b) { m_bReverse = b; }
public void SetParam(GFX_SKILL_PARAM p) { }
public void SetIsCluster(bool b) { }
}
}
```
**`GfxOnTargetMove.cs`** — Instant hit (no flight)
```csharp
using UnityEngine;
namespace BrewMonster
{
public class GfxOnTargetMove : IGfxMovement
{
private Vector3 m_vPos, m_vMoveDir;
private bool m_bReverse;
public void SetMaxFlyTime(uint t) { }
public void StartMove(Vector3 vHost, Vector3 vTarget)
{
m_vPos = vTarget;
m_vMoveDir = (vTarget - vHost);
if (m_vMoveDir.magnitude > 1e-4f) m_vMoveDir.Normalize();
else m_vMoveDir = Vector3.forward;
}
public bool TickMove(float dtMs, Vector3 h, Vector3 t) { m_vPos = t; return true; }
public Vector3 GetPos() => m_vPos;
public Vector3 GetMoveDir() => m_vMoveDir;
public GfxMoveMode GetMode() => GfxMoveMode.enumOnTarget;
public GfxHitPos GetHitPos() => GfxHitPos.enumHitCenter;
public bool IsReverse() => m_bReverse;
public void SetReverse(bool b) { m_bReverse = b; }
public void SetParam(GFX_SKILL_PARAM p) { }
public void SetIsCluster(bool b) { }
}
}
```
**`GfxMoveFactory.cs`** — Factory
```csharp
namespace BrewMonster
{
public static class GfxMoveFactory
{
public static IGfxMovement Create(GfxMoveMode mode)
{
switch (mode)
{
case GfxMoveMode.enumOnTarget: return new GfxOnTargetMove();
case GfxMoveMode.enumLinearMove: return new GfxLinearMove();
default: return new GfxLinearMove(); // fallback
}
}
}
}
```
### Step 2: Wire Up A3DSkillGfxEvent State Machine (1-2 days)
**In `A3DSkillGfxMan.cs`:**
1. **Add field** (un-comment line ~205):
```csharp
protected IGfxMovement m_pMoveMethod;
```
2. **In constructor** (replace line ~251):
```csharp
m_pMoveMethod = GfxMoveFactory.Create(mode);
```
3. **Un-comment in Tick():**
- Line 420-421: Flight timeout → `HitTarget(GetTargetCenter())`
- Line 434-435: `m_pMoveMethod.SetMaxFlyTime()` + `StartMove()`
- Line 454: `m_pMoveMethod.TickMove()` → `HitTarget(GetTargetCenter())`
4. **Un-comment setters** (lines 336-345):
```csharp
public IGfxMovement GetMoveMethod() { return m_pMoveMethod; }
public GfxMoveMode GetMode() { return m_pMoveMethod.GetMode(); }
public GfxHitPos GetHitPos() { return m_pMoveMethod.GetHitPos(); }
public void SetReverse(bool bReverse) { m_pMoveMethod.SetReverse(bReverse); }
public void SetParam(GFX_SKILL_PARAM param) { m_pMoveMethod.SetParam(param); }
public void SetIsCluster(bool bCluster) { m_pMoveMethod.SetIsCluster(bCluster); }
```
5. **In AddOneSkillGfxEvent():**
- Un-comment line 150: `pEvent.SetReverse(bReverse)`
- **DELETE** line 193: `pComposer.SpawnGFX(nTargetID)` ← temp hack
### Step 3: Implement GFX Spawning in CECSkillGfxEvent (1-2 days)
**In `CECSkillGfxMan.cs` — `CECSkillGfxEvent`:**
1. **Add fields:**
```csharp
private GameObject m_flyGfxInstance;
private GameObject m_hitGfxInstance;
```
2. **Un-comment Tick() position updates** (lines 125-195)
3. **Override key behaviors — add to the class after `GetTargetCenter()`:**
```csharp
private void SpawnFlyGfx()
{
if (m_pComposer == null) return;
GameObject prefab = m_pComposer.GetFlyGFX();
if (prefab == null) return;
Vector3 pos = m_pMoveMethod.GetPos();
Vector3 dir = m_pMoveMethod.GetMoveDir();
Quaternion rot = dir.magnitude > 1e-4f ? Quaternion.LookRotation(dir) : Quaternion.identity;
m_flyGfxInstance = GameObject.Instantiate(prefab, pos, rot);
}
private void UpdateFlyGfxTransform()
{
if (m_flyGfxInstance == null) return;
m_flyGfxInstance.transform.position = m_pMoveMethod.GetPos();
Vector3 dir = m_pMoveMethod.GetMoveDir();
if (dir.magnitude > 1e-4f)
m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir);
}
private void DestroyFlyGfx()
{
if (m_flyGfxInstance != null)
{
GameObject.Destroy(m_flyGfxInstance);
m_flyGfxInstance = null;
}
}
private void SpawnHitGfx(Vector3 vTarget)
{
if (m_pComposer == null) return;
GameObject prefab = m_pComposer.GetHitGFX();
if (prefab == null) return;
Quaternion rot = Quaternion.identity;
if (m_bHostExist)
{
Vector3 dir = vTarget - m_vHostPos;
dir.y = 0;
if (dir.magnitude > 1e-3f) rot = Quaternion.LookRotation(dir);
}
m_hitGfxInstance = GameObject.Instantiate(prefab, vTarget, rot);
GameObject.Destroy(m_hitGfxInstance, 3.0f); // auto-cleanup
}
```
4. **Update HitTarget():**
```csharp
protected override void HitTarget(Vector3 vTarget)
{
base.HitTarget(vTarget);
DestroyFlyGfx();
SpawnHitGfx(vTarget);
}
```
5. **Hook into base Tick() — call SpawnFlyGfx/UpdateFlyGfx from overridden Tick:**
The base `A3DSkillGfxEvent.Tick()` handles state transitions. In `CECSkillGfxEvent.Tick()`, after the position update and `base.Tick()`, check for fly GFX updates:
```csharp
public override void Tick(uint dwDeltaTime)
{
// [un-commented position update code]
GfxSkillEventState prevState = m_enumState;
base.Tick(dwDeltaTime);
// Spawn fly GFX when entering Flying state
if (prevState == GfxSkillEventState.enumWait && m_enumState == GfxSkillEventState.enumFlying)
SpawnFlyGfx();
// Update fly GFX transform during Flying
if (m_enumState == GfxSkillEventState.enumFlying)
UpdateFlyGfxTransform();
}
```
### Step 4: Fix Composer Play() (0.5 day)
**In `CECAttacksMan.cs` — `A3DSkillGfxComposer`:**
1. **Add scale fields:**
```csharp
private float m_fFlyGfxScale = 1.0f;
private float m_fHitGfxScale = 1.0f;
```
2. **Add accessor methods:**
```csharp
public GameObject GetFlyGFX() => flyGFX;
public GameObject GetHitGFX() => hitGFX;
public GameObject GetHitGrdGFX() => hitGrdGFX;
```
3. **Fix Play() method** — replace empty strings with actual GFX info:
```csharp
string szFly = flyGFX != null ? flyGfxName : null;
string szHit = hitGFX != null ? hitGfxName : null;
```
4. **Delete SpawnGFX()** — remove the entire method (lines 540-551)
5. **Fix AddOneTarget() scale** — un-comment scale calculation, use actual fields
### Step 5: Enable NPC Skill GFX (0.5 day)
**In `CECAttacksMan.cs` — `DoFire()` NPC branch (around line 980):**
Un-comment:
```csharp
var composerMan = m_pManager.GetSkillGfxComposerMan();
if (composerMan != null)
{
composerMan.Play(m_idSkill, m_idHost, m_idCastTarget, m_targets);
pComposer = composerMan.GetSkillGfxComposer(m_idSkill);
}
```
---
## Testing Your Implementation
### Quick Test — Instant Skill (虎击, Skill 1)
**Expected behavior:**
1. Player attacks → DoFire()
2. Composer loads: `m_MoveMode = enumOnTarget`, `m_dwFlyTime = 0`
3. Movement: `GfxOnTargetMove.TickMove()` → returns `true` immediately
4. `HitTarget()` → spawns hit GFX at target
5. `m_timeToDoDamage = 1` → damage immediately
**What you should see:**
- Hit GFX appears at target position
- Damage numbers pop up
- No fly GFX (it's instant)
### Quick Test — Projectile Skill
Find a skill with `m_dwFlyTime > 0` and `m_szFlyGfxPath` set.
**Expected behavior:**
1. Fly GFX spawns at caster
2. Moves toward target linearly
3. Hit GFX spawns at target when projectile arrives
4. Fly GFX destroyed
### Debug Logging
Add these temporarily to trace the flow:
```csharp
// In A3DSkillGfxEvent.Tick() state transitions:
BMLogger.Log($"[GFX] Event {m_nHostID}→{m_nTargetID}: {oldState} → {m_enumState}");
// In CECSkillGfxEvent.SpawnFlyGfx():
BMLogger.Log($"[GFX] Spawn fly at {m_pMoveMethod.GetPos()}, prefab={m_pComposer.GetFlyGFX()?.name}");
// In CECSkillGfxEvent.HitTarget():
BMLogger.Log($"[GFX] Hit target at {vTarget}, hitGFX={m_pComposer.GetHitGFX()?.name}");
```
---
## Common Issues & Solutions
### Issue: No GFX spawning at all
**Check:**
- Is the composer loaded? → `m_pSkillGfxComposerMan.GetSkillGfxComposer(skillId) != null`
- Is `flyGFX` / `hitGFX` not null on the composer?
- Did you remove `SpawnGFX()` call from `AddOneSkillGfxEvent()`?
- Are events in `SkillGfxMan.m_EventLst`?
### Issue: GFX spawns at wrong position
**Check:**
- Is `_get_pos_by_id()` returning correct positions?
- Is movement `StartMove()` called with correct host/target?
- For OnTarget mode: GFX should appear at target, not caster
### Issue: Fly GFX doesn't move
**Check:**
- Is `m_pMoveMethod.TickMove()` being called in base `Tick()`?
- Is `UpdateFlyGfxTransform()` being called each frame?
- Is `m_dwFlyTimeSpan` set (from `m_dwFlyTime`)?
### Issue: Timing off (damage before/after GFX)
**Check:**
- `m_timeToDoDamage` calculation in `DoFire()`
- For `m_dwFlyTime = 0` skills: `m_timeToDoDamage` should be `1`
- For flying skills: `distance / 20.0f * 1000.0f` should roughly match fly speed
### Issue: SpawnGFX still being called
**Check:**
- Did you delete `pComposer.SpawnGFX(nTargetID)` from `AddOneSkillGfxEvent()` (line 193)?
- Did you delete the `SpawnGFX()` method from `A3DSkillGfxComposer`?
---
## Key Files Reference
| File | What to Change | Phase |
|------|----------------|-------|
| `IGfxMovement.cs` | **CREATE** — movement interface | 1 |
| `GfxLinearMove.cs` | **CREATE** — linear projectile | 1 |
| `GfxOnTargetMove.cs` | **CREATE** — instant hit | 1 |
| `GfxMoveFactory.cs` | **CREATE** — factory | 1 |
| `A3DSkillGfxMan.cs` | **EDIT** — un-comment state machine, add movement field | 1 |
| `CECSkillGfxMan.cs` | **EDIT** — un-comment Tick, add GFX spawning | 1 |
| `CECAttacksMan.cs` | **EDIT** — fix GFX strings, remove SpawnGFX, add scale | 1 |
---
## Estimated Time: 3-5 days for Phase 1
- Day 1: Create movement system (4 files)
- Day 2: Un-comment + wire up state machine in A3DSkillGfxMan + CECSkillGfxMan
- Day 3: Fix composer Play() GFX refs, remove SpawnGFX hack, add accessor methods
- Day 4: Testing, debugging, logging
- Day 5: Polish, edge cases
---
For the full detailed plan including Phase 2 and Phase 3, see `SKILL_GFX_CONVERSION_PLAN.md`.