hook system process

This commit is contained in:
VDH
2026-03-02 19:22:44 +07:00
parent 85ee1de172
commit c9848d443b
4 changed files with 1008 additions and 276 deletions
@@ -43,6 +43,11 @@ namespace BrewMonster.Scripts
return boneTransforms;
}
}
public void GetBoneNoGC(string boneName, out Transform boneTransform)
{
boneTransform = bones.Find(bone => bone.name == boneName);
}
#if MODEL_RENDERER_PROJECT
private A3DSkeleton _skeleton;
private int _rootBoneIndex;
@@ -15,7 +15,7 @@ namespace BrewMonster
private const float _missile_acc = 20.0f; // acceleration, same as C++
private const float _missile_vel = 5.0f; // initial velocity, same as C++
private const float _missile_rot = Mathf.PI * 1.5f; // rotation speed (radians per second), same as C++
private const float _angle_limit = Mathf.Cos(15.0f * Mathf.Deg2Rad); // 15 degree limit, same as C++
private float _angle_limit = Mathf.Cos(15.0f * Mathf.Deg2Rad); // 15 degree limit, same as C++
public CGfxMissileMove(GfxMoveMode mode) : base(mode) { }
File diff suppressed because one or more lines are too long
+993
View File
@@ -0,0 +1,993 @@
# 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)
---
## 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<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:
```csharp
public Dictionary<string, Transform> hooks = new Dictionary<string, Transform>();
```
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
/// <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:
```csharp
private SkeletonBuilder m_skeletonBuilder;
public void SetSkeletonBuilder(SkeletonBuilder builder)
{
m_skeletonBuilder = builder;
m_hookCache?.Clear(); // Invalidate cache on model change
}
```
2. Add hook cache:
```csharp
private Dictionary<string, Transform> m_hookCache = new Dictionary<string, Transform>();
```
3. Add GetHook method:
```csharp
/// <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;
}
```
4. Add cache invalidation:
```csharp
public void InvalidateHookCache()
{
m_hookCache.Clear();
}
```
#### 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<CECModel>();
}
```
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<CECModel>();
}
```
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: Create HookUtils Helper Class (0.5 day)
**New File:** `Assets/PerfectWorld/Scripts/Utility/HookUtils.cs`
```csharp
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>
/// <returns>World position / 世界位置</returns>
public static Vector3 GetHookWorldPosition(Transform hookTransform, Vector3 offset, bool bRelative)
{
if (hookTransform == null)
return Vector3.zero;
if (bRelative)
{
// Relative offset: transform offset from hook's local space to world space
// 相对偏移:将偏移从挂点的局部空间变换到世界空间
return hookTransform.TransformPoint(offset);
}
else
{
// Absolute offset: apply offset in world space, then add hook position
// 绝对偏移:在世界空间中应用偏移,然后加上挂点位置
// C++ equivalent: vPos = pSkin->GetAbsoluteTM() * pOffset;
// vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
Vector3 hookWorldPos = hookTransform.position;
Vector3 offsetWorld = hookTransform.TransformDirection(offset);
return hookWorldPos + offsetWorld;
}
}
/// <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
- ✅ Hook position calculation utilities
- ✅ Basic hook caching for performance
---
### 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;
// Calculate position based on relative/absolute offset
// 根据相对/绝对偏移计算位置
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
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;
// Calculate position based on relative/absolute offset
// 根据相对/绝对偏移计算位置
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
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<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:**
```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<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):**
```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)
{
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
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
{
/// <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 `TransformDirection()` + add to hook position (offset in world space)
---
## 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.