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);
weak_ptr
\$\endgroup\$