5
\$\begingroup\$

I'm developing my own game engine in C++ as a learning exercise. I have employed a fairly standard method of handling destroyed GameObjects:

  1. Mark them as destroyed
  2. Have the scene delete destroyed GameObjects at the end of the update cycle

This is perfectly fine, however if I want a GameObject to hold a reference of another GameObject then I need to decide what happens to that now destroyed object.

At the moment I'm using std::shared_ptr to support multiple ownership of GameObjects so that they are held by both the scene and any other GameObjects within the game. This means that if the scene 'destroys' them they will live on as they are still pointed to by any GameObjects still using them. This could potentially cause strange behaviour when GameObjects no longer in the scene are still used by GameObjects in the scene.

There are alternatives involving raw pointers: where the GameObject pointing to another might start trying to use an GameObject that has actually been deleted, or std::unique_ptrs where GameObjects would not be able to hold a pointer to another.

Is the best strategy simply to ask the object if it has been destroyed before using it everywhere in the code base? This seems like a fairly risky way of ensuring issues don't occur.

\$\endgroup\$
  • \$\begingroup\$ "Is the best strategy" With regard to what? Ease of use for the developer? Code efficiency? \$\endgroup\$ – Vaillancourt Jan 25 '16 at 21:14
  • \$\begingroup\$ Good point. Ease of developer largely, with the idea of avoiding mistakes: having to add if(!gameObject.IsDestroyed()) to every function seems unnecessary if there is a better method that would not require the check. If you miss the check you may get errors or completely unknown behaviour... \$\endgroup\$ – sydan Jan 25 '16 at 21:16
  • \$\begingroup\$ Then I guess a shared_ptr is your answer :) \$\endgroup\$ – Vaillancourt Jan 25 '16 at 21:18
  • \$\begingroup\$ Sadly shared_ptr causes problems. I want to use shared_ptrs but I need a solution to the problem in the question. I'll highlight it more in a second, but essentially: if you use shared_ptr dangling pointers to objects will continue to act on objects that are actually destroyed, this will most probably lead to invalid behaviour. \$\endgroup\$ – sydan Jan 25 '16 at 21:21
  • 1
    \$\begingroup\$ You could use weak_ptr \$\endgroup\$ – user253751 Jan 26 '16 at 0:19
7
\$\begingroup\$

At the moment I'm using std::shared_ptr to support multiple ownership of GameObjects so that they are held by both the scene and any other GameObjects within the game

Don't do that. shared_ptr is often the wrong tool for the job, and that certainly applies here. Remember that smart pointers are for managing ownership; shared_ptr is about sharing ownership. You don't want that. You want the core engine to own your game objects, not random game code.

There are various solutions here. The one built-in to C++ (in the standard library) would be weak_ptr, but I advise against those even more strongly than I do against shared_ptr.

One of the more typical approaches in game engines is to use a concept of a handle instead of a raw pointer. In a "pure" ECS, that means that you just hold on to the Entity (which remember, would just be an integer ID and not a pointer!). When you need to manipulate the object, you then either request a (temporary) pointer or you use an interface that takes the handle for each manipulation.

The temporary pointer approach is roughly:

Handle id = /*whatever*/;

Object* object = system->GetObject(id );
if (object != nullptr) {
  object->DoThings(value1, value2);
  auto x = object->DoStuff();
  object->DoUpdate(x + 1);
}

With such an interface, the return pointer is "temporary." That means that you can't hold on to you. You can use it for your local calculations and then must release it. This is safe because the object system will never delete or free objects until the end of the frame during a cleanup step, so even if a game object is deleted, any pointers you've acquired are good.

You can take that a step further and have GetObject return a smart pointer (e.g. a shared_ptr in the simplest case) that "pins" the object in memory while the pointer is in use but allows it to be immediately cleaned up after release. That can be useful if you have a coroutine-based scripting language where some event callback function might be handled across frame boundaries as you don't want scripters to have to deal with pointer validity or object lifetimes. You might be best off using the system/id approach for all script function implementations, though.

Handle id = /*whatever*/;

pin<Object> object = system->PinObject(id);

if (object) {
  object->DoThings(value1, value2);
  auto x = object->DoStuff();
  object->DoUpdate(x + 1);
}

The other approach is to not actually have a publicly available class for your game objects and instead require all manipulation of objects to go through the owning system. The system is then free to manipulate the data structures holding the objects however and whenever it pleases, because nobody external to the system ever has a pointer directly to any internal object. This is an essential interface design technique for data-oriented design (there's more to it, of course).

Handle id = /*whatever*/;

system->DoThings(id, value1, value 2);
auto x = system->DoStuff(id); // returns a "default" value if the id is invalid/dead
system->DoUpdate(id, x + 1);
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ So some of the options I suggest are not completely out of place I see :) \$\endgroup\$ – Vaillancourt Jan 25 '16 at 22:09
  • \$\begingroup\$ Could you expand on the 'pinning' concept a little bit? I think it's quite interesting. How can you stop a user of the pinning system from simply acquiring a pin that lasts forever? Is this something you just have to convey via API? I'm not sure how your example of a pin could facilitate holding the object across frame boundaries? \$\endgroup\$ – sydan Jan 25 '16 at 22:22
  • \$\begingroup\$ @sydan:hat's Mostly a documentation problem. I tend to object to designs that attempt to "stop abuse" anyway (if you don't trust your programmers...). Certainly you want to make correct use of your interfaces easy and obvious! But the more you try to hold people's hands and prevent them from doing stuff, the more you get in their way of doing actual work. And no, my example didn't illustrate cross-frame pins, but threads were a bad example anyway (updating answer). \$\endgroup\$ – Sean Middleditch Jan 25 '16 at 22:49
  • 1
    \$\begingroup\$ @TerraPass: that's a longer conversation than really fits in a comment. Short version is that shared_ptr leads to reference cycles and delayed destruction which leads to incredibly difficult to diagnose memory leaks or crashes. A custom smart handle could at least enforce a back-tracking token system (disallow copies, mandate explicit handle clones that take some kind of identity token, record all live identity tokens in the target). There are good uses for shared_ptr/weak_ptr, but they're rarer than people seem to think. Resource handles aren't the worst use of them. \$\endgroup\$ – Sean Middleditch Jul 24 '16 at 18:27
  • 1
    \$\begingroup\$ It's important that handle values not be reused unless you know there are no outstanding copies of them or you can still get into some of the problems of dangling pointers (but at least you won't dereference random memory). \$\endgroup\$ – Sydius Nov 25 '17 at 19:10
2
\$\begingroup\$

I see a couple of options:

  1. Have the Scene refuse to destroy a GameObject if the ref_count of it's shared_ptr is more than 1. This will oblige your users to take a great care of what happens. This might not be very fun or practical, though.

  2. In your GameObject architecture, add a listener pattern: when GameObject A links to GameObject B, tell B that A is linked to it. So when GameObject B is destroyed, notify all the other linked GameObjects (A) that B has been destroyed and so delete the link.

  3. Don't link your GameObjects together. Keep some kind of reference to the game object that you need instead, and fetch it each frame from the Scene. If the GameObject it's destroyed, the Scene will return nullptr, and you'll know that the other object is dead. Your vector could then contain unique_ptr instead of shared_ptr.

| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

One possible solution could be to use weak_ptr.

A weak_ptr is like a shared_ptr, except it doesn't keep the object alive. If there are no shared_ptrs pointing to the object, it will be deleted, and then any weak_ptrs will hold NULL. You can use the lock method to get a shared_ptr from a weak_ptr.

Example use:

shared_ptr<Thingy> s = make_shared<Thingy>();
// Convert a shared_ptr to a weak_ptr
weak_ptr<Thingy> w = s;
// Convert a weak_ptr to a shared_ptr
shared_ptr<Thingy> s2 = w.lock();
// s, w and s2 point to the object, of course.

s.reset();
// s is a null pointer, w and s2 point to the object

s2.reset(); // deletes the object
// s and s2 are all null pointers now.
// w is also a null pointer (magically!)

shared_ptr<Thingy> s3 = w.lock();
// s3 is also a null pointer

Note that this might not completely eliminate your problem, as the object will still not be destroyed as long as there's a shared_ptr referring to it.

| improve this answer | |
\$\endgroup\$

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.