Files
test/HOOK_SYSTEM_UNITY_PARENTING_APPROACH.md
T
2026-03-05 17:39:11 +07:00

13 KiB

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:

// A3DSkillGfxEvent2.cpp:502-508
if (m_bTraceTarget)
{
    A3DMATRIX4 matTran;
    matTran.Identity();
    matTran.SetRow(3, GetTargetCenter());  // Updates every frame
    m_pHitGfx->SetParentTM(matTran);
}

Unity Approach:

// 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:

// 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:

// 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:

// Relative offset: transform offset in hook's local space
if (bRelHook) {
    vPos = pHook->GetAbsoluteTM() * (*pOffset);
}

Unity Approach:

// 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

For Hit GFX that follows target:

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:

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:

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

// 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

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

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