In order to create certain tools, you’re going to need to have some basics in your engine. Some of these are fairly obvious and some are not.
Entity Component System
WAIT DON’T SKIP THIS SECTION!
I know, I know. You know to do this. You probably have it already. But the key word here is SYSTEM.
If it turns out you do not know this, you’re probably a freshman, and that’s okay. This section will not cover Entity or Component, and instead primarily focus on the System part of ECS.
Every game engine has some version of an Engine class that runs the main game loop. Your “Engine” class should only do a handful of things.
- Contain a std::vector<System> which have an Initialize and Update function.
- Call Initialize on said systems before the game loop.
- Call Update on every System every frame.
- Jump out when a System tells it to exit the game.
In short, this:
#### main.cpp ####
int main(char **argv, int argc)
{
Engine engine;
engine.RegisterSystem(new GraphicsSystem());
engine.RegisterSystem(new PhysicsSystem());
...
engine.GameLoop();
}
### Engine.cpp ###
void GameLoop()
{
for (System* sys : systems_)
{
sys.Initialize();
}
bool keepRunning = true;
while (keepRunning)
{
// Note that I'm cutting out dt calculations
for (System* sys : systems_)
{
keepRunning = sys.Update(dt) && keepRunning;
}
}
}
Note that this version assumes that the order you register them is the order you will run them in. If you decide to multithread your system updates, you’ll need to add the ability to register dependencies between these systems as well.
What’s great about this is the engine has no idea what Systems exist inside of it. This is a technique called Dependency Injection. Dependency injection is when a class is given its dependencies rather than building them in the class. This is done by putting out a contract of some kind of what the class requires and external classes provide those dependencies. In C++, that’s an abstract class that you derive from. In Java, that’s an interface.
A dopey version of an engine class might look something like this:
Engine.cpp:
Engine()
: graphicsSys_(new GraphicsSystem())
, physicsSys_(new PhysicsSystem())
, logicSys_(new LogicSystem())
...
{}
void GameLoop()
{
bool keepRunning = true;
while (keepRunning)
{
// Again, cutting out dt calculations. Assume it exists.
keepRunning = graphicsSys_.Update(dt) && keepRunning;
keepRunning = physicsSys_.Update(dt) && keepRunning;
keepRunning = logicSys_.Update(dt) && keepRunning;
...
}
}
There’s a few things wrong with this.
One, you can’t easily have multiple versions of the engine. What if you need a version that is the editor? What about a version with certain tools in it (telemetry, logging, metrics, etc)? Are you going to flood Engine.cpp with a bunch of if statements and unneeded dependencies?
Two, this has multiple places you need to change to add/remove systems. If you want to remove a deprecated metrics system (for example), you have to remove it from the constructor and the game loop, and any if statements that conditionally add it! New people to your team have to learn this, too!
Three, this code is very hard to build around. What if you want to time each system? Are you going to put start and stop timer lines around every single system? What if you want to change it to be multithreaded? Are you going to restructure each if statement for every version of the engine you create? What if you want to log when a system is running? Are you going to have log statements inside of every single one of your systems? What if you forget to place one or a new team member doesn’t do? If you just have a function that returns the name of the class, all of this is done automatically!
Maybe you’re not convinced by that. The cons of the bad solution are certainly “workable”. What’s great about dependency injection are the pros. For one, you could handle engine anything. Want to conditionally turn on tools based on command line arguments?
if (argv[i] == "--telemetry") engine.RegisterSystem(new TelemetrySystem());
Done.
Want to register a fake/stub version of something for testing?
if (argv[i] == "--fakeRPCServer")
engine.RegisterSystem(new FakeRPCServer());
else
engine.RegisterSystem(new RPCServer());
Done.
What if we had the ability to convert strings into systems? We could read in systems from a file and load them in. Want to turn off a tool without recompiling? Just remove it from that text file and it’s gone at start up.
That’s right. We’re data driving our systems. Get at me.
Event Messaging System
The most powerful and important system you will ever add to your game is the Event Messaging System. Memory Manager can go to hell.
The most talked about benefit of this is decoupling code, but I don’t think that clearly gets across. Here’s an example.
Let’s pretend you’re making a function that causes the player to jump:
void Jump()
{
velocity.z = 50.0f;
}
Great. Now let’s say we want to log when the player jumps so we can analyze it after a playtest:
#include "Engine/Tools/Logger/Logger.h"
void Jump()
{
velocity.z = 50.0f;
LOG.Event("Player Jumped");
}
Eh…Alright. It’s not that bad, right? But now an animator approaches you and says that it needs to know when the player jumps so it can play the correct animation.
#include "Engine/Tools/Logger/Logger.h"
#include "Engine/Animation/Animator.h"
void Jump()
{
velocity.z = 50.0f;
LOG.Event("Player Jumped");
gAnimator.RegisterChange(PlayerAnimations::Jump);
}
Woof. It’s functional, but it looks like the animation system utilizes their own enum that you have to send. But, whatever, right? It’s only two- and there’s the other tools programmer.
“Hi there. I’m working on a telemetry system so we can visualize where the players went during playtests and locate bottle necks. Can you call this function when the player jumps?”
#include "Engine/Tools/Logger/Logger.h"
#include "Engine/Animation/Animator.h"
#include "Engine/Tools/Telemtry/MessageSender.h"
void Jump()
{
velocity.z = 50.0f;
LOG.Event("Player Jumped");
gAnimator.RegisterChange(PlayerAnimations::Jump);
TELE->SendTelemetryEvent(EventType::Gameplay, EntityType::Player, ActionType::Jump);
}
Oh God why. Not only is this more to compile in this code, it’s also a really strange API. $50 says they are going to change it to something that makes more sense. Now they have to change every single call to that function in your code to be able to fix it!
This problem is endless. There are so many examples of systems that would want to know that the player jumped. Sound effects? Metrics? Input recording? Particle effects? Achievements? Gameplay?
This is just jump! What if you had to add it for shoot, wall climb, dive and sucker punch, too?
What if you conditionally want to use the logger for jumping (verbose mode)? What if you only want to send telemetry events in non-release builds? Are you going to put all of that logic into that jump function?
Instead of going down that horrible rabbit hole any further, we can create an Event Messaging System (EMS).
The code above becomes this:
#include "Engine/Core/EventMessaging/EventMessagingSystem.h"
void Jump()
{
velocity.z = 50.0f;
EMS.SendEvent(EMS::BroadcastToAll, MotionEvent(GetOwner(), MotionEvent::Jump));
}
Now, all the other components that care about this fact will get an event with a MotionEvent class attached to it:
### Logger.cpp ###
Logger()
{
CONNECT_EVENT(GetSpace(), MotionEvent, OnMotionEvent);
}
void OnMotionEvent(MotionEvent& e)
{
LOG.Event(e.Owner().GetName() + " performed " + MotionEventToString(e.EventType));
}
If they want to add conditional logic to not log, more power to them. If they want to stop listening to jump events, they can just not register!
We also don’t have to have a suite of includes for all over the code for various tools/systems! You may learn someday just how bad the compilation system is for C++. Anything you can do to reduce the number of imports in your code the better!
The core idea of an event system is simple:
- Entities connect to an event type by leaving it their ID and a function to call.
- Events are sent from code of a certain event type and anything connected to that event has their registered function called.
- When entities are destroyed, they disconnect from that event.
There’s a million ways to implement this, but here’s a couple tips:
Do not use pointers to entities for registration
Let’s say a newbie to the team creates a component but forgets to disconnect from an event. If you use pointers to entities, it will crash when it tries to dereference a freed entity and call its function. What is the EMS going to tell you? Nothing. Someone, somewhere didn’t disconnect from something. Thanks.
If you use IDs, it will just tell you it didn’t find an ID and you can try to get information about it without crashing.
Use Macros
Yeah, yeah. They’re gross. But so is registering for an EMS. Make it simple for your users.
Have an Initialize Event for everything
If you initialize values with dependencies in the constructor, there’s no guarantee that those dependencies (ie, other components) have been created yet. Do yourself a favor and do initialization after all the components have been created.
Resource Manager
Your game is going to need to create and load a lot of resources from files. The more often you can do this, the better. One way to load those resources would be to simply call new and pass around a pointer to the information you need. This is gross and really hard to work with. There’s a few problems:
- If two entities need the resource, it either needs to be shared or it will double load the same resource twice.
- If you need to reload a resource, you need to reload everyone’s version of that resource.
- Deletion must be done exactly once on every resource and having so many possible owners can make this challenging.
A basic resource manager is a map from a string to some other resource type. On the user’s end, it should look something like this:
GetResource<Font>("Comic Sans.ttf");
If the resource has already been loaded, we just return the pointer to it. If the resource has not been loaded, we load it and return it.
We can also unload a resource when we’re done with it:
UnloadResource<Font>("Comic Sans.ttf");
This leads us to a class that looks something like this:
Fair warning to those who have made a system like this before: This will not cover anything advanced. Afterwards, I’ll mention some other techniques to improve on this, but this is designed to be a simple version to get people going, rather than a deep dive.
class Resource {
public:
explicit Resource(const std::string& name) : name_(name) {}
virtual ~Resource() {}
void Load() { LoadInternal(); }
void Unload() { UnloadInternal(); }
virtual const std::string& GetName() { return name_; }
virtual const std::string& GetPath() = 0;
protected:
virtual void LoadInternal() = 0;
virtual void UnloadInternal() = 0;
private:
std::string name_;
};
Explicit is really important here and nearly everywhere you use a single parameter constructor. Without it, your program may try to implicitly convert a string into any resource type. Get in the habit of putting explicit on any single parameter constructor!
Note that we have a public function for Load/Unload and a protected version called LoadInternal/UnloadInternal that are designed to be overridden. In this example, the public versions simply forward to the internal versions. This is so if in the future you would like to add more logic on Load/Unload, you can do it without having to rewrite in many different places! Remember that users of the resource system will be calling Load/Unload directly. Without this pass through, there’s no option to insert that extra logic. What if you want to log on Unload to ensure something is being deleted? What if you want to send an event when something is unloaded? What if you want to ensure something doesn’t call Load twice? What if you want to unload when someone calls Load twice?
This will serve as the base class for anything we want to make into a resource. Let’s make our first resource:
class Number : public Resource {
public:
explicit Number(const std::string& name)
: Resource(name) { std::cout << "Number() (" + name + ")" << std::endl; }
virtual ~Number() { std::cout << "~Number() (" + GetName() + ")" << std::endl; }
void LoadInternal() override {
// This part would actually be reading from file, not just assignment
value_ = 5;
}
void UnloadInternal() override {
// This would be deleting internal memory, unregistering, GetResource
value_ = 0;
}
const std::string& GetPath() override {
static std::string path = "Resources/Number/";
return path;
}
int GetNum() { return value_; }
private:
int value_;
};
The load function is stubbed out and simply assigns the integer 5 to value_. We also define the function GetPath to be where we actually store the file we’d be loading. This allows us to easily change the place to get the resource if it is renamed. It also means that we don’t need to specify the file path when defining the name of the resource.
The file type should be in the name of the resource. Picture.png, Picture.jpeg and Picture.DDS are all valid file types for a texture class!
Now, we need something to load it:
class ResourceManager {
public:
~ResourceManager() {
std::cout << "~ResourceManager()" << std::endl;
for (auto&& pair : resources_) {
pair.second->Unload();
delete pair.second;
}
resources_.clear();
}
template<typename T>
inline T* GetResource(const std::string& filename) {
auto iter = resources_.find(filename);
// If it already exists, return it.
if (iter != resources_.end()) {
return dynamic_cast<T*>(iter->second);
}
// Otherwise, load it.
Resource* toLoad = new T(filename.c_str());
resources_[filename.c_str()] = toLoad;
toLoad->Load();
return dynamic_cast<T*>(toLoad);
}
template<typename T>
inline void UnloadResource(const std::string& filename) {
auto iter = resources_.find(filename);
// If it already exists, return it.
if (iter != resources_.end()) {
iter->second->Unload();
delete iter->second;
resources_.erase(iter);
}
}
private:
std::unordered_map<std::string, Resource*> resources_;
};
GetResource is simple. If it exists, give it to me. Otherwise, create it and give it to me.
Unload checks if it exists, unloads it, deletes it and removes it from the map. All three steps are important!
It’s generally okay if two entities attempt to delete the same resource. You may want a log/exception thrown if this happens in a debug build, but there really isn’t a major problem if this happens in a release build.
The tricky part of a resource system is who “owns” the resources. If one of your other systems tries deleting their own resources through their own mechanisms, it can cause double deletes (they destruct and delete, then the resource manager does it as well). There should be one singular owner for any resource. Make sure anyone who uses the system understands that.
There’s a few ways we can improve upon this. Instead of returning a pointer, return a handle. This allows you to update the resource during run time! There’s a few handy functions for this:
std::filesystem::exists(filepath) std::filesystem::last_write_time(filepath)
You can ensure the file exists and see when it was last written to. If that value is updated since the last time you checked, call Unload() followed by Load()! This is where the Resource Manager needs to turn into Resource System. Make this the last system to be updated and have the resource manager perform that check! Now you can hot load assets! Hot loading is when you reload any resource without closing the program.
Here’s a few examples of assets that are really useful to hot load:
- Textures
- Audio
- Player controller values
- Weapon values
- Enemy stats
- Store costs
- Text (especially your credits!)
It can also be helpful to use handles rather than directly using file names like the example does. Rather than hard coding in a texture as “Textures_Nature_Bolder_05.png”, ask for “Level1_LargeRock_01”. You can then have a file that you read in that converts that key into a file name and then loads that resource. This allows you to data drive your resources completely!
Assuming you have these three implemented in your game, let’s talk easy tools.