Game Engine Development for the Hobby Developer Part 4, Utilities

You’ve thought about the rendering engine in Part 1, taken a look at all of the work needed in Part 2, and covered the sorts of functions you’ll need for audio in Part 3. Now we’ll go over the various “utilities” that you might need.

Utilities are the part of the engine that I’m not sure where to put something. For example, in my engine, utilities covers little things like converting a wide-string into an ANSI string; converting dates and times, random number functions; those sorts of things. Let’s take a look at what I have in my engine.

List of Utility Functions

Here’s a list of all of the utility functions and classes that are in my “Utilities” library in my 2D game engine:

  • Color
    This contains a 32bit typedef to define a colour (4x 8-bit values, R, G, B, A) as well as a few functions to create a colour (basically bit packing) based on either 4 8-bit integers or 4 float values.
  • DataPacket
    I use the data packets in a few places. They are used in the messaging sub-system, to pass data from one part of the engine to another, and they’re also used in the network system to pass data from game client to game server.
  • Message
    This is the messaging sub-system
  • Random
    Contains all of the random number generators that I need. Random Number Generation is not stateless in my engine, you need to define a class instance, but a stateless function is probably a good idea too.
  • Rect
    Defines a rectangle class as well as some collision detection and sub-rectangle selection functions.
  • Vector
    Defines a vector class as well as things like dot and cross product functions
  • Resource
    A templated class that allows me to re-load resources as the game is running. Allows me to see the new graphics instantly, without needing to exit and reload.
  • Timer
    A class that is used to register callbacks to occur after a given amount of time has passed. I’ve found it helpful to de-couple callbacks from the calling code, but I need to be really careful that all references remain etc.
  • Utils
    A bunch of random functions that don’t really fit anywhere else but seem useful to keep around. Things like “does this line segment pass completely through this rectangle” and “convert cartesian coordinate to polar coordinate”
  • Win32
    Bunch of functions that I use to wrap around various Windows APIs. If I ever wanted to target Linux I would replace this with wrappers around both APIs, but for now I’m only worried about Windows computers.

I’ll go through some of the more interesting ones now in a bit more detail.

Data Packet

This class is encapsulates a way to pack arbitrary data into a packet on the sending side and, assuming the receiver knows the format of the data, a way to extract it. In addition, this namespace also has a few streaming template functions that can be overridden by other classes, though I’ve never really used the streaming much yet.

Message

This namespace is used for to send and receive messages between engine components, so it’s one of the most important systems. It’s exceptionally simple though.

Firstly, a 64-bit integer is used as a Message Type Identifier. Basically, messages need to identify what type of message they are. Systems that generate messages can define new Message Types to send. Systems that subscribe to these messages need to know the Message Types they’re interested in.

The Message itself is simply a Message Type and a Data Packet. In this way, any message can contain any arbitrary data to go along with it.

The PostMaster is a static class that all the messages get sent through. It’s also where you register a new Message Type and where all the subscribers to a type are stored. Here’s a diagram to illustrate the way the system works:

The Messaging System
  1. System A sends some kind of registered message type, registered meaning that other systems can request to subscriber to messages of that type. A Message is inherited from DataPacket, it’s as described above.
  2. This is sent via the Post method, which effectively adds it to a queue
  3. Meanwhile, System B, which has no knowledge of System A, subscribes to a type of message. The only thing System B needs to know is that message type. I keep these message types in a shared header that everyone can access. Subscribing to a message type adds it to a list of subscribers for that type
  4. Once per-frame, the Process method is called, which takes messages off the queue and sends it to the correct subscribers. It’s sent via a provided functor object.

Resource

This is used to allow shared access to all resources. I consider an image or sound-file, even a map file, to be a heavy resource. I use this templated class to abstract away a lot of things and allow hot-loading of resources during the run of the game.

Hot-loading is incredibly useful. I can see the changes that a graphic has on the game instantly. I could expand this to hot-load shader code, maps, anything at all. The best way to demonstrate if through some code:


template<typename T> class ResourceManager { public: typedef T Type; typedef std::string IDType; typedef std::shared_ptr<T> ResourceHandle; struct Resource { float timeSinceReload; ResourceHandle data; bool onDisk; }; typedef std::unordered_map<IDType,Resource> MapType; typedef std::function<bool(const IDType&,T&)> LoadFunc; typedef std::function<void(T&)> ReleaseFunc; typedef std::function<bool(const IDType&,T&)> ReloadFunc; typedef std::function<IDType(const T&)> GetIdFunc; static ResourceHandle GetResource(const IDType& name) { typename MapType::iterator itr = m_nameResourceMap.find(name); if(itr != m_nameResourceMap.end()) { return itr->second.data; } ResourceHandle resourceData(new T); if(!m_loadFunc(name,*resourceData)) { std::cerr << "ERROR: Resource Manager could not load resource name \"" << name << "\" of type " << typeid(T).name() << std::endl; return ResourceHandle(nullptr); } Resource newResource; newResource.data = resourceData; newResource.timeSinceReload = 0; newResource.onDisk = true; m_nameResourceMap.emplace(name,newResource); return resourceData; } static ResourceHandle GetResource(const T& data) { IDType newId = m_getIdFunc(data); typename MapType::iterator itr = m_nameResourceMap.find(newId); if(itr == m_nameResourceMap.end()) { ResourceHandle resourceData(new T); *resourceData = data; Resource newResource; newResource.data = resourceData; newResource.timeSinceReload = 0; newResource.onDisk = false; m_nameResourceMap.emplace(newId,newResource); return resourceData; } return itr->second.data; } static void Destroy() { typename MapType::iterator itr = m_nameResourceMap.begin(); while(itr != m_nameResourceMap.end()) { m_releaseFunc(*(itr->second.data)); ++itr; } } static void Update(const float elapsedMilliseconds) { typename MapType::iterator itr = m_nameResourceMap.begin(); while(itr != m_nameResourceMap.end()) { if(itr->second.data.use_count() == 1) itr = m_nameResourceMap.erase(itr); else ++itr; } } static bool ReloadResources() { typename MapType::iterator itr = m_nameResourceMap.begin(); while(itr != m_nameResourceMap.end()) { itr->second.timeSinceReload = 0; if(!m_reloadFunc(itr->first,*(itr->second.data))) { std::cerr << "ERROR: Resource Manager could not RE-load resource \"" << itr->first << "\" of type " << typeid(T).name() << std::endl; // some can't be loaded because they weren't files but data } ++itr; } return true; } private: inline static MapType m_nameResourceMap = {}; static LoadFunc m_loadFunc; static ReleaseFunc m_releaseFunc; static ReloadFunc m_reloadFunc; static GetIdFunc m_getIdFunc; };

There’s really only a couple methods to look at:

  • GetResource basically executes one of two possibilities (depending on the parameter). It could load the resource from disk, in which case it’s been provided with the filename; or it could take ownership of the resource if it as loaded manually or constructed randomly etc.
  • Destroy calls a release function to dispose of the resource correctly
  • Update checks if there’s only one instance of the std::shared_ptr remaining (std::shared_ptr is used as the handle to the actual resource). If there is, then no one is using it and we can unload the resource by destroying the last copy of the std::shared_ptr (that we hold)
  • ReloadResources calls the reload function. You call this on a certain keyboard combination press, or every x seconds or something

What’s missing is where those load, reload, release functions are. Well, you need to specialise this class for every kind of resource you’re handling. Within those specialisations is where you define those functions. This allows the resource-handling code to exist alongside the resources themselves rather than in some gargantuan “ResourceManager” type of class.

Timer

This is the final interesting Utility in my engine. It came about because I’m often in need of something being executed in n seconds from now. I also love C++11 Lambdas.

Basically, you keep an instance of Timer for everything you need to execute later. I didn’t have it completely disconnected from what was calling it because I didn’t need it to, and because I was worried about dangling pointers and references if you could just hand a functor off to be executed whenever.

The instance of Timer gets a callback set, which is the functor that’s executed after so many milliseconds. Internally it has a timer that you need to keep updated by calls to an update method. There’s finally looping options to have it repeat or not. Because of the way it works, the functor can also change the repeat options of the timer that’s calling it.

I use it a lot, it’s very helpful. It’s like every class that uses it doesn’t need to keep track of a whole bunch of state to know to delay execution of something. It’s come in handy when animations need to play out, or the state shouldn’t finish until the fade effect has completed.


That’s it for this fourth and final article in the series. I hope it’s proven useful for someone trying to make a game engine. It takes a lot of knowledge and a lot of skill, so I hope you’re able to finish it off and then make a game with it.

Leave a Comment