8.6 KiB
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 pNPCimmediately 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 == nullreturnstruefor 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
NULLonly 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
nullimmediately 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
pNPCis null, we return false - no access attempted - If
pNPCis 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()returnsnullimmediately (disappear array not checked)- But even if it returned a reference, Unity's destroyed objects are "fake null"
pNPC != nullreturnstrueeven for destroyed GameObjects- Need additional check:
pNPC.gameObject != nullto 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
trueeven for destroyed GameObjects (Unity's "fake null") - Need additional check:
pNPC.gameObject != null
6. Why C++ Doesn't Have This Problem
-
Disappear Array Safety Net
- C++ searches
m_aDisappearNPCswhich provides grace period - NPCs remain accessible even after removal from main table
- GFX events can complete before NPC is actually deleted
- C++ searches
-
Immediate Destruction
- When
deleteis called, object is immediately destroyed - Pointer becomes invalid, null check prevents access
- No "fake null" state - either valid or null
- When
-
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
-
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
-
Delayed Destruction
Destroy(gameObject)marks for destruction at end of frame- C# reference persists in "fake null" state
- Property access throws error even though
== nullreturns true
-
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
- Unity overloads
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:
- Re-enable disappear array search in
GetNPCFromAll() - Add GameObject checks in
get_pos_by_id():pNPC != null && pNPC.gameObject != null - Early termination of GFX events when target is destroyed
- Target validation before creating GFX events
These changes will restore the safety mechanisms that existed in C++ but were lost during porting.