Rendering using Weak References, and the GC

2019-09-08 15:30发布

问题:

The Problem

I've recently started learning C#. I am doing this through making a game (as I am quite familiar with this in C++).

Objects that are to be drawn to the back buffer are 'registered' upon construction via an event being sent with a weak reference to the object. With C++ smart pointers being reference counted and (as a result) objects being destroyed as soon as the last reference goes out of scope, this works well for my purposes. However, in C#, this is certainly not the case.

Consider the following code snippet:

foreach(WeakReference<DrawableObject> drawable in drawables.ToList())
{
    DrawableObject target;
    drawable.TryGetTarget(out target);
    if(target != null)
    {
        spriteBatch.Draw(target.Texture, target.Position, target.Rectangle, target.Colour);
    }
    else
    {
        drawables.Remove(drawable);
    }
}

The main problem (as I'm sure there are most likely others I am yet to understand) with this in C#, is that target is not guaranteed to be null after the last strong reference to the object goes out of scope.


Potential Solutions

Forcing garbage collection

I could force invocation of the GC at an appropriate time beforehand, but after some research into the idea I have discovered that this is most likely a bad idea and/or a sign of other issues in the code.

Using a Hide() method

Whilst such a method is essential to any game, having to explicitly call Hide() every single drawable object is tedious and gives more room for error. Furthermore, when should all these Hide()'s be called? I cannot rely on the parent object being collected in time either, so placing them in a Finalizer would not solve the problem.

Implementing IDisposable

Similar to the above, with the alternative of a using statement (which I would not know where to place, since the objects need to be alive until their parents go out of scope).


The Question(s)

None of the solutions I could think of seem to appropriately tackle the problem. I of course missed out the idea of not using weak references in this way whatsoever, but will have to consider doing so if no proper solution is found that can work with the game in its current state. All this boils down to a few related questions:

  • Is this an appropriate use case for forcing garbage collection?

  • If not, how can I determine whether an object is due to be collected when the next run of the GC occurs?

  • Is there a way to call a method of an object as soon as the last reference to it goes out of scope (either implicitly or explicitly), rather than waiting for the GC to come along?

I am, of course, grateful for any suggestions that would avoid this problem altogether too.

回答1:

I'm going to attempt an answer at this..

What you're looking for is "I am dead according to the rules of the game" logic. Not "That object is dead because the Garbage Collector says so" logic.

To that effect.. you need to flip your thinking to something like this:

class DrawableObject {
    public bool IsDead { get; set; }
    public int Health { get; set; }

    // ...
    public void GetShot(int amount) {
        Health -= amount;

        if (Health <= 0)
            IsDead = true;
    }
}

..then:

foreach (var drawable in drawables) {

    DrawableObject target;
    drawable.TryGetTarget(out target);

    if(target == null || target.IsDead) {
        drawables.Remove(drawable);
    }
    else {
        spriteBatch.Draw(target.Texture, target.Position, target.Rectangle, target.Colour);
    }
}

If its null based on your TryGetXXX logic.. or it has been marked as dead by your game logic.. remove it from the list of drawable objects. Let the garbage collector clean it up whenever it wants after that.

TLDR: Mark your objects as dead and base your logic around your own "I am dead" flag.