
Handling references to game objects (I'll call these objects entities) comes up very often. Sometimes it's about parent-child relationships between entities. It is often useful to keep a reference to an object while working with event data, in a task scheduler class, and so on.
Sometimes a plain pointer, reference, or std::reference_wrapper is enough. But raw pointers and references have a problem: if you use them, you have to be sure that the entity the reference points to stays alive and does not move in memory without notifying the objects holding a reference to it. In a well-designed program this is probably achievable.
Still, stale pointers/references cause some of the nastiest bugs. These bugs are hard to track down, not so easy to detect, and they can crash your game. Some entities may end up at different addresses than expected (for example, after you reload a level or decide to recreate all enemies from scratch). Of course, you can manually update all the pointers to the entities you referenced earlier. But this is definitely better done automatically, so there is no chance of forgetting a pointer that later goes stale.
Let's look at how to solve all of these problems. The newest solution (storing references in Lua) is one I only found recently, and it prompted me to write this article, but I want to show that there are other ways to solve this task. Let's begin.
shared_ptr and weak_ptr
Some problems with plain references are solvable with std::shared_ptr and std::weak_ptr. First we create our entities the way we would with std::make_shared. Then we create all references to them with std::weak_ptr, which does not affect your entity's lifetime. After that, we can use std::weak_ptr::expired to check whether the reference is still valid.
This solution is imperfect. First of all, because it requires creating all your entities as shared_ptr, which can carry serious overhead compared to unique_ptr. Ultimately, entity lifetimes are better not shared, so unique_ptr semantics are preferable. Also, shared pointers do not solve the problem of entities moving in memory: you cannot change what is inside a shared_ptr and make all weak_ptr automatically update their pointers.
Using unique ids
The problem could be solved simply by generating unique ids for entities and storing those ids instead of raw pointers or references. There are many ways to generate and represent entity ids. An id can be represented as a plain integer via EntityManager and a counter whose value increases as new entities are created: the first entity gets id=0, the second id=1, and so on. Ids can also be generated with some hashing algorithm or a UUID. Whatever happens, your ids must stay unique, unless you deliberately add extra information to an id (for example, the entity's creation time or some tags).
Here is what your EntityManager class could look like:
class EntityManager {
public:
Entity* getEntity(EntityId id) const;
bool entityExists(EntityId id) const;
// ...
private:
std::unordered_map<EntityId, std::unique_ptr<Entity>> entities;
// ...
};
Using ids also makes the recreation problem easier: you can easily recreate/reload an entity and simply assign it the same id it had before. Yes, it will occupy a new address in memory, but when someone calls getEntity again, they get the updated entity. These ids are also easy to send over the network and store in files.
With entity ids, your code looks like this:
auto entityPtr = g_EntityManager.getEntity(entityId);
entityPtr->doSomething();
Of course, this comes with some overhead, since we now have a level of indirection: we have to look up the unordered_map inside EntityManager to get a plain reference to the entity. But if you do not want to do this too often (most likely you do not), it will work for you!
Here is one more improvement: you can wrap your id in a struct and then overload operator-> so the handle behaves like a plain pointer:
struct EntityHandle {
EntityId id;
EntityManager* entityManager;
EntityHandle(EntityId id, EntityManager* entityManager) :
id(id), entityManager(entityManager)
{}
Entity* operator->() const {
return get();
}
Entity* get() const {
assert(entityManager->entityExists(id));
return entityManager->getEntity(id);
}
};
Now you can do things like this:
EntityHandle handle(someEntityId, &g_EntityManager);
handle->doSomething();
// or...
auto entityPtr = handle.get();
entityPtr->doSomething();
Note that we also gained a form of error checking: we can now make and check assertions about whether references are valid. Great! Now let's look at how to reference entities from Lua scripts.
Storing references to entities in Lua
Full implementation is here: C++ version, Lua version.
First, the obvious point: this approach works in Lua as well. Your handle can be simply a number or a table with an __index metamethod, so the handle can be used exactly like a plain reference. But there is a nicer method I stumbled on recently. Let's see how it works.
We start with handles as tables holding plain C++ references, which are stored in these tables as user data. There is also a boolean isValid that helps us check whether the handle is still valid. We also have a global table of references in Lua, so we can easily get a pointer from anywhere without calling C++. The nice part here is that you get exactly references to your handles, not copies. That is great, because you can easily compare two handles or even use them as table keys. You do not waste memory, though the issue is not that serious anyway since pointers are very lightweight.
If you want to delete or recreate an entity, you must notify the master Lua handle, which is stored in some global Lua table. Since all your Lua handles are references to the master handle, you do not have to worry about them: once you update the master handle, it updates everywhere. Another benefit: as soon as an entity is deleted, you can just set isValid to false and the plain reference to nil — just for extra safety.
On to the implementation! We will use sol2 as the Lua/C++ binding library. Let's write simple Entity and EntityManager classes for testing:
using EntityId = int;
class Entity {
public:
explicit Entity(EntityId id) :
name("John"), id(id)
{}
const std::string& getName() const { return name; }
void setName(const std::string& n) { name = n; }
EntityId getId() const { return id; }
private:
std::string name;
EntityId id;
};
sol::state lua; // globals are bad, but we use them here to keep the implementation simple
class EntityManager {
public:
EntityManager() : idCounter(0) {}
Entity& createEntity()
{
auto id = idCounter;
++idCounter;
auto inserted = entities.emplace(id, std::make_unique<Entity>(id));
auto it = inserted.first; // iterator for the inserted id/entity pair
auto& e = *it->second; // the created entity
lua["createHandle"](e);
return e;
}
void removeEntity(EntityId id)
{
lua["onEntityRemoved"](id);
entities.erase(id);
}
private:
std::unordered_map<EntityId, std::unique_ptr<Entity>> entities;
EntityId idCounter;
};
Here is how we create a handle in Lua:
function createHandle(cppRef)
local handle = {
cppRef = cppRef,
isValid = true
}
setmetatable(handle, mt)
Handles[cppRef:getId()] = handle
return handle
end
All handles are stored in the global Handles table, so we can easily grab them from there later. As you can see, we keep using integer ids as table keys. So if an entity is deleted, we can easily find its handle and make the needed changes.
This metatable has an important function: it lets us use the handle as if it were the original reference. Here is how it is written:
local mt = { }
mt.__index = function(handle, key)
if not handle.isValid then
print(debug.traceback())
error("Error: handle is not valid!", 2)
end
return function(handle, ...) return Entity[key](handle.cppRef, ...) end
end
Quick reminder: the __index metamethod is called when a key is not found in a table, and it receives the table itself (our handle) and the missing key.
Here is an example showing how all of this works. If we do this:
handle:setName("John")
then Lua checks whether the handle table has the key "setName", and it does not. So the __index metamethod is called, with the handle and the value "John" as its arguments. A wrapper around the Entity instance function is returned, and that function gets called. The function we get back is a closure that calls the Entity class instance function through the original plain reference. Why not just return Entity[key]? That would be a problem: Entity[key] would receive our handle, while the function expects a plain reference (cppRef:setName("John"), which is functionally the same as Entity.setName(cppRef, "John")).
The error checking we have set up here is extremely important and useful! It lets us easily debug problems that can come up in the code: we even print the stack trace to find the exact place where our code failed! Note that we pass "2" as the second argument to the error function. This argument tells it that the problem is not in the function that called it, but in another function further down the stack.
Another great property of this implementation is that we can handle errors on the Lua side rather than the C++ side. Every time a C++ function is called from Lua, it is hard to properly catch and throw an error from C++. Lua has to be compiled as C++ for everything to go through. If we throw the error before calling C++, we can easily catch it on the Lua side and handle it correctly. We will look at how this is done shortly.
Let's test our reference first:
function test(cppRef)
local handle = createHandle(cppRef)
testHandle(handle)
end
function testHandle(handle)
print("Hello, my name is " .. handle:getName())
handle:setName("Mark")
print("My name is " .. handle:getName() .. " now!")
end
Output:
It works! What do we do when an entity is removed? Let's write a function for that:
function onEntityRemoved(id)
local handle = Handles[id];
handle.cppRef = nil
handle.isValid = false
Handles[id] = nil
end
It must be called before the entity in question is removed. This function can be placed in the Entity destructor or in EntityManager's removeEntity function. Note that the handle itself is not deleted: someone may still reference it. But it is still useful to set the corresponding value in the Handles table to nil, because if someone tries to look the handle up later, they get nil. More importantly, isValid is set to false. So the next time someone tries to use the handle, an error is raised.
Now let's see what happens if we try to use an invalid reference. Now we can even do error handling on the Lua side!
function testBadReference()
local handle = Handles[0] -- this handle exists and causes no problems
handle.isValid = false -- but suppose this object was removed!
local _, err = pcall(testHandle, handle)
if err then
print(err)
end
end
Calling this function from C++ does not produce the kind of crash we would get from using a stale reference. Instead, we get a useful error message and a stack traceback:
stack traceback:
script.lua:23: in function 'getName'
script.lua:57: in function <script.lua:56>
[C]: in function 'pcall'
script.lua:65: in function <script.lua:62>
script.lua:57: Error: handle is not valid!
What about performance? My tests show that calling a C++ instance function takes about 600 nanoseconds. Not bad, but it may still not be good enough for some people. In that case, it is easy to get the plain reference and then use it with no extra error-checking overhead:
local rawRef = handle.cppRef
print("Raw reference used. Name: " .. rawRef:getName())
We can also speed up the __index function. I found that the biggest cost is creating a closure every time… so let's memoize our wrapper functions! First, let's create a table that will hold our wrapper functions:
local memoizedFuncs = {}
And then change our __index method like this:
mt.__index = function(handle, key)
if not handle.isValid then
print(debug.traceback())
error("Error: handle is not valid!", 2)
end
local f = memoizedFuncs[key]
if not f then
f = function(handle, ...) return Entity[key](handle.cppRef, ...) end
memoizedFuncs[key] = f
end
return f
end
The closure for each function gets created only once, then reused. This makes everything much faster! The overhead drops to about 200 nanoseconds per call.
What else? Calling a function through __index also adds extra overhead. Suppose we use the getName function very often and want to put it into the handle table so the function is called directly. Let's do that!
function createHandle(cppRef)
local handle = {
cppRef = cppRef,
isValid = true
}
handle.getName = function(handle, ...)
return Entity.getName(handle.cppRef, ...)
end
setmetatable(handle, mt)
Handles[cppRef:getId()] = handle
return handle
end
Wait a moment… what happens if getName is called on a broken handle? There is no error check! Let's fix that:
unction getWrappedSafeFunction(f)
return function(handle, ...)
if not handle.isValid then
print(debug.traceback())
error("Error: handle is not valid!", 2)
end
return f(handle.cppRef, ...)
end
end
And then in createHandle we write:
handle.getName = getWrappedSafeFunction(Entity.getName)
Now the overhead is just 70 nanoseconds. It all comes from the extra function call, the validity check, and extracting the plain reference from the handle table, but I think that cost is very small given what we gained.