# 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](#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 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) ```cpp // 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 ```csharp // 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 ```csharp // Hook lookup API public class CECModel { public Transform GetHook(string hookName, bool recursive = true); public CECModel GetChildModel(string hangerName); private Dictionary 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: ```csharp public Dictionary hooks = new Dictionary(); ``` 2. Store hooks during `BuildSkeleton()`: ```csharp // In BuildSkeleton(), after creating hook GameObject (line 151): hooks[hook.m_strName] = hookGO.transform; ``` 3. Add public lookup method: ```csharp /// /// Get hook Transform by name /// 根据名称获取挂点变换 /// /// Hook name / 挂点名称 /// Search in child models / 在子模型中搜索 /// Hook Transform or null if not found / 挂点变换,未找到返回null 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: ```csharp private SkeletonBuilder m_skeletonBuilder; public void SetSkeletonBuilder(SkeletonBuilder builder) { m_skeletonBuilder = builder; m_hookCache?.Clear(); // Invalidate cache on model change } /// /// Get the SkeletonBuilder component /// 获取SkeletonBuilder组件 /// public SkeletonBuilder GetSkeletonBuilder() { return m_skeletonBuilder; } ``` 2. **Add Auto-Initialization Logic:** ```csharp /// /// Initialize SkeletonBuilder reference - call this after model is loaded /// 初始化SkeletonBuilder引用 - 在模型加载后调用 /// public void InitializeSkeletonBuilder() { // Try to find SkeletonBuilder in children (where it's typically attached) // 尝试在子对象中查找SkeletonBuilder(通常附加在那里) m_skeletonBuilder = GetComponentInChildren(true); if (m_skeletonBuilder == null) { // Fallback: search in parent hierarchy (for NPCs/Players) // 回退:在父层次结构中搜索(用于NPC/玩家) m_skeletonBuilder = GetComponentInParent(); } 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}"); } } /// /// Auto-initialize on Awake if SkeletonBuilder is already available /// 如果SkeletonBuilder已可用,在Awake时自动初始化 /// private void Awake() { // Only auto-initialize if SkeletonBuilder exists in hierarchy // 仅在层次结构中存在SkeletonBuilder时自动初始化 if (GetComponentInChildren(true) != null || GetComponentInParent() != null) { InitializeSkeletonBuilder(); } } ``` 2. Add hook cache: ```csharp private Dictionary m_hookCache = new Dictionary(); ``` 3. Add GetHook method: ```csharp /// /// Get hook Transform by name /// 根据名称获取挂点变换 /// /// Hook name / 挂点名称 /// Search recursively / 递归搜索 /// Hook Transform or null / 挂点变换,未找到返回null 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; } ``` 4. Add cache invalidation: ```csharp public void InvalidateHookCache() { m_hookCache.Clear(); } ``` 5. **Update GetHook to handle uninitialized state:** ```csharp 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): ```csharp public CECModel GetPlayerModel() { // TODO: Return player's CECModel instance // This should return the model component attached to the player GameObject return GetComponent(); } ``` 2. Add convenience GetHook method: ```csharp 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): ```csharp public CECModel GetModel() { return GetComponent(); } ``` 2. Add convenience GetHook method: ```csharp 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: ```csharp CECModel model = GetComponent(); 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:** ```csharp // After SkeletonBuilder.BuildSkeleton() completes: void OnSkeletonBuilt(SkeletonBuilder skeleton) { CECModel model = GetComponent(); 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` ```csharp using UnityEngine; namespace BrewMonster { /// /// Utility functions for hook position/rotation calculation /// 挂点位置/旋转计算的工具函数 /// public static class HookUtils { /// /// Get world position from hook Transform with offset /// 从挂点变换获取世界位置(带偏移) /// /// Hook Transform / 挂点变换 /// Offset vector / 偏移向量 /// If true, offset is relative to hook's local space; if false, absolute / 如果为true,偏移相对于挂点局部空间;如果为false,绝对偏移 /// Model transform for absolute offset calculation (optional) / 用于绝对偏移计算的模型变换(可选) /// World position / 世界位置 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(); 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; } } /// /// Get world rotation from hook Transform /// 从挂点变换获取世界旋转 /// /// Hook Transform / 挂点变换 /// World rotation / 世界旋转 public static Quaternion GetHookWorldRotation(Transform hookTransform) { if (hookTransform == null) return Quaternion.identity; return hookTransform.rotation; } /// /// Get world direction from hook Transform /// 从挂点变换获取世界方向 /// /// Hook Transform / 挂点变换 /// Forward direction in world space / 世界空间的前方向 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): ```csharp // 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): ```csharp // 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:** ```csharp // 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:** ```csharp // 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: ```csharp private Dictionary m_childModels = new Dictionary(); 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:** ```csharp 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(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):** ```csharp // 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):** ```csharp 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:** ```csharp 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` ```csharp #if UNITY_EDITOR using UnityEngine; namespace BrewMonster.Debug { /// /// Debug visualization for hooks /// 挂点调试可视化 /// 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(); 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:** ```csharp // In model loader, after skeleton is built: void OnModelLoaded(GameObject modelObject) { SkeletonBuilder skeleton = modelObject.GetComponentInChildren(); if (skeleton != null) { skeleton.BuildSkeleton(skeletonData); // Initialize CECModel with skeleton CECModel model = modelObject.GetComponent(); 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.