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
- System Overview
- Current State Analysis
- Architecture Comparison
- Implementation Phases
- File-by-File Status
- Key Technical Challenges
- 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
-
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
-
Model GFX Attachment (
CECModel.PlayGfx())- Attach persistent effects to character hooks
- Support fade-out and scale factors
-
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 ✅
-
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
- Creates GameObject for each hook from
-
Hook Data Structure (
A3DSkeletonHook)- Hook name (
m_strName) - Bone index (
Data.iBone) - Transform matrix (
Data.matHookTM) - ✅ Data loaded from model files
- Hook name (
-
Skeleton Structure (
SkeletonBuilder.cs)boneslist: All bone TransformsrootBone: Root bone TransformGetBones()/GetBoneNoGC(): Bone lookup methods- ✅ Skeleton hierarchy built correctly
2.2 What's Partially Done ⚠️ (Structure Exists, Logic Commented Out)
-
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
-
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
-
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 ❌
-
Hook Lookup API
- No
GetHook(string hookName)method on CECModel/CECPlayer/CECNPC - No
GetSkeletonHook()equivalent - No hook Transform caching for performance
- No
-
Hook Position Calculation
- No
GetAbsoluteTM()equivalent (world transform from hook) - No relative vs absolute offset calculation
- No hook-to-world-space conversion
- No
-
Child Model System
- No
GetChildModel(string hangerName)method - No child model hook lookup
- No hanger hierarchy support
- No
-
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:
-
Add hook storage dictionary:
public Dictionary<string, Transform> hooks = new Dictionary<string, Transform>(); -
Store hooks during
BuildSkeleton():// In BuildSkeleton(), after creating hook GameObject (line 151): hooks[hook.m_strName] = hookGO.transform; -
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:
-
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; } -
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(); } } -
Add hook cache:
private Dictionary<string, Transform> m_hookCache = new Dictionary<string, Transform>(); -
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; } -
Add cache invalidation:
public void InvalidateHookCache() { m_hookCache.Clear(); } -
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:
-
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>(); } -
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:
-
Add GetModel method (if missing):
public CECModel GetModel() { return GetComponent<CECModel>(); } -
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:
-
Model Loading Integration:
- After
SkeletonBuilder.BuildSkeleton()is called, callCECModel.InitializeSkeletonBuilder() - This ensures hooks are available immediately after skeleton is built
- After
-
Character Creation Integration:
- In
CECPlayer/CECNPCinitialization, after model is loaded:CECModel model = GetComponent<CECModel>(); if (model != null) { model.InitializeSkeletonBuilder(); }
- In
-
Model Reload Handling:
- When model is reloaded (equipment change, etc.), call
InitializeSkeletonBuilder()again - This updates the SkeletonBuilder reference and clears cache
- When model is reloaded (equipment change, etc.), call
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):
-
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; } -
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):
- 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:
-
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; } -
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 toMatrix * Vector3)TransformDirection()for direction vectorsposition/rotationfor 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_childModelsdictionary - 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
nullfromGetHook() 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: UseTransformPoint()(offset in hook's local space)bRelHook = false: UseTransformPoint()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:
- After
SkeletonBuilder.BuildSkeleton()completes, callCECModel.InitializeSkeletonBuilder() - This ensures hooks are immediately available and cache is properly initialized
- 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:
-
SkillStub defines hook parameters:
m_FlyPos.szHook- Hook name for fly GFX startm_FlyEndPos.szHook- Hook name for fly GFX endm_HitPos.szHook- Hook name for hit GFX
-
A3DSkillGfxComposer stores hook info:
m_FlyPos,m_FlyEndPos,m_HitPoscontain hook parameters
-
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
- Calls
-
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.