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

37 KiB

Hook System Conversion Plan (C++ to Unity C#)

Executive Summary

This document provides a comprehensive plan to convert the Perfect World Hook System from C++ to Unity C#. The Hook System enables attaching visual effects (GFX) to specific bone positions on character models, such as weapon tips, hands, chest, etc.

Date Created: 2026-02-24
Status: Planning Phase — Core structure exists, needs implementation
Complexity: Medium — Requires integration with existing skeleton system
Dependencies: SkeletonBuilder.cs ( Complete), CECSkillGfxMan.cs (⚠️ Hook logic stubbed)

Related Documents:

  • HOOK_SYSTEM_C++_REFERENCE.md - C++ implementation reference and conversion verification

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 Hook System?

The Hook System provides named attachment points on character skeletons for positioning visual effects:

  • Hooks: Named Transform positions attached to bones (e.g., "weapon_tip", "chest", "hand_left")
  • Hook Lookup: Find hook Transform by name from a character model
  • Hook Positioning: Calculate world position/rotation from hook Transform with optional offsets
  • Child Models: Support for weapon/pet sub-models with their own hooks (hangers)

1.2 Use Cases

  1. Skill GFX Positioning (CECSkillGfxEvent.get_pos_by_id())

    • Attach fly GFX to caster's weapon tip
    • Attach hit GFX to target's chest/head
    • Support relative/absolute offsets
  2. Model GFX Attachment (CECModel.PlayGfx())

    • Attach persistent effects to character hooks
    • Support fade-out and scale factors
  3. Animation Events

    • Spawn effects at specific bone positions during animations

1.3 Flow Diagram

Character Model Loaded
    ↓
SkeletonBuilder.BuildSkeleton() - Creates hook GameObjects ✅
    ↓
Hooks stored as child GameObjects of bones ✅
    ↓
CECSkillGfxEvent.get_pos_by_id() - Lookup hook by name ⚠️ TODO
    ↓
Get hook Transform position/rotation
    ↓
Apply offset (relative or absolute)
    ↓
Return world position for GFX spawning

2. Current State Analysis

2.1 What's Fully Working

  1. SkeletonBuilder Hook Creation (SkeletonBuilder.cs:147-156)

    • Creates GameObject for each hook from skeleton.m_aHooks
    • Parents hook GameObject to bone Transform
    • Sets local position/rotation/scale from hook matrix
    • Hooks exist in scene hierarchy
  2. Hook Data Structure (A3DSkeletonHook)

    • Hook name (m_strName)
    • Bone index (Data.iBone)
    • Transform matrix (Data.matHookTM)
    • Data loaded from model files
  3. Skeleton Structure (SkeletonBuilder.cs)

    • bones list: All bone Transforms
    • rootBone: Root bone Transform
    • GetBones() / GetBoneNoGC(): Bone lookup methods
    • Skeleton hierarchy built correctly

2.2 What's Partially Done ⚠️ (Structure Exists, Logic Commented Out)

  1. CECSkillGfxEvent.get_pos_by_id() (CECSkillGfxMan.cs:442-578)

    • Player hook lookup: Commented out (lines 484-509)
    • NPC hook lookup: Commented out (lines 542-554)
    • Currently falls back to character center position
    • Hook parameters (szHook, bRelHook, vOffset, szHanger, bChildHook) are passed but ignored
  2. CECModel Hook Access (CECModel.cs)

    • PlayGfx() method exists but hook lookup is commented out (lines 422-426)
    • GetECMHook() method not implemented
    • Hook scale factor support missing
  3. Child Model Support (CECModel.GetChildModel())

    • Referenced in comments but not implemented
    • Needed for weapon/pet sub-models with separate hooks

2.3 What's Completely Missing

  1. Hook Lookup API

    • No GetHook(string hookName) method on CECModel/CECPlayer/CECNPC
    • No GetSkeletonHook() equivalent
    • No hook Transform caching for performance
  2. Hook Position Calculation

    • No GetAbsoluteTM() equivalent (world transform from hook)
    • No relative vs absolute offset calculation
    • No hook-to-world-space conversion
  3. Child Model System

    • No GetChildModel(string hangerName) method
    • No child model hook lookup
    • No hanger hierarchy support
  4. Hook Caching

    • No Transform cache to avoid repeated lookups
    • No invalidation on model reload

3. Architecture Comparison

3.1 C++ Architecture (Original)

// Hook structure
class A3DSkeletonHook {
    string m_strName;           // Hook name
    int iBone;                  // Parent bone index
    Matrix4x4 matHookTM;        // Local transform matrix
    Matrix4x4 GetAbsoluteTM(); // Get world transform
};

// Model access
class CECModel {
    A3DSkinModel* GetA3DSkinModel();
    CECModel* GetChildModel(string szHanger);
    A3DSkeletonHook* GetSkeletonHook(string szHook, bool bRecursive);
};

// Usage in GFX positioning
CECModel* pModel = pPlayer->GetPlayerModel();
A3DSkeletonHook* pHook = pModel->GetA3DSkinModel()->GetSkeletonHook(szHook, true);
Vector3 vPos = pHook->GetAbsoluteTM() * pOffset;  // Relative offset
// OR
Vector3 vPos = pSkin->GetAbsoluteTM() * pOffset;
vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);  // Absolute offset

3.2 Current C# Architecture

// Hook creation (SkeletonBuilder.cs)
GameObject hookGO = new GameObject(hook.m_strName);
hookGO.transform.parent = boneGameObjects[hook.Data.iBone].transform;
hookGO.transform.localPosition = ...;
hookGO.transform.localRotation = ...;

// Hook lookup (MISSING)
// No GetHook() method exists

// Usage in GFX positioning (STUBBED)
// CECSkillGfxEvent.get_pos_by_id() - TODO comments only

3.3 Target C# Architecture

// Hook lookup API
public class CECModel {
    public Transform GetHook(string hookName, bool recursive = true);
    public CECModel GetChildModel(string hangerName);
    private Dictionary<string, Transform> m_hookCache;  // Performance cache
}

// Hook position calculation
public static class HookUtils {
    public static Vector3 GetHookWorldPosition(Transform hookTransform, Vector3 offset, bool bRelative);
    public static Quaternion GetHookWorldRotation(Transform hookTransform);
}

// Usage in GFX positioning
CECModel pModel = pPlayer.GetPlayerModel();
Transform pHook = pModel.GetHook(szHook, true);
Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);

4. Implementation Phases

Phase 1: Core Hook Lookup API (2-3 days)

Goal: Enable basic hook lookup by name from character models

Task 1.1: Add Hook Lookup to SkeletonBuilder (0.5 day)

File: Assets/ModelRenderer/Scripts/SkinnedMesh/SkeletonBuilder.cs

Changes:

  1. Add hook storage dictionary:

    public Dictionary<string, Transform> hooks = new Dictionary<string, Transform>();
    
  2. Store hooks during BuildSkeleton():

    // In BuildSkeleton(), after creating hook GameObject (line 151):
    hooks[hook.m_strName] = hookGO.transform;
    
  3. Add public lookup method:

    /// <summary>
    /// Get hook Transform by name
    /// 根据名称获取挂点变换
    /// </summary>
    /// <param name="hookName">Hook name / 挂点名称</param>
    /// <param name="recursive">Search in child models / 在子模型中搜索</param>
    /// <returns>Hook Transform or null if not found / 挂点变换,未找到返回null</returns>
    public Transform GetHook(string hookName, bool recursive = true)
    {
        if (string.IsNullOrEmpty(hookName))
            return null;
    
        // Direct lookup
        if (hooks.TryGetValue(hookName, out Transform hook))
            return hook;
    
        // Recursive search in child models (Phase 2)
        if (recursive)
        {
            // TODO Phase 2: Search child models
        }
    
        return null;
    }
    

Task 1.2: Add Hook Access to CECModel (1 day)

File: Assets/PerfectWorld/Scripts/NPC/CECModel.cs

Changes:

  1. Add SkeletonBuilder reference:

    private SkeletonBuilder m_skeletonBuilder;
    
    public void SetSkeletonBuilder(SkeletonBuilder builder)
    {
        m_skeletonBuilder = builder;
        m_hookCache?.Clear();  // Invalidate cache on model change
    }
    
    /// <summary>
    /// Get the SkeletonBuilder component
    /// 获取SkeletonBuilder组件
    /// </summary>
    public SkeletonBuilder GetSkeletonBuilder()
    {
        return m_skeletonBuilder;
    }
    
  2. Add Auto-Initialization Logic:

    /// <summary>
    /// Initialize SkeletonBuilder reference - call this after model is loaded
    /// 初始化SkeletonBuilder引用 - 在模型加载后调用
    /// </summary>
    public void InitializeSkeletonBuilder()
    {
        // Try to find SkeletonBuilder in children (where it's typically attached)
        // 尝试在子对象中查找SkeletonBuilder(通常附加在那里)
        m_skeletonBuilder = GetComponentInChildren<SkeletonBuilder>(true);
    
        if (m_skeletonBuilder == null)
        {
            // Fallback: search in parent hierarchy (for NPCs/Players)
            // 回退:在父层次结构中搜索(用于NPC/玩家)
            m_skeletonBuilder = GetComponentInParent<SkeletonBuilder>();
        }
    
        if (m_skeletonBuilder == null)
        {
            BMLogger.LogWarning($"[CECModel] SkeletonBuilder not found for {gameObject.name}. Hooks will not be available.");
        }
        else
        {
            // Clear cache when skeleton is set
            // 设置骨架时清除缓存
            m_hookCache?.Clear();
            BMLogger.Log($"[CECModel] SkeletonBuilder initialized for {gameObject.name}");
        }
    }
    
    /// <summary>
    /// Auto-initialize on Awake if SkeletonBuilder is already available
    /// 如果SkeletonBuilder已可用,在Awake时自动初始化
    /// </summary>
    private void Awake()
    {
        // Only auto-initialize if SkeletonBuilder exists in hierarchy
        // 仅在层次结构中存在SkeletonBuilder时自动初始化
        if (GetComponentInChildren<SkeletonBuilder>(true) != null || 
            GetComponentInParent<SkeletonBuilder>() != null)
        {
            InitializeSkeletonBuilder();
        }
    }
    
  3. Add hook cache:

    private Dictionary<string, Transform> m_hookCache = new Dictionary<string, Transform>();
    
  4. Add GetHook method:

    /// <summary>
    /// Get hook Transform by name
    /// 根据名称获取挂点变换
    /// </summary>
    /// <param name="hookName">Hook name / 挂点名称</param>
    /// <param name="recursive">Search recursively / 递归搜索</param>
    /// <returns>Hook Transform or null / 挂点变换,未找到返回null</returns>
    public Transform GetHook(string hookName, bool recursive = true)
    {
        if (m_skeletonBuilder == null)
            return null;
    
        if (string.IsNullOrEmpty(hookName))
            return null;
    
        // Check cache first
        if (m_hookCache.TryGetValue(hookName, out Transform cachedHook))
        {
            if (cachedHook != null)  // Unity "fake null" check
                return cachedHook;
            m_hookCache.Remove(hookName);  // Remove invalid entry
        }
    
        // Lookup from skeleton
        Transform hook = m_skeletonBuilder.GetHook(hookName, recursive);
    
        if (hook != null)
            m_hookCache[hookName] = hook;  // Cache for performance
    
        return hook;
    }
    
  5. Add cache invalidation:

    public void InvalidateHookCache()
    {
        m_hookCache.Clear();
    }
    
  6. Update GetHook to handle uninitialized state:

    public Transform GetHook(string hookName, bool recursive = true)
    {
        // Auto-initialize if not set (lazy initialization)
        // 如果未设置则自动初始化(延迟初始化)
        if (m_skeletonBuilder == null)
        {
            InitializeSkeletonBuilder();
            if (m_skeletonBuilder == null)
                return null;  // Still no skeleton found
        }
    
        if (string.IsNullOrEmpty(hookName))
            return null;
    
        // Check cache first
        if (m_hookCache.TryGetValue(hookName, out Transform cachedHook))
        {
            if (cachedHook != null)  // Unity "fake null" check
                return cachedHook;
            m_hookCache.Remove(hookName);  // Remove invalid entry
        }
    
        // Lookup from skeleton
        Transform hook = m_skeletonBuilder.GetHook(hookName, recursive);
    
        if (hook != null)
            m_hookCache[hookName] = hook;  // Cache for performance
    
        return hook;
    }
    

Task 1.3: Add Hook Access to CECPlayer (0.5 day)

File: Find CECPlayer.cs (likely in Assets/PerfectWorld/Scripts/Player/)

Changes:

  1. Add GetPlayerModel method (if missing):

    public CECModel GetPlayerModel()
    {
        // TODO: Return player's CECModel instance
        // This should return the model component attached to the player GameObject
        return GetComponent<CECModel>();
    }
    
  2. Add convenience GetHook method:

    public Transform GetHook(string hookName, bool recursive = true)
    {
        CECModel model = GetPlayerModel();
        return model?.GetHook(hookName, recursive);
    }
    

Task 1.4: Add Hook Access to CECNPC (0.5 day)

File: Find CECNPC.cs (likely in Assets/PerfectWorld/Scripts/NPC/)

Changes:

  1. Add GetModel method (if missing):

    public CECModel GetModel()
    {
        return GetComponent<CECModel>();
    }
    
  2. Add convenience GetHook method:

    public Transform GetHook(string hookName, bool recursive = true)
    {
        CECModel model = GetModel();
        return model?.GetHook(hookName, recursive);
    }
    

Task 1.5: Wire Up SkeletonBuilder Initialization (0.5 day)

Integration Points:

  1. Model Loading Integration:

    • After SkeletonBuilder.BuildSkeleton() is called, call CECModel.InitializeSkeletonBuilder()
    • This ensures hooks are available immediately after skeleton is built
  2. Character Creation Integration:

    • In CECPlayer / CECNPC initialization, after model is loaded:
      CECModel model = GetComponent<CECModel>();
      if (model != null)
      {
          model.InitializeSkeletonBuilder();
      }
      
  3. Model Reload Handling:

    • When model is reloaded (equipment change, etc.), call InitializeSkeletonBuilder() again
    • This updates the SkeletonBuilder reference and clears cache

Example Integration in Model Loader:

// After SkeletonBuilder.BuildSkeleton() completes:
void OnSkeletonBuilt(SkeletonBuilder skeleton)
{
    CECModel model = GetComponent<CECModel>();
    if (model != null)
    {
        model.SetSkeletonBuilder(skeleton);
        // Or call InitializeSkeletonBuilder() which does the same
    }
}

Task 1.6: Create HookUtils Helper Class (0.5 day)

New File: Assets/PerfectWorld/Scripts/Utility/HookUtils.cs

using UnityEngine;

namespace BrewMonster
{
    /// <summary>
    /// Utility functions for hook position/rotation calculation
    /// 挂点位置/旋转计算的工具函数
    /// </summary>
    public static class HookUtils
    {
        /// <summary>
        /// Get world position from hook Transform with offset
        /// 从挂点变换获取世界位置(带偏移)
        /// </summary>
        /// <param name="hookTransform">Hook Transform / 挂点变换</param>
        /// <param name="offset">Offset vector / 偏移向量</param>
        /// <param name="bRelative">If true, offset is relative to hook's local space; if false, absolute / 如果为true,偏移相对于挂点局部空间;如果为false,绝对偏移</param>
        /// <param name="modelTransform">Model transform for absolute offset calculation (optional) / 用于绝对偏移计算的模型变换(可选)</param>
        /// <returns>World position / 世界位置</returns>
        public static Vector3 GetHookWorldPosition(Transform hookTransform, Vector3 offset, bool bRelative, Transform modelTransform = null)
        {
            if (hookTransform == null)
                return Vector3.zero;
            
            if (bRelative)
            {
                // Relative offset: transform offset from hook's local space to world space
                // 相对偏移:将偏移从挂点的局部空间变换到世界空间
                // C++ equivalent: pHook->GetAbsoluteTM() * pOffset
                return hookTransform.TransformPoint(offset);
            }
            else
            {
                // Absolute offset: transform offset in model space, then translate to hook position
                // 绝对偏移:在模型空间中变换偏移,然后平移到挂点位置
                // C++ equivalent: 
                //   vPos = pSkin->GetAbsoluteTM() * pOffset;
                //   vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
                
                // Get model transform (skeleton root or provided)
                // 获取模型变换(骨架根或提供的)
                if (modelTransform == null)
                {
                    // Fallback: find SkeletonBuilder in hierarchy and use root bone
                    // 回退:在层次结构中查找SkeletonBuilder并使用根骨骼
                    SkeletonBuilder skeleton = hookTransform.GetComponentInParent<SkeletonBuilder>();
                    modelTransform = skeleton != null ? skeleton.rootBone : hookTransform.root;
                }
                
                // Transform offset in model's world space
                // 在模型的世界空间中变换偏移
                Vector3 offsetWorld = modelTransform.TransformPoint(offset);
                
                // Translate to hook position
                // 平移到挂点位置
                return offsetWorld - modelTransform.position + hookTransform.position;
            }
        }
        
        /// <summary>
        /// Get world rotation from hook Transform
        /// 从挂点变换获取世界旋转
        /// </summary>
        /// <param name="hookTransform">Hook Transform / 挂点变换</param>
        /// <returns>World rotation / 世界旋转</returns>
        public static Quaternion GetHookWorldRotation(Transform hookTransform)
        {
            if (hookTransform == null)
                return Quaternion.identity;
            
            return hookTransform.rotation;
        }
        
        /// <summary>
        /// Get world direction from hook Transform
        /// 从挂点变换获取世界方向
        /// </summary>
        /// <param name="hookTransform">Hook Transform / 挂点变换</param>
        /// <returns>Forward direction in world space / 世界空间的前方向</returns>
        public static Vector3 GetHookWorldForward(Transform hookTransform)
        {
            if (hookTransform == null)
                return Vector3.forward;
            
            return hookTransform.forward;
        }
    }
}

Phase 1 Deliverables:

  • Hook lookup by name from SkeletonBuilder
  • GetHook() methods on CECModel, CECPlayer, CECNPC
  • CECModel initialization logic (auto-detect SkeletonBuilder)
  • Hook position calculation utilities
  • Basic hook caching for performance
  • Integration points for model loading

Phase 2: Wire Up GFX Positioning (2-3 days)

Goal: Enable skill GFX to use hooks for positioning

Task 2.1: Implement Player Hook Lookup in get_pos_by_id() (1 day)

File: Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs

Changes to get_pos_by_id() method (lines 442-578):

  1. Un-comment and adapt player hook lookup (lines 484-509):

    // Replace the TODO block (lines 484-509) with:
    if (!string.IsNullOrEmpty(szHook))
    {
        CECModel pModel = pPlayer.GetPlayerModel();
        if (pModel == null)
            break;
    
        // Handle child model (hanger) if specified
        // 如果指定了子模型(挂载者),则处理
        if (!string.IsNullOrEmpty(szHanger) && bChildHook)
        {
            pModel = pModel.GetChildModel(szHanger);
            if (pModel == null)
                break;
        }
    
        // Get hook Transform
        // 获取挂点变换
        Transform pHook = pModel.GetHook(szHook, true);
        if (pHook == null)
            break;
    
        // Get model transform for absolute offset calculation
        // 获取模型变换用于绝对偏移计算
        Transform modelTransform = pModel.transform;  // Or get from SkeletonBuilder if available
    
        // Calculate position based on relative/absolute offset
        // 根据相对/绝对偏移计算位置
        vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
    
        return true;
    }
    
  2. Ensure fallback to center position if hook not found (existing code handles this)

Task 2.2: Implement NPC Hook Lookup in get_pos_by_id() (1 day)

File: Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs

Changes to get_pos_by_id() method (NPC branch, lines 532-575):

  1. Un-comment and adapt NPC hook lookup (lines 542-554):
    // Replace the TODO block (lines 542-554) with:
    if (!string.IsNullOrEmpty(szHook))
    {
        // Get NPC model
        // 获取NPC模型
        CECModel pModel = pNPC.GetModel();
        if (pModel == null)
            break;
    
        // Handle child model (hanger) if specified
        // 如果指定了子模型(挂载者),则处理
        if (!string.IsNullOrEmpty(szHanger) && bChildHook)
        {
            pModel = pModel.GetChildModel(szHanger);
            if (pModel == null)
                break;
        }
    
        // Get hook Transform
        // 获取挂点变换
        Transform pHook = pModel.GetHook(szHook, true);
        if (pHook == null)
            break;
    
        // Get model transform for absolute offset calculation
        // 获取模型变换用于绝对偏移计算
        Transform modelTransform = pModel.transform;  // Or get from SkeletonBuilder if available
    
        // Calculate position based on relative/absolute offset
        // 根据相对/绝对偏移计算位置
        vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
    
        return true;
    }
    

Task 2.3: Add Debug Logging (0.5 day)

File: Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs

Add logging to hook lookup:

// In get_pos_by_id(), after successful hook lookup:
#if UNITY_EDITOR
BMLogger.Log($"[HOOK_DEBUG] Found hook '{szHook}' for ID {nID}, position={vPos}, relative={bRelHook}");
#endif

Add logging for hook not found:

// In get_pos_by_id(), when hook lookup fails:
#if UNITY_EDITOR
if (!string.IsNullOrEmpty(szHook))
{
    BMLogger.LogWarning($"[HOOK_DEBUG] Hook '{szHook}' not found for ID {nID}, falling back to center position");
}
#endif

Task 2.4: Testing & Validation (0.5 day)

  • Test with skills that use hooks (check SkillStub for hook names)
  • Verify fly GFX spawns at correct hook position
  • Verify hit GFX spawns at target hook position
  • Test with/without offsets (relative vs absolute)
  • Verify fallback to center when hook not found

Phase 2 Deliverables:

  • Player hook positioning in skill GFX
  • NPC hook positioning in skill GFX
  • Relative and absolute offset support
  • Debug logging for troubleshooting

Phase 3: Child Model & Advanced Features (2-3 days)

Goal: Support weapon/pet sub-models and advanced hook features

Task 3.1: Implement GetChildModel() (1 day)

File: Assets/PerfectWorld/Scripts/NPC/CECModel.cs

Changes:

  1. Add child model storage:

    private Dictionary<string, CECModel> m_childModels = new Dictionary<string, CECModel>();
    
    public void RegisterChildModel(string hangerName, CECModel childModel)
    {
        if (!string.IsNullOrEmpty(hangerName) && childModel != null)
        {
            m_childModels[hangerName] = childModel;
        }
    }
    
    public void UnregisterChildModel(string hangerName)
    {
        m_childModels.Remove(hangerName);
    }
    
    public CECModel GetChildModel(string hangerName)
    {
        if (string.IsNullOrEmpty(hangerName))
            return null;
    
        return m_childModels.TryGetValue(hangerName, out CECModel child) ? child : null;
    }
    
  2. Wire up child model registration when weapons/pets are equipped

Task 3.2: Recursive Hook Search (0.5 day)

File: Assets/ModelRenderer/Scripts/SkinnedMesh/SkeletonBuilder.cs

Update GetHook() to support recursive search:

public Transform GetHook(string hookName, bool recursive = true)
{
    if (string.IsNullOrEmpty(hookName))
        return null;
    
    // Direct lookup
    if (hooks.TryGetValue(hookName, out Transform hook))
        return hook;
    
    // Recursive search in child models (if recursive flag is true)
    // 在子模型中递归搜索(如果递归标志为true)
    if (recursive)
    {
        // Search in child SkeletonBuilders (weapons, pets, etc.)
        // 在子SkeletonBuilder中搜索(武器、宠物等)
        SkeletonBuilder[] childBuilders = GetComponentsInChildren<SkeletonBuilder>(true);
        foreach (SkeletonBuilder childBuilder in childBuilders)
        {
            if (childBuilder == this) continue;  // Skip self
            
            Transform childHook = childBuilder.GetHook(hookName, false);  // Don't recurse again
            if (childHook != null)
                return childHook;
        }
    }
    
    return null;
}

Task 3.3: Implement CECModel.PlayGfx() Hook Support (1 day)

File: Assets/PerfectWorld/Scripts/NPC/CECModel.cs

Un-comment and adapt PlayGfx() hook lookup (lines 422-426):

// In PlayGfx() method, replace commented code with:
if (bUseECMHook && !string.IsNullOrEmpty(szHook))
{
    Transform pHook = GetHook(szHook, true);
    if (pHook != null)
    {
        // Apply hook scale factor if available
        // 如果可用,应用挂点缩放因子
        // TODO: Add hook scale factor support (Phase 4)
        // fScale *= pHook->GetScaleFactor();
    }
}

Task 3.4: Goblin Skill Hook Support (0.5 day)

File: Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs

Update goblin skill handling in get_pos_by_id() (lines 466-473):

if (bIsGoblinSkill)
{
    // Get goblin model from player
    // 从玩家获取小精灵模型
    CECGoblin goblin = pPlayer.GetGoblin();
    if (goblin != null)
    {
        CECModel goblinModel = goblin.GetModel();
        if (goblinModel != null)
        {
            // Use hook if specified, otherwise use model center
            // 如果指定了挂点则使用挂点,否则使用模型中心
            if (!string.IsNullOrEmpty(szHook))
            {
                Transform pHook = goblinModel.GetHook(szHook, true);
                if (pHook != null)
                {
                    Transform modelTransform = goblinModel.transform;
                    vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
                    return true;
                }
            }
            
            // Fallback to goblin position
            // 回退到小精灵位置
            vPos = goblin.transform.position;
            vPos.y += 0.5f;
            return true;
        }
    }
    return false;
}

Phase 3 Deliverables:

  • Child model support (weapons, pets)
  • Recursive hook search
  • CECModel.PlayGfx() hook integration
  • Goblin skill hook support

Phase 4: Performance & Polish (1-2 days)

Goal: Optimize hook lookups and add production features

Task 4.1: Hook Cache Optimization (0.5 day)

File: Assets/PerfectWorld/Scripts/NPC/CECModel.cs

Enhance caching:

  • Cache all hooks on model load (pre-populate cache)
  • Invalidate cache on model reload
  • Add cache statistics for profiling

Task 4.2: Hook Scale Factor Support (0.5 day)

File: Assets/PerfectWorld/Scripts/Utility/HookUtils.cs

Add scale factor support:

public static float GetHookScaleFactor(Transform hookTransform)
{
    if (hookTransform == null)
        return 1.0f;
    
    // Get scale from hook Transform or custom component
    // 从挂点变换或自定义组件获取缩放
    // TODO: Add HookScaleComponent if needed
    return hookTransform.localScale.magnitude;
}

Task 4.3: Debug Visualization (0.5 day)

New File: Assets/PerfectWorld/Scripts/Debug/HookDebugVisualizer.cs

#if UNITY_EDITOR
using UnityEngine;

namespace BrewMonster.Debug
{
    /// <summary>
    /// Debug visualization for hooks
    /// 挂点调试可视化
    /// </summary>
    public class HookDebugVisualizer : MonoBehaviour
    {
        public bool showHooks = false;
        public Color hookColor = Color.yellow;
        public float hookSize = 0.1f;
        
        void OnDrawGizmos()
        {
            if (!showHooks) return;
            
            SkeletonBuilder skeleton = GetComponent<SkeletonBuilder>();
            if (skeleton == null) return;
            
            // Draw all hooks
            // 绘制所有挂点
            foreach (var kvp in skeleton.hooks)
            {
                Transform hook = kvp.Value;
                if (hook == null) continue;
                
                Gizmos.color = hookColor;
                Gizmos.DrawSphere(hook.position, hookSize);
                
                // Draw hook name label
                // 绘制挂点名称标签
                #if UNITY_EDITOR
                UnityEditor.Handles.Label(hook.position + Vector3.up * 0.2f, kvp.Key);
                #endif
            }
        }
    }
}
#endif

Task 4.4: Production Testing (0.5 day)

  • Performance profiling: Hook lookup time
  • Memory profiling: Cache size
  • Stress test: 100+ characters with hooks
  • Edge cases: Hook not found, model destroyed, hook destroyed

Phase 4 Deliverables:

  • Optimized hook caching
  • Hook scale factor support
  • Debug visualization tools
  • Production-ready performance

5. File-by-File Status

5.1 Files — No Changes Needed

File Lines Notes
SkeletonBuilder.cs 244 Hook creation works, needs lookup method
A3DSkeletonHook (data) Hook data structure complete

5.2 Files — Need Edits ⚠️

File Lines What Needs Changing
CECSkillGfxMan.cs 442-578 Un-comment and implement hook lookup in get_pos_by_id()
CECModel.cs 364-477 Add GetHook(), GetChildModel(), hook cache
CECPlayer.cs Add GetPlayerModel(), GetHook() convenience method
CECNPC.cs Add GetModel(), GetHook() convenience method

5.3 Files — Need Creation

File Phase Purpose
HookUtils.cs Phase 1 Hook position/rotation calculation utilities
HookDebugVisualizer.cs Phase 4 Debug visualization for hooks

6. Key Technical Challenges

6.1 Challenge: Unity Transform vs C++ Matrix

Issue: C++ uses Matrix4x4 for transforms, Unity uses Transform component.

Solution: Use Unity's Transform API:

  • TransformPoint() for relative offset (equivalent to Matrix * Vector3)
  • TransformDirection() for direction vectors
  • position / rotation for world space

6.2 Challenge: Hook Caching Performance

Issue: Repeated string lookups in dictionary can be slow.

Solution:

  • Cache Transform references in CECModel
  • Pre-populate cache on model load
  • Invalidate cache only when model reloads

6.3 Challenge: Child Model Hierarchy

Issue: Weapons/pets are separate models with their own skeletons.

Solution:

  • Store child models in CECModel.m_childModels dictionary
  • Recursive search in SkeletonBuilder.GetHook()
  • Register child models when equipped

6.4 Challenge: Hook Not Found Fallback

Issue: What if hook doesn't exist on model?

Solution:

  • Return null from GetHook()
  • get_pos_by_id() falls back to character center position (existing code)
  • Log warning in debug builds

6.5 Challenge: Relative vs Absolute Offset

Issue: C++ code has two offset modes.

Solution:

  • bRelHook = true: Use TransformPoint() (offset in hook's local space)
  • bRelHook = false: Use TransformPoint() on model transform, then translate to hook position (offset in model space)

6.6 Challenge: CECModel Initialization Timing

Issue: SkeletonBuilder may not be available when CECModel is created, or may be created later.

Solution:

  • Lazy Initialization: Auto-detect SkeletonBuilder in GetHook() if not set (fallback)
  • Explicit Initialization: Call InitializeSkeletonBuilder() after model loads (recommended)
  • Auto-Initialization: Try in Awake() if SkeletonBuilder exists in hierarchy (convenience)
  • Manual Setup: Call SetSkeletonBuilder() directly when skeleton is built (most control)

Best Practice:

  1. After SkeletonBuilder.BuildSkeleton() completes, call CECModel.InitializeSkeletonBuilder()
  2. This ensures hooks are immediately available and cache is properly initialized
  3. For model reloads (equipment changes), call InitializeSkeletonBuilder() again

Integration Example:

// In model loader, after skeleton is built:
void OnModelLoaded(GameObject modelObject)
{
    SkeletonBuilder skeleton = modelObject.GetComponentInChildren<SkeletonBuilder>();
    if (skeleton != null)
    {
        skeleton.BuildSkeleton(skeletonData);
        
        // Initialize CECModel with skeleton
        CECModel model = modelObject.GetComponent<CECModel>();
        if (model != null)
        {
            model.InitializeSkeletonBuilder();  // Auto-detects skeleton
            // OR: model.SetSkeletonBuilder(skeleton);  // Direct assignment
        }
    }
}

7. Testing Strategy

7.1 Phase 1 Testing

Test Hook Lookup:

  • GetHook("weapon_tip") returns valid Transform
  • GetHook("nonexistent") returns null
  • Hook position matches expected bone position
  • Hook cache works (second lookup is faster)

Test HookUtils:

  • GetHookWorldPosition() with relative offset
  • GetHookWorldPosition() with absolute offset
  • GetHookWorldRotation() returns correct rotation

7.2 Phase 2 Testing

Test Skill GFX Positioning:

  • Fly GFX spawns at caster's weapon tip hook
  • Hit GFX spawns at target's chest hook
  • Offset (relative/absolute) applied correctly
  • Fallback to center when hook not found

Test Skills:

  • Find skills with hook names in SkillStub
  • Verify GFX positions match expected hook locations

7.3 Phase 3 Testing

Test Child Models:

  • Weapon hook lookup works
  • Pet hook lookup works
  • Recursive search finds hooks in child models

Test Goblin Skills:

  • Goblin hook positioning works
  • Fallback to goblin center if hook not found

7.4 Phase 4 Testing

Performance:

  • Hook lookup < 1ms (cached)
  • Cache memory usage reasonable
  • No memory leaks on model reload

Edge Cases:

  • Hook destroyed mid-use (null check)
  • Model destroyed (fallback works)
  • Multiple simultaneous hook lookups

8. Estimated Timeline

Phase Duration What's Done What's Left
Phase 1 2-3 days SkeletonBuilder creates hooks Add lookup API, HookUtils
Phase 2 2-3 days Wire up GFX positioning
Phase 3 2-3 days Child models, recursive search
Phase 4 1-2 days Optimization, polish
Total 7-11 days ~30% structural ~70% implementation + testing

9. Quick Reference: Hook Names

Common hook names used in Perfect World (from C++ codebase):

Hook Name Purpose Typical Location
weapon_tip Weapon tip / 武器尖端 End of weapon bone
weapon_hand Weapon hand / 武器手 Hand holding weapon
chest Chest / 胸部 Center of chest bone
head Head / 头部 Top of head bone
hand_left Left hand / 左手 Left hand bone
hand_right Right hand / 右手 Right hand bone
foot_left Left foot / 左脚 Left foot bone
foot_right Right foot / 右脚 Right foot bone

Note: Hook names are model-specific. Check model files or use debug visualization to find available hooks.


10. Integration with Skill GFX System

The Hook System integrates with the Skill GFX System as follows:

  1. SkillStub defines hook parameters:

    • m_FlyPos.szHook - Hook name for fly GFX start
    • m_FlyEndPos.szHook - Hook name for fly GFX end
    • m_HitPos.szHook - Hook name for hit GFX
  2. A3DSkillGfxComposer stores hook info:

    • m_FlyPos, m_FlyEndPos, m_HitPos contain hook parameters
  3. CECSkillGfxEvent.get_pos_by_id() uses hooks:

    • Calls GetHook() to find hook Transform
    • Uses HookUtils.GetHookWorldPosition() to calculate position
    • Falls back to center if hook not found
  4. GFX spawns at hook position:

    • Fly GFX spawns at caster's hook position
    • Hit GFX spawns at target's hook position

End of Document

This plan was created 2026-02-24. The Hook System is a critical component for accurate skill GFX positioning and will significantly improve visual quality when implemented.