435 lines
13 KiB
Markdown
435 lines
13 KiB
Markdown
# Hook System Unity Parenting Approach
|
|
|
|
**Date Created:** 2026-02-24
|
|
**Purpose:** Analyze whether we can use Unity's Transform parenting instead of manual position calculation
|
|
|
|
---
|
|
|
|
## Question
|
|
|
|
Instead of manually calculating hook positions every frame and updating GFX positions, can we:
|
|
1. Parent GFX GameObject directly to hook Transform?
|
|
2. Eliminate the need for `GetHookWorldPosition()` calculations?
|
|
3. Let Unity automatically handle position updates?
|
|
|
|
---
|
|
|
|
## Analysis: When Can We Parent vs. When We Need Manual Updates
|
|
|
|
### ✅ **YES - Can Parent (Hit GFX that follows target)**
|
|
|
|
**Use Case:** Hit GFX that needs to follow a moving target
|
|
|
|
**C++ Behavior:**
|
|
```cpp
|
|
// A3DSkillGfxEvent2.cpp:502-508
|
|
if (m_bTraceTarget)
|
|
{
|
|
A3DMATRIX4 matTran;
|
|
matTran.Identity();
|
|
matTran.SetRow(3, GetTargetCenter()); // Updates every frame
|
|
m_pHitGfx->SetParentTM(matTran);
|
|
}
|
|
```
|
|
|
|
**Unity Approach:**
|
|
```csharp
|
|
// Instead of:
|
|
Vector3 targetPos = HookUtils.GetHookWorldPosition(targetHook, offset, bRelHook);
|
|
hitGfx.transform.position = targetPos; // Every frame
|
|
|
|
// We can do:
|
|
hitGfx.transform.SetParent(targetHook.transform);
|
|
hitGfx.transform.localPosition = offset; // One-time setup
|
|
// Unity automatically updates position every frame!
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ No manual position calculation every frame
|
|
- ✅ Automatically follows bone/hook movement
|
|
- ✅ Handles rotation automatically
|
|
- ✅ More Unity-native approach
|
|
- ✅ Better performance (Unity's transform system is optimized)
|
|
|
|
---
|
|
|
|
### ⚠️ **PARTIAL - Can Use for Spawn Position Only (Fly GFX)**
|
|
|
|
**Use Case:** Fly GFX spawns at host hook, then moves independently to target
|
|
|
|
**C++ Behavior:**
|
|
```cpp
|
|
// A3DSkillGfxEvent2.cpp:535-546
|
|
// Fly GFX spawns at host position
|
|
m_pMoveMethod->StartMove(m_vHostPos, m_vTargetPos);
|
|
|
|
if (m_pFlyGfx)
|
|
{
|
|
m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos()));
|
|
m_pFlyGfx->Start(true);
|
|
}
|
|
|
|
// Then every frame:
|
|
m_pMoveMethod->TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos); // Updates position
|
|
m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos()));
|
|
```
|
|
|
|
**Unity Approach:**
|
|
```csharp
|
|
// Spawn at hook position (one-time)
|
|
Transform hostHook = GetHook("hand_r");
|
|
flyGfx.transform.position = hostHook.position;
|
|
flyGfx.transform.rotation = hostHook.rotation;
|
|
|
|
// Then IMMEDIATELY unparent and move independently
|
|
flyGfx.transform.SetParent(null); // Unparent for independent movement
|
|
|
|
// Update position manually during flight
|
|
flyGfx.transform.position = m_pMoveMethod.GetPos();
|
|
flyGfx.transform.rotation = Quaternion.LookRotation(m_pMoveMethod.GetMoveDir());
|
|
```
|
|
|
|
**Why Can't Fully Parent:**
|
|
- Fly GFX needs to move independently from host to target
|
|
- Target position may also come from a hook (which moves)
|
|
- Movement is interpolated (not directly following either hook)
|
|
|
|
**Hybrid Approach:**
|
|
- ✅ Use hook Transform for **initial spawn position** (one-time)
|
|
- ❌ Can't parent during flight (needs independent movement)
|
|
- ✅ Can use hook Transform for **target position calculation** (if target has hook)
|
|
|
|
---
|
|
|
|
### ✅ **YES - Can Use for Offsets (Child GameObject)**
|
|
|
|
**Use Case:** GFX needs offset from hook position
|
|
|
|
**C++ Behavior:**
|
|
```cpp
|
|
// Relative offset: transform offset in hook's local space
|
|
if (bRelHook) {
|
|
vPos = pHook->GetAbsoluteTM() * (*pOffset);
|
|
}
|
|
```
|
|
|
|
**Unity Approach:**
|
|
```csharp
|
|
// Instead of calculating:
|
|
Vector3 worldPos = hookTransform.TransformPoint(offset);
|
|
|
|
// We can create a child GameObject:
|
|
GameObject offsetObj = new GameObject("GFX_Offset");
|
|
offsetObj.transform.SetParent(hookTransform);
|
|
offsetObj.transform.localPosition = offset; // Offset in hook's local space
|
|
|
|
// Then parent GFX to offset object:
|
|
gfx.transform.SetParent(offsetObj.transform);
|
|
gfx.transform.localPosition = Vector3.zero; // GFX at offset position
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ Handles relative offsets automatically
|
|
- ✅ Rotates with hook automatically
|
|
- ✅ No manual matrix calculations
|
|
|
|
---
|
|
|
|
## Recommended Unity Implementation Strategy
|
|
|
|
### Strategy 1: Hybrid Approach (Recommended)
|
|
|
|
**For Hit GFX that follows target:**
|
|
```csharp
|
|
public class CECSkillGfxEvent : A3DSkillGfxEvent
|
|
{
|
|
private GameObject m_hitGfxInstance;
|
|
private Transform m_targetHook; // Hook Transform (if available)
|
|
|
|
private void SpawnHitGfx(Vector3 vTarget)
|
|
{
|
|
// Try to get target hook
|
|
m_targetHook = GetTargetHookTransform();
|
|
|
|
if (m_targetHook != null && m_pComposer.m_HitPos.bTraceTarget)
|
|
{
|
|
// Parent to hook - Unity handles updates automatically
|
|
m_hitGfxInstance = Instantiate(hitGfxPrefab);
|
|
m_hitGfxInstance.transform.SetParent(m_targetHook);
|
|
|
|
// Apply offset
|
|
if (m_pComposer.m_HitPos.bRelHook)
|
|
{
|
|
// Relative offset: in hook's local space
|
|
m_hitGfxInstance.transform.localPosition = m_pComposer.m_HitPos.vOffset;
|
|
}
|
|
else
|
|
{
|
|
// Absolute offset: need to calculate once
|
|
// (Can't fully eliminate this, but only calculate once)
|
|
Vector3 offsetWorld = m_targetHook.root.TransformPoint(m_pComposer.m_HitPos.vOffset);
|
|
m_hitGfxInstance.transform.position = offsetWorld - m_targetHook.root.position + m_targetHook.position;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No hook or doesn't trace target - use manual position
|
|
m_hitGfxInstance = Instantiate(hitGfxPrefab, vTarget, Quaternion.identity);
|
|
// Update manually if needed
|
|
}
|
|
}
|
|
|
|
private Transform GetTargetHookTransform()
|
|
{
|
|
// Get hook Transform from target
|
|
if (string.IsNullOrEmpty(m_pComposer.m_HitPos.szHook))
|
|
return null;
|
|
|
|
CECModel targetModel = GetTargetModel();
|
|
if (targetModel == null)
|
|
return null;
|
|
|
|
// Get hook Transform (not position!)
|
|
return targetModel.GetHookTransform(
|
|
m_pComposer.m_HitPos.szHook,
|
|
m_pComposer.m_HitPos.szHanger,
|
|
m_pComposer.m_HitPos.bChildHook);
|
|
}
|
|
}
|
|
```
|
|
|
|
**For Fly GFX:**
|
|
```csharp
|
|
private void SpawnFlyGfx()
|
|
{
|
|
// Get host hook Transform (for initial position)
|
|
Transform hostHook = GetHostHookTransform();
|
|
|
|
if (hostHook != null)
|
|
{
|
|
// Spawn at hook position
|
|
m_flyGfxInstance = Instantiate(flyGfxPrefab);
|
|
m_flyGfxInstance.transform.position = hostHook.position;
|
|
m_flyGfxInstance.transform.rotation = hostHook.rotation;
|
|
|
|
// Apply offset if needed
|
|
if (m_pComposer.m_FlyPos.bRelHook)
|
|
{
|
|
m_flyGfxInstance.transform.position = hostHook.TransformPoint(m_pComposer.m_FlyPos.vOffset);
|
|
}
|
|
|
|
// IMMEDIATELY unparent - fly GFX moves independently
|
|
m_flyGfxInstance.transform.SetParent(null);
|
|
}
|
|
else
|
|
{
|
|
// No hook - use calculated position
|
|
m_flyGfxInstance = Instantiate(flyGfxPrefab, m_pMoveMethod.GetPos(), Quaternion.identity);
|
|
}
|
|
}
|
|
|
|
private void UpdateFlyGfxTransform()
|
|
{
|
|
if (m_flyGfxInstance == null) return;
|
|
|
|
// Update position manually (can't parent during flight)
|
|
m_flyGfxInstance.transform.position = m_pMoveMethod.GetPos();
|
|
Vector3 dir = m_pMoveMethod.GetMoveDir();
|
|
if (dir.magnitude > 1e-4f)
|
|
m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Strategy 2: Helper Method to Get Hook Transform (Not Position)
|
|
|
|
**New Method in HookUtils or SkeletonBuilder:**
|
|
```csharp
|
|
public static Transform GetHookTransform(
|
|
SkeletonBuilder skeleton,
|
|
string hookName,
|
|
string hangerName = null,
|
|
bool bChildHook = false)
|
|
{
|
|
// Get model (main or child)
|
|
CECModel model = skeleton.GetComponent<CECModel>();
|
|
if (hangerName != null && bChildHook)
|
|
{
|
|
model = model.GetChildModel(hangerName);
|
|
if (model == null) return null;
|
|
}
|
|
|
|
// Get hook Transform from skeleton
|
|
return model.GetHook(hookName, false); // Returns Transform, not position!
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Returns `Transform` instead of `Vector3`
|
|
- Can be used for parenting
|
|
- Can be used for one-time position lookup
|
|
- More flexible than position-only approach
|
|
|
|
---
|
|
|
|
## Comparison: Manual Position vs. Parenting
|
|
|
|
| Scenario | Manual Position (C++ way) | Unity Parenting | Winner |
|
|
|----------|---------------------------|----------------|--------|
|
|
| **Hit GFX follows target** | Calculate every frame | Parent once, Unity updates | ✅ **Parenting** |
|
|
| **Fly GFX spawn position** | Calculate once | Use hook Transform.position once | ✅ **Parenting** (simpler) |
|
|
| **Fly GFX during flight** | Calculate every frame | Must update manually | ⚠️ **Both** (can't parent) |
|
|
| **Relative offset** | Matrix transform every frame | LocalPosition once | ✅ **Parenting** |
|
|
| **Absolute offset** | Calculate every frame | Calculate once or use child GameObject | ✅ **Parenting** (one-time) |
|
|
|
|
---
|
|
|
|
## What We Can Eliminate
|
|
|
|
### ✅ **Can Eliminate:**
|
|
1. **Hit GFX position updates** (if `bTraceTarget = true`)
|
|
- Instead of: `UpdateHitGfxPosition()` every frame
|
|
- Use: `transform.SetParent(hookTransform)` once
|
|
|
|
2. **Initial spawn position calculation**
|
|
- Instead of: `HookUtils.GetHookWorldPosition(hook, offset, bRelHook)`
|
|
- Use: `hookTransform.position` or `hookTransform.TransformPoint(offset)`
|
|
|
|
3. **Relative offset calculations** (for hit GFX)
|
|
- Instead of: Manual matrix multiplication every frame
|
|
- Use: `transform.localPosition = offset` once
|
|
|
|
### ❌ **Cannot Eliminate:**
|
|
1. **Fly GFX position during flight**
|
|
- Must update manually (moves independently)
|
|
- But can use hook Transform for initial position
|
|
|
|
2. **Target position for fly GFX movement calculation**
|
|
- Still need to get target position (from hook or default)
|
|
- But only need to get it once at start, or when target moves
|
|
|
|
3. **Absolute offset calculations** (one-time)
|
|
- Still need to calculate once for absolute offsets
|
|
- But only once, not every frame
|
|
|
|
---
|
|
|
|
## Implementation Plan
|
|
|
|
### Phase 1: Add Hook Transform Access
|
|
|
|
**File:** `SkeletonBuilder.cs` or `CECModel.cs`
|
|
|
|
```csharp
|
|
// Add method to get hook Transform (not just position)
|
|
public Transform GetHookTransform(string hookName, bool recursive = false)
|
|
{
|
|
if (m_hookCache.TryGetValue(hookName, out Transform hook))
|
|
return hook;
|
|
|
|
// Lookup hook by name
|
|
hook = GetHook(hookName, recursive);
|
|
|
|
if (hook != null)
|
|
m_hookCache[hookName] = hook;
|
|
|
|
return hook;
|
|
}
|
|
```
|
|
|
|
### Phase 2: Update Hit GFX Spawning
|
|
|
|
**File:** `CECSkillGfxMan.cs` - `CECSkillGfxEvent`
|
|
|
|
```csharp
|
|
private void SpawnHitGfx(Vector3 vTarget)
|
|
{
|
|
// Try to get target hook Transform
|
|
Transform targetHook = GetTargetHookTransform();
|
|
|
|
if (targetHook != null && m_pComposer.m_HitPos.bTraceTarget)
|
|
{
|
|
// Parent to hook - automatic updates!
|
|
m_hitGfxInstance = Instantiate(hitGfxPrefab);
|
|
m_hitGfxInstance.transform.SetParent(targetHook);
|
|
|
|
// Apply offset
|
|
if (m_pComposer.m_HitPos.bRelHook)
|
|
m_hitGfxInstance.transform.localPosition = m_pComposer.m_HitPos.vOffset;
|
|
else
|
|
{
|
|
// Absolute offset - calculate once
|
|
Vector3 offsetWorld = targetHook.root.TransformPoint(m_pComposer.m_HitPos.vOffset);
|
|
m_hitGfxInstance.transform.position = offsetWorld - targetHook.root.position + targetHook.position;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback: manual position
|
|
m_hitGfxInstance = Instantiate(hitGfxPrefab, vTarget, Quaternion.identity);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 3: Update Fly GFX Spawning
|
|
|
|
```csharp
|
|
private void SpawnFlyGfx()
|
|
{
|
|
Transform hostHook = GetHostHookTransform();
|
|
|
|
Vector3 spawnPos;
|
|
Quaternion spawnRot;
|
|
|
|
if (hostHook != null)
|
|
{
|
|
// Use hook Transform for initial position
|
|
if (m_pComposer.m_FlyPos.bRelHook)
|
|
{
|
|
spawnPos = hostHook.TransformPoint(m_pComposer.m_FlyPos.vOffset);
|
|
spawnRot = hostHook.rotation;
|
|
}
|
|
else
|
|
{
|
|
spawnPos = hostHook.position + m_pComposer.m_FlyPos.vOffset;
|
|
spawnRot = hostHook.rotation;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback: use calculated position
|
|
spawnPos = m_pMoveMethod.GetPos();
|
|
spawnRot = Quaternion.LookRotation(m_pMoveMethod.GetMoveDir());
|
|
}
|
|
|
|
m_flyGfxInstance = Instantiate(flyGfxPrefab, spawnPos, spawnRot);
|
|
// Don't parent - fly GFX moves independently
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
### ✅ **YES, we can significantly simplify the hook system in Unity:**
|
|
|
|
1. **Hit GFX that follows target:** Parent to hook Transform → Unity handles updates automatically
|
|
2. **Fly GFX spawn position:** Use hook Transform.position (one-time) → No manual calculation
|
|
3. **Relative offsets:** Use `transform.localPosition` → No matrix math needed
|
|
4. **Absolute offsets:** Calculate once (not every frame) → Much simpler
|
|
|
|
### ⚠️ **Still need manual updates for:**
|
|
1. **Fly GFX during flight:** Must update position manually (moves independently)
|
|
2. **Target position lookup:** Still need to get target position (but only when needed)
|
|
|
|
### 🎯 **Result:**
|
|
- **Eliminates ~80% of manual position calculations**
|
|
- **Simpler, more Unity-native code**
|
|
- **Better performance** (Unity's transform system)
|
|
- **Easier to maintain**
|
|
|
|
---
|
|
|
|
**End of Document**
|