0
\$\begingroup\$

I have a class named GameManager, which job is to manage GameObject allocation and deallocations. The thing is, every GameObject can interact with each other, so there's a possibility that a pointer to another GameObject in a GameObject's script becomes deleted by manager the next frame. (Something like a missile, the missile's target may be already destroyed by another object)

I can't use shared pointer because there are times where a GameObject should be removed, but can't be removed because a GameObject won't release. What is a good design pattern to tackle this problem? Or should GameObject check for null each time?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ std::weak_ptr will be your friend. \$\endgroup\$ – LukeG Aug 28 '16 at 15:10
  • \$\begingroup\$ @LukeG ah, I just thought about that. I guess you're right, thank you \$\endgroup\$ – Rei Aug 28 '16 at 15:19
2
\$\begingroup\$

There are two facets to this issue:

First, you may want to defer destruction until the end of the frame (or at least until some time after you know all your inter-object interactions will have been resolved). This is fairly straightforward: when you "destroy" an object, put that object into a "pending kill" set and at the end of the frame, iterate the set, actually destroying every object and then emptying the set.

Second is the issue of outstanding references to dead objects from live ones. This is a much larger design issue for object lifetime and there's a lot of ways you can handle it. You need to decide certain things about how your objects will function to choose a solution.

  • Does a reference to an object never need to keep that object alive?
  • Does a reference to an object always need to keep that object alive?
  • Does a reference to an object sometimes need to keep an object alive? Under what conditions, and are those conditions determined at compile time or at run time?
  • Or you simply need to be able to tell if a referenced object has been destroyed and consequently prevent accessing it?

(When I say "keep alive" I mean doing so simply by virtue of the reference existing. Whether or not you can request destruction of that object via that reference is another thing altogether.)

If a reference never needs to keep an object alive, you are in the ideal world. This probably means that reference is the only reference to the object, though, and the containing objects entirely controls its lifetime. If you are extremely rigid about the way your objects are created and destroyed you can make this work (always ensuring objects which have non-lifetime-preserving references are destroyed before the things they reference are, in and inside-out fashion), but it takes discipline.

If a reference must always keep an object alive, a reference-counting approach is potentially what you want to look at. You will also want to include some way of making non-counting references (so called "weak pointers," like C++'s std::weak_ptr) to break cycles or otherwise create non-counting references. You can also look into solutions involving garbage collection, although in some languages (like C++) this would be quite an intensive option. It's possible though (Unreal does it).

If a reference sometimes needs to keep an object alive and those conditions are determined at compile time, a reference-counting approach can still be used, you just choose a weak reference for this instance. If the conditions are determined at compile time, you'd have to use a more complex reference counting approach where a reference can switch from counting to non-counting, and I'd argue this is a sign you need to make your ownership policies more rigid instead.

If a reference simply needs to be able to determine if a thing is destroyed, you're probably in the second-best scenario. This is where you should try to be, in my opinion (reference counting makes overall determinism related to destruction harder to reason about). There are various handle-based solutions to object management that can work here. The crux of this approach is to keep all your objects in one place along with some metadata that includes a "salt" value, and hand out handles which reference those objects. The handle holds a pointer or index that refers to the actual object, and a salt value of its own. Whenever you actually delete an object, you increment the salt for that object's slot in the object storage. That way allows handles to test their salt against the current salt in storage and if they differ, the handle refers to a dead object.

There are various structures that can be used to implement this, such as innumerable variations on a slot map or related data structures like plf::colony (which can be conceptually adapted to support salted handles instead of iterators or bare pointers).

\$\endgroup\$
4
  • \$\begingroup\$ Hi, that's a comprehensive explanation, thank you. Yeah, I think I'm in that second best scenario you described, what's the advantage of using salt over checking for null pointer? \$\endgroup\$ – Rei Aug 29 '16 at 4:42
  • \$\begingroup\$ When you delete an object and null out that pointer, other pointers to that object are unaffected. They aren't set to null. So checking for null does not work if you're just passing around pointers that may outlive the lifetime of the thing they are pointing to. You need something more, thus the salt. \$\endgroup\$ – user1430 Aug 29 '16 at 13:56
  • \$\begingroup\$ Aaah, so it's the same as weak_ptr? \$\endgroup\$ – Rei Aug 30 '16 at 0:08
  • 1
    \$\begingroup\$ Conceptually it is similar in some ways. Implementation-wise it is very different. \$\endgroup\$ – user1430 Aug 30 '16 at 0:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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