I recently ran into a memory usage defect for my Noah’s Ark game. Basically, as the player scrolled to different tile maps in the overworld, memory usage would continue to climb, eventually bringing the game to a crawl. The fix was really simple but involved some more subtleties of C++ smart pointers, so it seemed worth writing about.
Graphics Management
First, it is important to understand how graphics are currently managed in the game code. Different types of graphics components are created from the GraphicsSystem
. The GraphicsSystem
is then responsible for rendering all of the graphics on screen.
I’m not necessarily sure this is the “best design” for the game (if you have other ideas, feel free to suggest them!). It is just something I’m experimenting with from various things I’ve read (mostly on entity component systems or ways to avoid having all of my game objects have explicit render methods). My main goal was to be able to easily apply global settings to all of the rendered graphics without having to touch a bunch of classes. Time will tell how well this works out.
Map Scrolling
The second major piece of the puzzle is to understand how the game scrolls between individual tile maps in the overworld. At any given time, the OverworldMap
holds the current, top, bottom, left, and right TileMaps
(assuming they exist):
When the player reaches the edge of the current map, the game scrolls to the appropriate adjacent map (if one exists), and then reloads any new maps surrounding this new map being scrolled to.
The Bug
There is a bit of a problem with the above approach, if you look at the details of the specific data types used. The graphics components created by the GraphicsSystem
are returned as shared_ptrs
and were (before the fix) also being stored as shared_ptrs
in the GraphicsSystem
class. These shared_ptrs
are then used by whatever objects the graphics are for. In this case, these shared_ptrs
were also used by Tiles
in the TileMap
class.
When scrolling to new TileMaps
, the old TileMaps
would be destructed, decrementing the reference counts for the shared_ptrs
for Tiles
in the map. However, since the GraphicsSystem
still stored these shared_ptrs
for the tile graphics, the reference count would still be 1, so the tile graphics would persist in memory. New maps would be loaded, creating new tile graphics components, but all of the old tiles would still exist. Obviously, this is problematic and leads to increasing memory usage over time, which ended up slowing the game to a crawl.
The Solution
I actually was aware of this problem during original implementation of the design outlined above but forgot about it by the time the defect actually manifested itself. This was a pretty major architecture problem, but I didn’t want to abandon the idea of having a centralized place (away from the main game objects) to control rendering.
I couldn’t use unique_ptrs
since the graphics components needed to be accessible by both the GraphicsSystem
and the individual objects the graphics are for. There are ways around this, such as using raw pointers, but that would make communicating when the resources were to be deleted from the GraphicsSystem
pretty ugly (I also don’t like the lack of clarity that raw pointers can present when reading code, but that is for another blog post).
Fortunately, C++ had an elegant solution. Enter the weak_ptr
. The description on the linked page is best to read for an understanding (and examples help), but a weak_ptr
allowed me to break the > 0 reference count problem described above in a pretty nice way.
When an object like a Tile
(part of a TileMap
) is destructed, I wanted it to communicate to the GraphicsSystem
that the graphics for the Tile
could be deleted. To accomplish this, I made the following changes:
The GraphicsSystem
was changed to store its graphics components as weak_ptrs
instead of shared_ptrs
:
std::list< std::weak_ptr<IGraphicsComponent> > m_graphicsComponents;
One nice thing is that the weak_ptr
class has a constructor that allows it to be constructed from a shared_ptr
, meaning I didn’t have to touch the code that added items to the above container.
Next, I had to update the for
loops that iterated over the graphics components to use weak_ptrs
instead of shared_ptrs
. Before updating/rendering an individual graphics component, I needed to check if the object had already been deleted elsewhere (i.e. the Tile
destructor) using the expired
method:
if (!graphicsComponentWeakReference.expired())
(See the Wikipedia page for an overview of the concept of weak references.)
Assuming that the weak_ptr
hasn’t expired, then the lock
method can be called to obtain a usable shared_ptr
to the object:
std::shared_ptr<IGraphicsComponent> graphicsComponent = graphicsComponentWeakReference.lock();
Tada! We’re almost done. The above code allows for checking if a graphics component has been deleted elsewhere, but it doesn’t actually solve the problem of increasing memory usage. The weak_ptrs
are still stored in the list, so they need to be removed at some point. Fortunately, the list
container has a remove_if
that allows us to easily remove elements from the container that meet a certain condition (without resorting to the erase-remove idiom). Removing the expired graphics components can be done in what is effectively a single line of code:
m_graphicsComponents.remove_if([](std::weak_ptr<IGraphicsComponent>& graphicsComponent) { return graphicsComponent.expired(); });
That’s it! You can browse the full set of changes for this fix here. Finding and fixing this bug was a good learning experience for me, so hopefully it will be of use to some of you.