hook system process
This commit is contained in:
@@ -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) { }
|
||||
|
||||
|
||||
+9
-275
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
Reference in New Issue
Block a user