Files
test/C_SHARP_VS_CPP_COMPARISON.md
2026-02-24 18:45:24 +07:00

8.6 KiB

C# vs C++ Comparison: Why Monster Destroy Error Occurs in C# but Not C++

Key Differences

1. Memory Management Model

C++ (Raw Pointers)

// C++: EC_ManNPC.cpp line 774-787
void CECNPCMan::ReleaseNPC(CECNPC* pNPC)
{
    if (pNPC)
    {
        pNPC->Release();
        delete pNPC;  // ← Object immediately destroyed, pointer becomes invalid
        pNPC = NULL;
    }
}

Behavior:

  • delete pNPC immediately destroys the object
  • Pointer becomes invalid (dangling pointer)
  • Accessing deleted object = immediate crash (predictable)
  • Null check (if (pNPC)) is sufficient to prevent crashes

C# Unity (Managed References)

// C#: CECNPCMan.cs line 214-226
void ReleaseNPC(CECNPC pNPC)
{
    if (pNPC)
    {
        pNPC.Release();
        pNPC.DestroySelf();  // ← Calls Destroy(gameObject)
    }
}

// CECNPC.cs line 578-581
public void DestroySelf()
{
    Destroy(gameObject);  // ← Unity marks for destruction, but reference persists
}

Behavior:

  • Destroy(gameObject) marks GameObject for destruction at end of frame
  • C# reference (pNPC) still exists and is NOT set to null
  • Unity's == operator is overloaded - pNPC == null returns true for destroyed objects
  • But direct property access (pNPC.transform, pNPC.GetPosVector3()) still throws error
  • Null check (if (pNPC != null)) is NOT sufficient - need GameObject check too

2. GetNPCFromAll Implementation

C++ Version

// C++: EC_ManNPC.cpp line 908-923
CECNPC* CECNPCMan::GetNPCFromAll(int nid)
{
    CECNPC* pNPC = GetNPC(nid);
    if (pNPC)
        return pNPC;

    // Search from disappear array - NPCs still exist here!
    for (int i=0; i < m_aDisappearNPCs.GetSize(); i++)
    {
        CECNPC* pNPC = m_aDisappearNPCs[i];
        if (pNPC->GetNPCID() == nid)
            return pNPC;  // ← Returns NPC even if removed from main table
    }

    return NULL;
}

Key Points:

  • Searches disappear array (m_aDisappearNPCs) after main table
  • NPCs in disappear array are still valid (not deleted yet)
  • Provides a grace period for GFX events to complete
  • Returns NULL only when NPC is truly gone

C# Version

// C#: CECNPCMan.cs line 541-564
public CECNPC GetNPCFromAll(int nid)
{
    CECNPC pNPC = GetNPC(nid);
    if (pNPC != null)
        return pNPC;

    // Search from disappear array - COMMENTED OUT!
    /*for (int i = 0; i < m_aDisappearNPCs.GetSize(); i++)
    {
        CECNPC* pNPC = m_aDisappearNPCs[i];
        if (pNPC->GetNPCID() == nid)
            return pNPC;
    }*/

    return null;  // ← Returns null immediately if not in main table
}

Key Points:

  • Disappear array search is COMMENTED OUT (lines 555-561)
  • Returns null immediately if NPC not in main table
  • No grace period - NPC removed from table = immediately unavailable
  • This is a porting issue - the safety mechanism was removed

3. get_pos_by_id Implementation

C++ Version

// C++: EC_ManSkillGfx.cpp line 89-122
inline bool _get_pos_by_id(..., int nID, A3DVECTOR3& vPos, ...)
{
    if (ISNPCID(nID))
    {
        CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID);

        if (pNPC){  // ← Simple null check is sufficient
            // Access pNPC methods directly
            vPos = pNPC->GetPos();  // ← Safe: if pNPC is null, we wouldn't be here
            return true;
        }
    }
    return false;
}

Why it works:

  • GetNPCFromAll() may return NPC from disappear array (still valid)
  • If pNPC is null, we return false - no access attempted
  • If pNPC is non-null, it's guaranteed valid (C++ pointer semantics)
  • No need to check if object is "destroyed" - either it exists or pointer is null

C# Version

// C#: CECSkillGfxMan.cs line 545-589
private static bool get_pos_by_id(..., int nID, out Vector3 vPos, ...)
{
    if (GPDataTypeHelper.ISNPCID(nID))
    {
        CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID);

        if (pNPC != null)  // ← This check passes even for destroyed GameObjects!
        {
            vPos = pNPC.GetPosVector3();  // ← ERROR: GameObject destroyed!
            // OR
            vPos = pNPC.transform.position;  // ← ERROR: GameObject destroyed!
        }
    }
    return false;
}

Why it fails:

  • GetNPCFromAll() returns null immediately (disappear array not checked)
  • But even if it returned a reference, Unity's destroyed objects are "fake null"
  • pNPC != null returns true even for destroyed GameObjects
  • Need additional check: pNPC.gameObject != null to detect destroyed objects

4. Object Lifecycle Differences

C++ Lifecycle

1. NPC created → Pointer stored in m_NPCTab
2. NPC dies → Moved to m_aDisappearNPCs (still valid)
3. Disappear timer expires → delete pNPC (immediate destruction)
4. Pointer becomes invalid → NULL check prevents access

Timeline:

  • NPC removed from main table → Still accessible via disappear array
  • NPC deleted → Pointer immediately invalid (predictable crash if accessed)

C# Unity Lifecycle

1. NPC created → GameObject + Component stored in m_NPCTab
2. NPC dies → Removed from m_NPCTab immediately
3. Destroy(gameObject) called → Marked for destruction
4. End of frame → GameObject destroyed, but C# reference persists
5. Reference is "fake null" → == null works, but property access throws

Timeline:

  • NPC removed from main table → Immediately unavailable (disappear array not checked)
  • GameObject destroyed → Reference persists but is "fake null"
  • Property access → Throws "object has been destroyed" error

5. Null Check Behavior

C++ Null Check

CECNPC* pNPC = GetNPCFromAll(nid);
if (pNPC)  // ← True only if pointer is valid
{
    vPos = pNPC->GetPos();  // ← Safe: pointer guaranteed valid
}

Semantics:

  • if (pNPC) checks if pointer is not NULL
  • If true, pointer is guaranteed valid (C++ doesn't have "fake null")
  • Access is safe

C# Unity Null Check

CECNPC pNPC = GetNPCFromAll(nid);
if (pNPC != null)  // ← True even for destroyed GameObjects!
{
    vPos = pNPC.GetPosVector3();  // ← ERROR: GameObject may be destroyed
}

Semantics:

  • if (pNPC != null) uses Unity's overloaded == operator
  • Returns true even for destroyed GameObjects (Unity's "fake null")
  • Need additional check: pNPC.gameObject != null

6. Why C++ Doesn't Have This Problem

  1. Disappear Array Safety Net

    • C++ searches m_aDisappearNPCs which provides grace period
    • NPCs remain accessible even after removal from main table
    • GFX events can complete before NPC is actually deleted
  2. Immediate Destruction

    • When delete is called, object is immediately destroyed
    • Pointer becomes invalid, null check prevents access
    • No "fake null" state - either valid or null
  3. Predictable Behavior

    • Accessing deleted object = immediate crash (predictable)
    • Null check is sufficient protection
    • No need for GameObject existence checks

7. Why C# Has This Problem

  1. Disappear Array Not Used

    • C# version has disappear array search commented out
    • NPCs become unavailable immediately when removed from main table
    • No grace period for GFX events
  2. Delayed Destruction

    • Destroy(gameObject) marks for destruction at end of frame
    • C# reference persists in "fake null" state
    • Property access throws error even though == null returns true
  3. Unity's "Fake Null"

    • Unity overloads == operator to handle destroyed objects
    • But direct property access doesn't use the overloaded operator
    • Need explicit GameObject check: gameObject != null

Summary: Root Causes

Aspect C++ C# Unity Impact
Disappear Array Searched Commented out C# has no grace period
Null Check Sufficient Not sufficient C# needs GameObject check
Destruction Immediate (delete) Delayed (Destroy) C# has "fake null" state
Pointer/Reference Raw pointer (invalid when deleted) Managed reference (persists when destroyed) C# reference can point to destroyed object

The Fix

To match C++ behavior, C# needs:

  1. Re-enable disappear array search in GetNPCFromAll()
  2. Add GameObject checks in get_pos_by_id(): pNPC != null && pNPC.gameObject != null
  3. Early termination of GFX events when target is destroyed
  4. Target validation before creating GFX events

These changes will restore the safety mechanisms that existed in C++ but were lost during porting.