1

I am working on a small scale game engine and have reached a point where I would like to destroy a Game Object (destroy meaning all references to an object become null). This is important because even if I remove the object from the list of objects I want to update, there could be lingering references still in memory from anything that had a reference to the object, preventing it from being collected. However after some research it is clear that there is no way to outright delete an object in C#. Despite this fact the Unity Game Engine has a method that can destroy an object outright.

I found this thread on the Unity forums that explains how Unity does it. It uses a method dubbed as a "smart reference" or a "handle" where there is a wrapper class of sorts around the actual object where only the wrapper references the real object everything references the wrapper. When the object needs to be destroyed, the wrapper removes its reference to the real object, allowing it to be freed when the garbage collector is ready.

This all makes sense to me I am confused about some details. For one, how could I get properties and call functions of the real object class directly from the wrapper? Unity allows you to access properties directly like so gameObject.transform instead of gameObject.obj.transform implying one of four things

  1. The GameObject wrapper is filled with methods and properties that just call the real object's methods and properties (seems horrible to maintain having to make sure every method has a duplicate)
  2. The GameObject wrapper stores the actual data and returns null on every property if the real object was destroyed (this does solve other things messing around with objects that no longer exist but it keeps all of the data in memory so does not solve anything)
  3. Something related to C++ which I wouldn't be able to implement in pure C#
  4. Some method I don't know about

If anybody has the answers I need or knows a source I could learn more about these "smart references" please let me know, it would be greatly appreciated

1
  • 1
    If this question is about Unity game development then the tags should reflect that. I have added an appropriate tag. Please apply ALL tags that are appropriate in future. It's in your best interests to do so because many people watch specific tags for questions relevant to them. Commented Mar 3 at 5:00

1 Answer 1

3

This all makes sense to me I but am confused about some details

Yes! you are confused about this detail:

When the object needs to be destroyed, the wrapper removes its reference to the real object, allowing it to be freed when the garbage collector is ready.

But you're not that confused because you correctly surmised:

Something related to C++ which I wouldn't be able to implement in pure C#

What you're missing is that the garbage collector doesn't get involved at all. The managed object is a thin wrapper around a resource allocated by native code. When "destroy" is called on the managed object, it calls back into the native code to do whatever it does to release those resources.

The managed wrapper hangs around until the GC runs, or forever; that doesn't matter. It's just a tiny little wrapper object, not actually holding on to any expensive resources.

Take a look at the source code:

https://github.com/Unity-Technologies/UnityCsReference/blob/e5f43177f856c5f5bfe8537c9ab6f92425fdcc3b/Runtime/Export/Scripting/UnityEngineObject.bindings.cs

    // Removes a gameobject, component or asset.
    [NativeMethod(Name = "Scripting::DestroyObjectFromScripting", IsFreeFunction = true, ThrowsException = true)]
    public extern static void Destroy(Object obj, [uei.DefaultValue("0.0F")] float t);

The method has no body! Everywhere you see extern, that's a method that is implemented in native code somewhere. Calls to that method dispatch via black magic back into unmanaged code.

You also surmised:

The GameObject wrapper is filled with methods and properties that just call the real object's methods and properties (seems horrible to maintain having to make sure every method has a duplicate)

Look at the source code:

https://github.com/Unity-Technologies/UnityCsReference/blob/e5f43177f856c5f5bfe8537c9ab6f92425fdcc3b/Runtime/Export/Scripting/GameObject.bindings.cs

There are 32 extern methods in this file alone. Those all have to be very carefully kept in sync with the signatures of the native code, otherwise it will be a disaster. Yes this is horrible to maintain, so often this sort of code is machine-generated from a schema so that the build can ensure everything agrees on the signatures.

If anybody knows a source I could learn more about these "smart references" please let me know

Let me caution you that correctly implementing a native code interop system that wraps unmanaged resources is a for-advanced-players-only job. Learning more about it is great, running off pell mell trying to implement your own safe handles is a bad idea. A brief read of the extensive comments in the SafeHandle source code should convey the number of issues the interop team had to consider. (Note in particular the use of constrained execution regions, one of the more obscure features of .NET.)

https://referencesource.microsoft.com/#mscorlib/system/runtime/interopservices/safehandle.cs

I would start by reading the platform invoke documentation.

https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke

3
  • 1
    Thank you, I understand now. While I can see myself being able to implement this with enough work, it would probably take quite a bit of time and the ultimate point of this engine is to get a game working sooner rather than later. With this in mind I guess the best option is to be aware of the fact that objects will hang around beyond when they have been removed from the scene and work around that, as well as simply dealing with objects not being freed up (likely requiring the implementation of an object pooling system)
    – Wolf
    Commented Mar 3 at 15:20
  • 1
    @Wolf: Pooling is often a good strategy but it has costs. It reduces collection pressure while increasing the cost of (infrequent) gen 2 collections. Pooling strategies that use finalizers to "resurrect" dead objects back into the pool require a thorough understanding of finalizer semantics. And anything dealing with finalizers requires extremely defensive programming because you don't know what thread your code will run on or what state an object is in when finalized. Commented Mar 3 at 17:09
  • 1
    Compilers do not have the frame rate requirements that game engines do, but code analysis engines that run in the background of IDEs have similar perf budgets, so we thought about these issues a lot when designing Roslyn. Like game engines, compilers also allocate a lot of small short-lived objects, and that means collection pressure. We used pooling strategies to good effect, but getting it good enough required dedicated a lot of engineering resources to measuring performance and doing experiments. Commented Mar 3 at 17:12

Not the answer you're looking for? Browse other questions tagged or ask your own question.