Files
test/SKILL_GFX_CONVERSION_PLAN.md
T
2026-02-24 18:45:24 +07:00

37 KiB

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-24
Status: Phase 1 Complete — Movement system created, scale removed (Particle Systems)
Complexity: Medium - Core structure exists, needs wiring up


Table of Contents

  1. System Overview
  2. Current State Analysis
  3. Architecture Comparison
  4. Implementation Phases
  5. File-by-File Status
  6. Key Technical Challenges
  7. 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) ✅ WORKING
    ↓
CGfxMoveBase - Movement calculation ✅ CREATED (Linear + OnTarget)
    ↓
GFX Spawning (fly/hit) - Instantiate prefabs ✅ IMPLEMENTED (CECSkillGfxEvent)

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 Working (AddSkillGfxEvent, clustering, scale removed)
A3DSkillGfxEvent A3DSkillGfxMan.cs State machine working (Wait→Flying→Hit→Finished)
CECSkillGfxEvent CECSkillGfxMan.cs Working (Tick, SpawnFlyGfx, SpawnHitGfx, HitTarget)
SkillGfxMan CECSkillGfxMan.cs Event pool, Tick loop, GetEmptyEvent
CGfxMoveBase (Movement) CGfxMoveBase.cs Created (Linear + OnTarget, factory method)
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 CGfxMoveBase abstract class
    • 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/CGfxMoveBase.cs (same name as C++)

// Mirrors C++ CGfxMoveBase exactly:
// - StartMove / TickMove are pure virtual (= 0) → abstract
// - SetParam / SetIsCluster / SetReverse / UpdateGfxParam are virtual with default impl → virtual
// - GetMode / GetPos / GetMoveDir / IsReverse / SetMaxFlyTime are non-virtual → regular methods
public abstract class CGfxMoveBase
{
    // Fields (same as C++)
    protected GfxMoveMode m_Mode;
    protected GfxHitPos m_HitPos = GfxHitPos.enumHitCenter;
    protected Vector3 m_vPos;
    protected Vector3 m_vMoveDir;
    protected bool m_bOneOfCluser;  // C++ spelling kept
    protected uint m_dwMaxFlyTime;
    protected bool m_bReverse;
    protected bool m_bArea;
    protected EmitShape m_Shape;
    protected Vector3 m_vSize;
    protected Vector3 m_vXRange;
    protected Vector3 m_vYRange;
    protected Vector3 m_vZRange;
    protected float m_fSquare;
    protected float m_fSquareH;

    protected CGfxMoveBase(GfxMoveMode mode)
    {
        m_Mode = mode;
        m_HitPos = GfxHitPos.enumHitCenter;
    }

    // Pure virtual (= 0) → abstract — subclasses MUST override
    public abstract void StartMove(Vector3 vHost, Vector3 vTarget);
    public abstract bool TickMove(uint dwDeltaTime, Vector3 vHostPos, Vector3 vTargetPos);

    // Protected helpers (same as C++ CGfxMoveBase)
    protected static float Normalize(ref Vector3 v)
    {
        float mag = v.magnitude;
        if (mag < 1e-6f) { v = Vector3.zero; return 0f; }
        v /= mag;
        return mag;
    }

    protected void CalcRange(Vector3 vDir)
    {
        m_vYRange = Vector3.up;
        m_vZRange = new Vector3(vDir.x, 0, vDir.z);
        if (Normalize(ref m_vZRange) < 0.01f) m_vZRange = Vector3.forward;
        m_vXRange = Vector3.Cross(m_vYRange, m_vZRange);
        m_vXRange *= m_vSize.x;
        m_vYRange *= m_vSize.y;
        m_vZRange *= m_vSize.z;
        m_fSquare = m_vSize.sqrMagnitude;
        m_fSquareH = m_vSize.x * m_vSize.x + m_vSize.z * m_vSize.z;
    }

    protected Vector3 GetRandOff()
    {
        // Mirrors C++ GetRandOff() with enumBox/enumSphere/enumCylinder
        float x, y, z;
        if (m_Shape == EmitShape.enumBox)
            return SymRandom() * m_vXRange + SymRandom() * m_vYRange + SymRandom() * m_vZRange;
        else if (m_Shape == EmitShape.enumSphere)
        {
            do { x = SymRandom(); y = SymRandom(); z = SymRandom(); }
            while (x*x + y*y + z*z > 1f);
            return x * m_vXRange + y * m_vYRange + z * m_vZRange;
        }
        else if (m_Shape == EmitShape.enumCylinder)
        {
            y = SymRandom();
            do { x = SymRandom(); z = SymRandom(); }
            while (x*x + z*z > 1f);
            return x * m_vXRange + y * m_vYRange + z * m_vZRange;
        }
        return Vector3.zero;
    }

    private static float SymRandom() { return UnityEngine.Random.value * 2f - 1f; }

    // Virtual with default implementation — subclasses CAN override, but don't have to
    public virtual void SetParam(GFX_SKILL_PARAM param)
    {
        m_bArea = param.m_bArea;
        m_Shape = param.m_Shape;
        m_vSize = new Vector3(param.m_vSize.x, param.m_vSize.y, param.m_vSize.z);
    }
    public virtual void SetIsCluster(bool bCluster) { m_bOneOfCluser = bCluster; }
    public virtual void SetReverse(bool bReverse) { m_bReverse = bReverse; }
    public virtual void UpdateGfxParam(Vector3 vHost, Vector3 vTarget) { }

    // Non-virtual — same as C++ (no override possible)
    public GfxMoveMode GetMode() { return m_Mode; }
    public GfxHitPos GetHitPos() { return m_HitPos; }
    public Vector3 GetPos() { return m_vPos; }
    public Vector3 GetMoveDir() { return m_vMoveDir; }
    public bool IsReverse() { return m_bReverse; }
    public void SetMaxFlyTime(uint dwTime) { m_dwMaxFlyTime = dwTime; if (m_dwMaxFlyTime == 0) m_dwMaxFlyTime = 1; }
    public void SetIsArea(bool bArea) { m_bArea = bArea; }
    public void SetShape(EmitShape shape) { m_Shape = shape; }
    public void SetRange(Vector3 vSize) { m_vSize = vSize; }

    // Factory method (same as C++ CGfxMoveBase::CreateMoveMethod)
    public static CGfxMoveBase CreateMoveMethod(GfxMoveMode mode)
    {
        switch (mode)
        {
            case GfxMoveMode.enumLinearMove:    return new CGfxLinearMove(mode);
            case GfxMoveMode.enumOnTarget:      return new CGfxOnTargetMove(mode);
            // Phase 2: add more modes
            default:                            return new CGfxOnTargetMove(mode);
        }
    }
}

Assets/PerfectWorld/Scripts/Vfx/CGfxLinearMove.cs (C++ ref: A3DSkillGfxEvent2.cpp:22-52)

// Mirrors C++ CGfxLinearMove exactly (A3DSkillGfxEvent2.cpp:22-52)
public class CGfxLinearMove : CGfxMoveBase
{
 protected float m_fSpeed;
 private const float _fly_speed = 20.0f / 1000.0f; // same as C++

 public CGfxLinearMove(GfxMoveMode mode) : base(mode) { }

 public override void StartMove(Vector3 vHost, Vector3 vTarget)
 {
     if (m_bArea)
     {
         CalcRange((vTarget - vHost).normalized);
         m_vPos = vHost + GetRandOff();
     }
     else
         m_vPos = vHost;

     m_vMoveDir = vTarget - m_vPos;
     float fDist = Normalize(ref m_vMoveDir);
     float fMax = _fly_speed * m_dwMaxFlyTime;

     if (fMax >= fDist)
         m_fSpeed = _fly_speed;
     else
         m_fSpeed = fDist / m_dwMaxFlyTime;
 }

 public override bool TickMove(uint dwDeltaTime, Vector3 vHostPos, Vector3 vTargetPos)
 {
     Vector3 vFlyDir = vTargetPos - m_vPos;
     float fDist = Normalize(ref vFlyDir);
     float fFlyDist = m_fSpeed * dwDeltaTime;

     if (fFlyDist >= fDist) return true; // target hit
     m_vPos += vFlyDir * fFlyDist;
     m_vMoveDir = vFlyDir;
     return false;
 }
}

Assets/PerfectWorld/Scripts/Vfx/CGfxOnTargetMove.cs (for instant-hit skills like 虎击)

// Mirrors C++ CGfxOnTargetMove exactly (A3DSkillGfxEvent2.cpp:297-322)
public class CGfxOnTargetMove : CGfxMoveBase
{
 protected float m_fRadius;
 protected Vector3 m_vOffset;

 public CGfxOnTargetMove(GfxMoveMode mode) : base(mode) { m_HitPos = GfxHitPos.enumHitBottom; m_fRadius = 0; }

 public override void StartMove(Vector3 vHost, Vector3 vTarget)
 {
     m_vPos = vTarget;
     m_vMoveDir = vTarget - vHost;
     m_vMoveDir.y = 0;  // C++: zero out Y before normalize
     if (Normalize(ref m_vMoveDir) == 0)
         m_vMoveDir = Vector3.forward; // _unit_z

     if (m_bOneOfCluser)
     {
         float fRandAng = UnityEngine.Random.value * Mathf.PI * 2f;
         float fRadius = UnityEngine.Random.value * m_fRadius;
         m_vOffset.x = Mathf.Cos(fRandAng) * fRadius;
         m_vOffset.z = Mathf.Sin(fRandAng) * fRadius;
         m_vOffset.y = 0;
         m_vPos += m_vOffset;
     }
     else
         m_vOffset = Vector3.zero;
 }

 public override bool TickMove(uint dwDeltaTime, Vector3 vHostPos, Vector3 vTargetPos)
 {
     m_vPos = vTargetPos + m_vOffset;
     return false; // C++ returns false — hit is triggered by fly time timeout, NOT by TickMove
 }

 public override void SetParam(GFX_SKILL_PARAM param)
 {
     base.SetParam(param);
     m_fRadius = param.value.fVal;  // C# union access: param.value.fVal
 }
}

(No separate factory file needed — CGfxMoveBase.CreateMoveMethod() handles creation, same as C++)

Task 1.2: Un-comment & Wire Up Event State Machine (1-2 days)

Changes to A3DSkillGfxMan.csA3DSkillGfxEvent class:

  1. Add m_pMoveMethod field (un-comment):

    protected CGfxMoveBase m_pMoveMethod;
    
  2. In constructor, create movement:

    m_pMoveMethod = CGfxMoveBase.CreateMoveMethod(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.csCECSkillGfxEvent class:

  1. Add GFX instance fields:

    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.csAddOneSkillGfxEvent():

  1. Remove the temporary pComposer.SpawnGFX(nTargetID) call (line 193)

Task 1.3: Fix Composer Play() GFX References (0.5 day)

Changes to CECAttacksMan.csA3DSkillGfxComposer.Play():

  1. Pass actual GFX names instead of empty strings:

    string szFly = flyGFX != null ? flyGfxName : null;
    string szHit = hitGFX != null ? hitGfxName : null;
    
  2. Add m_fFlyGfxScale and m_fHitGfxScale fields REMOVED — Unity Particle Systems handle their own scale (see agent-skills/11-gfx-to-particle-system.md)

  3. Un-comment scale calculation in AddOneTarget() REMOVED — not needed for Particle Systems

  4. Make GFX prefab references accessible to events:

    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.csDoFire() 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 CGfxLinearMove lines 22-52 Done in Phase 1
OnTarget CGfxOnTargetMove Done in Phase 1
Parabolic CGfxParabolicMove lines 54-92 Medium High
Missile CGfxMissileMove lines 94-142 High High
Meteoric CGfxMeteoricMove lines 144-188 Low Medium
Accelerated CGfxAccMove lines 319-370 Low Medium
Helix CGfxHelixMove lines 190-256 Medium Low
Curved CGfxCurvedMove lines 258-317 High Low
Link CGfxLinkMove lines 372-420 Medium Low
Random CGfxRandMove lines 422-458 Medium Low

Pattern: Each extends CGfxMoveBase, added to CGfxMoveBase.CreateMoveMethod()

Task 2.2: GFX Lifecycle Management (2-3 days)

  1. Object Pooling — Replace Instantiate/Destroy with pool:

    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:

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

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

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:

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)

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

#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 CGfxMoveBase 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
CGfxMoveBase.cs Phase 1 Movement abstract base class (same as C++)
CGfxLinearMove.cs Phase 1 Straight-line projectile
CGfxOnTargetMove.cs Phase 1 Instant-hit (no flight)
(no separate factory file) Factory is CGfxMoveBase.CreateMoveMethod(), same as C++
CGfxParabolicMove.cs Phase 2 Arc trajectory
CGfxMissileMove.cs Phase 2 Homing missile
CGfxMeteoricMove.cs Phase 2 Falls from sky
CGfxAccMove.cs Phase 2 Accelerated flight
CGfxHelixMove.cs Phase 2 Spiral path
CGfxCurvedMove.cs Phase 2 Bezier curve
CGfxLinkMove.cs Phase 2 Lightning chain
CGfxRandMove.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:

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

// 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 CGfxMoveBase
251 m_pMoveMethod = CGfxMoveBase.CreateMoveMethod(mode) Un-comment (same name as C++)
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 plan was last updated 2026-02-24. Phase 1 is now complete (~95%):

  1. Movement system created (CGfxMoveBase, CGfxLinearMove, CGfxOnTargetMove)
  2. State machine working (Wait→Flying→Hit→Finished)
  3. GFX spawning via CECSkillGfxEvent (SpawnFlyGfx, SpawnHitGfx)
  4. Scale parameters removed — Unity Particle Systems handle their own scale
  5. SkillGfxMan.Tick() wired into Update loop
  6. NPC skill GFX path wired up

Remaining Phase 2+ work:

  • Additional movement modes (Parabolic, Missile, Helix, etc.)
  • Hook/bone attachment for GFX positions
  • Multi-section skill LoadConfig
  • Optimization (LOD, culling)