Key contributions
- Project setup
- Base engine
- Core systems
- Service Manager
- Level Loading
- Event System
- Audio via FMOD
- User interfaces via Rive
- Integrating Gameplay state to UI
Overview
Spite is an action RPG built in our very own custom C++ game engine. I built critical systems with efficiency and structure at their core.
- The level loading system moves loading responsibility from the system itself to the component that’s being loaded, which lets the creator of that particular component decide how it’s loaded, as well as minimises game dependencies in our engine project.
- Our event system was built to be as ergonomic and efficient as possible through the use of templates.
- A simple FMOD wrapper was created to streamline the audio pipeline’s syntax (and further expanded in the next project).
- UI was implemented via Rive, a great third-party tool for creating user interfaces, although it lacked documentation.
Project setup and base engine
I’ve always been the go-to guy for project setup, so like every other project I’ve been involved in, I was tasked with ensuring that everything was ready for the rest of our group to use. This was the fifth time for me, so it was basically muscle memory at this point.
This was the project where we had to choose one of our graphics engines as base. We all agreed to use my engine, and me being familiar with my own engine made it way quicker for me to set up the project, since I knew exactly where everything was!
I threw together a basic component system with similar functionality to the one found in Unity. We all agreed that it was the most ergonomic way of working with components. We joked about using Entity Component System (ECS) again, but decided against it. Read more about the use of ECS in my previous group project.
With all that, we had a basic game engine ready for development, and my work on mission-critical systems could finally begin!
Core Systems: Service Manager
During early development of the engine, we needed a way to manage global services. Having singletons everywhere wasn’t optimal, as that would quickly lead to a confusing game of hide and seek. I built a simple Service Manager which allows us to register and retrieve services easily.
// Register service
RN::Services::Register<RN::Audio>();
// Retrieve service
RN::Audio* audioService = RN::Services::Get<RN::Audio>();Core Systems: Level Loading
This was an area I really wanted to work on. In previous projects, our level loading systems were always massive blobs with over a thousand lines of code. Simply unacceptable! So for this project, I wanted to take matters in my own hands and ensure that our level loading system was both efficient and easy to use.
I started thinking about what I truly wanted to achieve. How would this system be used, what would make it different from previous systems? After discussing with the rest of the team, I decided to make something modular. This system moves responsibility off the level loading system onto the actual component being loaded, keeping the level loading system simple.
Core Systems: Event System
Another area I really wanted to work on was the events system. This is something which was already covered in a course, but I felt it had lots of room for improvement. For example, the variant we were taught is built with inheritance, which means each listener must inherit from a class. This is something I really wanted to avoid because it didn’t allow for fast iteration and felt a bit clunky to use.
So, how would I create an event system? It absolutely must be ergonomic to use. My priority isn’t to “just get it working fast”, I firmly believe good things take time, and would rather spend an extra 30 minutes planning something out, testing different approaches and polishing, than spend three extra hours down the line when the time is ticking.
Unlike what we were taught in the assignment, I chose to not send unknown types like void*.
It opens up unsafe operations and adds further complexity when casting the data.
The solution I came up with is an event bus based on templates,
which makes it easy to define new events and its required data.
This solution also has proper isolation of events, which means simple events won’t have to trigger a lookup through every single listener.
c++
// Define an event struct
struct PlayerPrimary : RN::rEvent
{
unsigned ComboCounted;
};
// Subscribe with lambdas
RN::Events<PlayerPrimary>::Subscribe([](const PlayerPrimary& anEvent) { ... });
// ...or function references
RN::Events<PlayerPrimary>::Subscribe({ this, &HandlePlayerPrimaryAttack });
// Somewhere in our code...
RN::Events<PlayerPrimary>::Send({ 0 });This way, each event type has its own list of listeners, making it way more efficient.
Core Systems: Audio via FMOD
I happen to be very familiar with music and sound work, and the other programmers are not very interested in this area,
so this task was a no-brainer for me to grab as well. After our previous project,
I felt like the SoundEngine we were provided wasn’t cutting it.
It would randomly refuse to read sound events while also returning S_OK, the status code for “no issues, all is good!”
It also returned that same status code when attempting to play those “corrupted” events. It was ridiculous,
and I had spent several hours chasing a nonexistent mouse in our codebase. Call it Schrodingers Mouse, if you will.
Another reason why I wanted to build an interface for audio is to familiarize myself more with the FMOD API. I based our audio engine on SoundEngine in order to have working audio early on. Unfortunately, I had to review my priorities and the audio system ended up simpler than I first envisioned, but it still covered all our needs and lived up to the goal of being more stable than SoundEngine.
I did implement a couple of awesome features, though, such as the ability to register and bind FMOD events to our event system in one line, which was great for productivity:
RN::Audio::Register<GameEvent::PlayerDefeat>("event:/player/defeat");
RN::Audio::Register<GameEvent::PlayerPrimary>("event:/player/primaryAttack");
RN::Audio::Register<GameEvent::PlayerSecondary>("event:/player/secondaryAttack");Toward the end of the project, I considered quickly implementing spatial audio, but decided it would be more fitting to do it properly in our next project.
Core Systems: User interfaces via Rive
This was something I was looking forward to for a while. For our game engine, I really didn’t want to waste time calculating and aligning elements with code. I first considered finding a framework which could parse and display HTML/CSS (with animations and transitions), but there turned out to be nothing which would fulfil our needs.
That is, until I remembered this one little tool I read about a couple of years ago, called Rive. It comes with a visual editor and a built in animation suite. This tool would allow the entire team to work on our interfaces collaboratively. I thought, since it has so many runtimes available, that it would be similar to implementing something like ImGui, or really any other library.
Boy was I wrong. The first red flag was that there was absolutely no documentation for implementing the C++ runtime. But having Rive would mean that us programmers wouldn’t have to spend much time making interfaces (let alone high quality ones), so I pushed forward. Over the next two weeks I would implement Rive bit by bit. We had to work with basic types for data binding up until about two months in, since we struggled to find a way to data bind DirectX textures to Rive image assets.
At that point, we were the only group to have basic health & resource orbs. The week before our deadline, we finally cracked the code and we finally had a complete interface:

This was a massive win for us, and I’m really proud of what we accomplished with so little documentation. You can read more about implementing Rive in this article.
Conclusion
It’s been very relieving to be able to just focus on making great systems as well as addressing issues I’ve had in past projects.
When it comes to Rive, I wouldn’t say I regret implementing it. It has definitely helped us ship user interfaces with exceptional quality, but it took more time to integrate than expected. And the learning curve is definitely there; While the main purpose was to make it easier for other disciplines (primarily art and animation), I found that it was pretty much only I who sat in the editor setting up state machines, layouts, and data binds.
There was also a short period of uncertainty after Rive had suddenly announced that they were disabling exports for free accounts, while also quietly opening up student license applications. Luckily, our license application was approved, but I had started developing an alternative to Rive in my free time, just in case.

I would’ve loved to have worked more on this alternative interface editor, but Rive was already in place and I didn’t want to spend too much time on essentially the same thing.
Overall, this project has been great. Having this many skilled people on the team has been very reassuring. I could always ask them for sincere feedback on what I was working on, or get help whenever I felt stuck.
