hook system convert plan

This commit is contained in:
VDH
2026-03-03 15:27:03 +07:00
parent b2d49f8c65
commit d5e854eef7
2 changed files with 616 additions and 13 deletions
+408
View File
@@ -0,0 +1,408 @@
# Hook System C++ Reference & Conversion Verification
This document shows the actual C++ implementation of the Hook System and verifies the conversion plan matches it correctly.
**Date Created:** 2026-02-24
---
## 1. C++ Hook System Architecture
### 1.1 Core Classes
#### A3DSkeletonHook (Angelica2/Angelica3D/Source/A3DSkeleton.cpp)
```cpp
class A3DSkeletonHook {
A3DSkeleton* m_pA3DSkeleton; // Parent skeleton
int m_iBone; // Bone index (-1 = root)
A3DMATRIX4 m_matHookTM; // Local hook transform (relative to bone)
A3DMATRIX4 m_matScaledHookTM; // Scaled hook transform
A3DMATRIX4 m_matAbs; // Cached absolute world matrix
A3DMATRIX4 m_matNoScaleAbs; // Cached absolute matrix (no scale)
bool m_bFixDir; // Fix direction to skeleton
DWORD m_dwUpdateCnt; // Update counter
// Get world transform matrix
const A3DMATRIX4& GetAbsoluteTM() {
Update(false); // Update if needed
return m_matAbs;
}
// Update hook transform (called automatically)
void Update(bool bForce) {
if (m_iBone < 0) {
// Root bone hook
m_matAbs = m_matScaledHookTM * m_pA3DSkeleton->GetAbsoluteTM();
} else {
// Bone-attached hook
A3DBone* pBone = m_pA3DSkeleton->GetBone(m_iBone);
m_matAbs = matWholeScale * (m_matScaledHookTM * pBone->GetNoScaleAbsTM());
}
}
};
```
**Key Points:**
- Hook has local transform (`m_matHookTM`) relative to bone
- `GetAbsoluteTM()` returns world-space transform matrix
- Automatically updates when skeleton animates
- Supports bone scaling
#### A3DSkinModel::GetSkeletonHook() (A3DSkinModel.cpp:2357)
```cpp
A3DSkeletonHook* A3DSkinModel::GetSkeletonHook(const char* szName, bool bNoChild)
{
A3DSkeletonHook* pHook = NULL;
// Search in main skeleton
if (m_pA3DSkeleton) {
if ((pHook = m_pA3DSkeleton->GetHook(szName, NULL)))
return pHook;
}
// Search in child models (if bNoChild == false, recursive)
if (!bNoChild) {
for (int i = 0; i < m_aChildModels.GetSize(); i++) {
A3DSkinModel* pChild = m_aChildModels[i];
if ((pHook = pChild->GetSkeletonHook(szName, false)))
return pHook;
}
}
return NULL;
}
```
**Key Points:**
- `bNoChild = false` means **recursive search** (searches child models)
- `bNoChild = true` means **non-recursive** (only main skeleton)
- Searches child models (weapons, pets) if recursive
---
## 2. C++ Hook Usage in Skill GFX
### 2.1 Player Hook Lookup (CECSkillGfxEvent::get_pos_by_id)
**C++ Code (from commented C# code):**
```cpp
// Player branch
CECModel* pModel = pPlayer->GetPlayerModel();
if (!pModel)
break;
// Handle child model (hanger) if specified
if (szHanger && bChildHook)
pModel = pModel->GetChildModel(szHanger);
if (!pModel)
break;
A3DSkinModel* pSkin = pModel->GetA3DSkinModel();
A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); // true = non-recursive
if (!pHook)
break;
// Calculate position
if (bRelHook) {
// Relative offset: transform offset in hook's local space
vPos = pHook->GetAbsoluteTM() * pOffset; // Matrix * Vector3
} else {
// Absolute offset: offset in model space, then translate to hook position
vPos = pSkin->GetAbsoluteTM() * pOffset;
vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
}
return true;
```
**Logic Breakdown:**
1. Get player model
2. If `szHanger` and `bChildHook` are set, get child model (weapon/pet)
3. Get skeleton hook by name (non-recursive search)
4. Calculate position:
- **Relative (`bRelHook = true`)**: `hookWorldMatrix * offset` → offset in hook's local space
- **Absolute (`bRelHook = false`)**: `modelWorldMatrix * offset` then translate to hook position
### 2.2 NPC Hook Lookup (CECNPC::GetSgcHook)
**C++ Code (EC_NPCModel.cpp:505):**
```cpp
A3DSkeletonHook* CECNPCModelDefaultPolicy::GetSgcHook(
const char* szHanger,
bool bChildHook,
const char* szHook)
{
if (A3DSkinModel* pSkinModel = GetSgcSkinModel(szHanger, bChildHook, szHook)) {
return pSkinModel->GetSkeletonHook(szHook, true); // true = non-recursive
}
return NULL;
}
```
**Usage in get_pos_by_id (NPC branch):**
```cpp
// NPC branch
A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook);
if (!pHook)
break;
A3DSkinModel* pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook);
if (bRelHook) {
vPos = pHook->GetAbsoluteTM() * pOffset;
} else {
vPos = pSkin->GetAbsoluteTM() * pOffset;
vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
}
return true;
```
---
## 3. C++ to C# Conversion Mapping
### 3.1 Transform Matrix Operations
| C++ Operation | C# Equivalent | Notes |
|---------------|---------------|-------|
| `pHook->GetAbsoluteTM()` | `hookTransform` (Unity Transform) | World transform |
| `pHook->GetAbsoluteTM() * pOffset` | `hookTransform.TransformPoint(offset)` | Relative offset |
| `pSkin->GetAbsoluteTM() * pOffset` | `skinTransform.TransformPoint(offset)` | Model-space offset |
| `pHook->GetAbsoluteTM().GetRow(3)` | `hookTransform.position` | Hook world position |
| `pSkin->GetAbsoluteTM().GetRow(3)` | `skinTransform.position` | Model world position |
### 3.2 Hook Lookup
| C++ | C# (Plan) | Status |
|-----|-----------|--------|
| `pSkin->GetSkeletonHook(szHook, true)` | `skeletonBuilder.GetHook(szHook, false)` | ⚠️ **INVERTED FLAG** |
| `pModel->GetChildModel(szHanger)` | `pModel.GetChildModel(szHanger)` | ✅ Matches |
| `pNPC->GetSgcHook(...)` | `pNPC.GetHook(...)` | ✅ Simplified |
**⚠️ IMPORTANT: Flag Inversion**
C++ `GetSkeletonHook(szHook, true)` means **non-recursive** (don't search children).
C# plan uses `GetHook(szHook, false)` which means **non-recursive** (don't recurse).
**The conversion plan is CORRECT** - Unity's `recursive = false` matches C++'s `bNoChild = true`.
### 3.3 Position Calculation
**C++ Relative Offset:**
```cpp
vPos = pHook->GetAbsoluteTM() * pOffset;
```
**C# Equivalent (from plan):**
```csharp
vPos = hookTransform.TransformPoint(offset); // ✅ CORRECT
```
**C++ Absolute Offset:**
```cpp
vPos = pSkin->GetAbsoluteTM() * pOffset;
vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3);
```
**C# Equivalent (from plan):**
```csharp
Vector3 hookWorldPos = hookTransform.position;
Vector3 offsetWorld = hookTransform.TransformDirection(offset);
vPos = hookWorldPos + offsetWorld; // ⚠️ NEEDS VERIFICATION
```
**Verification Needed:** The C++ absolute offset logic:
1. Transforms offset in **model's world space** (`pSkin->GetAbsoluteTM() * pOffset`)
2. Subtracts model position
3. Adds hook position
This is equivalent to: **offset in model's local space, then translate to hook position**.
**C# Correct Implementation:**
```csharp
// Get model transform (equivalent to pSkin)
Transform modelTransform = pModel.transform; // Or get from SkeletonBuilder root
// Transform offset in model's world space
Vector3 offsetWorld = modelTransform.TransformPoint(offset);
// Translate to hook position
vPos = offsetWorld - modelTransform.position + hookTransform.position;
```
**OR simpler (if offset is in model local space):**
```csharp
// Transform offset from model local to world
Vector3 offsetWorld = modelTransform.TransformDirection(offset);
// Add hook position
vPos = hookTransform.position + offsetWorld;
```
---
## 4. Conversion Plan Verification
### 4.1 ✅ Correct Aspects
1. **Hook Storage**: Plan correctly stores hooks in `Dictionary<string, Transform>`
2. **Hook Lookup**: Plan correctly implements `GetHook()` with recursive flag ✅
3. **Relative Offset**: Plan correctly uses `TransformPoint()`
4. **Child Model Support**: Plan correctly implements `GetChildModel()`
5. **Caching**: Plan correctly adds hook cache for performance ✅
### 4.2 ⚠️ Needs Correction
1. **Absolute Offset Calculation** (HookUtils.cs:400-409)
**Current Plan:**
```csharp
Vector3 hookWorldPos = hookTransform.position;
Vector3 offsetWorld = hookTransform.TransformDirection(offset);
return hookWorldPos + offsetWorld;
```
**Should Be:**
```csharp
// Need model transform reference
// C++: pSkin->GetAbsoluteTM() * pOffset
Vector3 offsetWorld = modelTransform.TransformPoint(offset);
// C++: vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3)
return offsetWorld - modelTransform.position + hookTransform.position;
```
**Problem:** `HookUtils.GetHookWorldPosition()` doesn't have access to model transform.
**Solution:** Pass model transform as parameter, OR get it from hook's root parent.
**Updated HookUtils:**
```csharp
public static Vector3 GetHookWorldPosition(
Transform hookTransform,
Vector3 offset,
bool bRelative,
Transform modelTransform = null) // Add model transform parameter
{
if (hookTransform == null)
return Vector3.zero;
if (bRelative) {
return hookTransform.TransformPoint(offset);
} else {
// Absolute offset: transform in model space, then translate to hook
if (modelTransform == null) {
// Fallback: use hook's root (skeleton root)
modelTransform = hookTransform.root;
}
Vector3 offsetWorld = modelTransform.TransformPoint(offset);
return offsetWorld - modelTransform.position + hookTransform.position;
}
}
```
2. **GetSkeletonHook Flag Usage**
**C++:** `GetSkeletonHook(szHook, true)` = non-recursive
**Plan:** `GetHook(szHook, true)` = recursive (inverted!)
**Correction:** The plan's `recursive` parameter should match C++'s `bNoChild`:
- C++ `bNoChild = true` → C# `recursive = false`
- C++ `bNoChild = false` → C# `recursive = true`
**Current plan uses `recursive = true` in calls, which is CORRECT** (matches C++ `bNoChild = false` for recursive search).
**Verification:**
- C++: `GetSkeletonHook(szHook, true)` = non-recursive (only main skeleton)
- Plan: `GetHook(szHook, false)` = non-recursive ✅ **CORRECT**
---
## 5. Updated Conversion Plan Corrections
### 5.1 Fix HookUtils.GetHookWorldPosition()
**File:** `Assets/PerfectWorld/Scripts/Utility/HookUtils.cs`
**Update absolute offset calculation:**
```csharp
public static Vector3 GetHookWorldPosition(
Transform hookTransform,
Vector3 offset,
bool bRelative,
Transform modelTransform = null) // Add optional model transform
{
if (hookTransform == null)
return Vector3.zero;
if (bRelative) {
// Relative offset: transform offset from hook's local space to world space
// C++: pHook->GetAbsoluteTM() * pOffset
return hookTransform.TransformPoint(offset);
} else {
// Absolute offset: transform offset in model space, then translate to hook position
// C++: 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: use hook's root bone (skeleton root)
// Find SkeletonBuilder component in hierarchy
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;
}
}
```
### 5.2 Update get_pos_by_id() Calls
**File:** `Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs`
**Pass model transform for absolute offset:**
```csharp
// In player branch:
CECModel pModel = pPlayer.GetPlayerModel();
Transform modelTransform = pModel.transform; // Or get from SkeletonBuilder
Transform pHook = pModel.GetHook(szHook, true);
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
```
---
## 6. Summary
### ✅ Conversion Plan is Mostly Correct
The conversion plan correctly:
- Maps C++ hook lookup to Unity Transform system
- Implements recursive search correctly
- Handles relative offset correctly
- Supports child models
### ⚠️ One Correction Needed
**Absolute Offset Calculation:**
- Current plan uses `TransformDirection()` which is incorrect
- Should use `TransformPoint()` on model transform, then translate
- Need to pass model transform to `HookUtils.GetHookWorldPosition()`
### 📝 Implementation Notes
1. **Hook Lookup Flag**: C++ `bNoChild = true` → C# `recursive = false`
2. **Relative Offset**: `TransformPoint()` is correct ✅
3. **Absolute Offset**: Needs model transform reference ⚠️
4. **Child Models**: Plan correctly implements `GetChildModel()`
---
**End of Document**
+208 -13
View File
@@ -9,6 +9,9 @@ This document provides a comprehensive plan to convert the Perfect World Hook Sy
**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
@@ -265,6 +268,63 @@ Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
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:**
```csharp
/// <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();
}
}
```
2. Add hook cache:
@@ -315,6 +375,40 @@ Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
}
```
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/`)
@@ -363,7 +457,43 @@ Vector3 vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
}
```
#### Task 1.5: Create HookUtils Helper Class (0.5 day)
#### 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<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:**
```csharp
// 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`
@@ -385,8 +515,9 @@ namespace BrewMonster
/// <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)
public static Vector3 GetHookWorldPosition(Transform hookTransform, Vector3 offset, bool bRelative, Transform modelTransform = null)
{
if (hookTransform == null)
return Vector3.zero;
@@ -395,17 +526,34 @@ namespace BrewMonster
{
// Relative offset: transform offset from hook's local space to world space
// 相对偏移:将偏移从挂点的局部空间变换到世界空间
// C++ equivalent: pHook->GetAbsoluteTM() * pOffset
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;
// 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;
}
}
@@ -443,8 +591,10 @@ namespace BrewMonster
**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
---
@@ -482,9 +632,13 @@ namespace BrewMonster
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);
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
return true;
}
@@ -524,9 +678,13 @@ namespace BrewMonster
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);
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
return true;
}
@@ -688,7 +846,8 @@ if (bIsGoblinSkill)
Transform pHook = goblinModel.GetHook(szHook, true);
if (pHook != null)
{
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook);
Transform modelTransform = goblinModel.transform;
vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform);
return true;
}
}
@@ -878,7 +1037,43 @@ namespace BrewMonster.Debug
**Solution:**
- `bRelHook = true`: Use `TransformPoint()` (offset in hook's local space)
- `bRelHook = false`: Use `TransformDirection()` + add to hook position (offset in world 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<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
}
}
}
```
---