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

273 lines
8.6 KiB
Markdown

# C# vs C++ Comparison: Why Monster Destroy Error Occurs in C# but Not C++
## Key Differences
### 1. **Memory Management Model**
#### C++ (Raw Pointers)
```cpp
// 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)
```csharp
// 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
```cpp
// 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
```csharp
// 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
```cpp
// 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
```csharp
// 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
```cpp
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
```csharp
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.