skill behavior system plan
This commit is contained in:
@@ -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
|
||||
@@ -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`.
|
||||
Reference in New Issue
Block a user