273 lines
8.6 KiB
Markdown
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.
|