CAB .1: Infrastructure
Code excerpts for CAB can be found here.
Hi,
It’s been a while since I’ve written one of these and to be honest, things have changed a lot. Don’t worry, I’m going to keep things focused on what I’ve been working on, but TLDR; I’m considerably better at what I do now.
Let’s talk projects - I haven’t had the chance for long time to really sink my teeth into a project, sure, a couple of ideas here and there, a game jam last summer, a mechanic I was overly curious about, but nothing that really came together and functions in one cohesive ecosystem.
That was until CAB (CardAutoBattler), a foundational sandbox I’ve made myself to pursue more complex game ideas. I say pursue here as I’m not quite sure what I wanna get into yet, I had an idea for a multiplayer card auto battler, but I’m not sure if that’s the direction I want to takes things yet.
Either way, we’re here to talk about progress, so lets get into it.
Single Entry Point
Why Is it important and what we’ve got so far?
CAB’s infrastructure was created with scope in mind. For instance, we know that we want the game to be multiplayer and localisation should be supported, so lets get those setup from the get-go. We also know that we’re gonna need certain functionalities, so lets get systems in place to handle such tasks, like storing data persistently on disk, or handling platform dependent logic.
Having the foresight to start building on these systems from the beginning is great, but we need a way of managing their initialisation and usage, as well as (arguably more importantly) their lifetimes within the project and insuring that any persistency we have stays unique to avoid memory leaks.
Introducing the Singleton pattern, not anything special, we’ve all seen this before, and for the record I’m not the biggest fan, I find that their simplicity is often overused, leading to runtime memory bloat. I will however, admit they do have a time and a place within a games infrastructure with core functionalities like networking and localisation being obvious candidates.
So that takes use back to our original task, and the title of this section - we have a bunch of singletons that require custom initialisation processes, some directly relying on the loading of others before they can operate safely. Let’s take a look at a diagram showcasing CAB’s current Single Entry Point infrastructure and how it’s used to initialise our core systems.
A simple diagram showing the basic flow on project load.
It’s worth mentioning that we’re using a Coroutine for this loading process, which is not entirely asynchronous but most of this functionality is engine dependent and must be done on the main thread regardless. Also, if there was a time to get the heavy lifting out of the way, the start-up screen is probably the best place to do it.
I previously said that some of these systems are dependent on others, an example of this would be the localisation manager, which pulls the currently saved locale from the save system, which would have needed to be initialised before any data is deemed valid.
Because of this, the ordering in which we load singletons is very important; the Save System MUST be loaded before the Localisation Manager, and therefore we must index these singletons with this in mind.
The singleton collection with a consideration for indexing. (Most systems currently have no dependencies)
It’s probably worth noting that most singletons currently have no dependencies on other systems and that their indexing here is only based on order of implementation, however, if this does change moving forwards, they can always be moved to accommodate accordingly.
You might also notice that the Scene Manager is being initialised last - currently this provides an underlying benefit of transitioning into the title scene automatically without the need for an external call, whilst also assuring that we’re ready to do so due to it being the last to be initialised.
This might make more sense if we take a look under the hood of one of these systems.
The LocalisationManager’s Initialise function
If the Singleton is part of the start-up sequence, it’s required to implement this method and the “initialised” flag must be set to true when all necessary functionality has been completed. I’ve chosen a flag to dictate completion here as some classes call processes that operate outside the bounds of the main thread, and thus a base flag is a more maintainable approach than custom implementations for each case.
What about external classes? How can we be sure systems are ready before initialising a given component?
I’m a big fan of enforcing class ownership in projects, whilst static and public variables have their place, I would say that in practice, the vast majority of data should be modified internally, and only accessed externally with read-only restrictions.
I mention this because we have cases in the project where we want to run logic when objects are enabled, but there’s no guarantee that our systems are ready when this logic fires. To resolve this, the Singleton Manager contains a "NotifyOnInitialised” function that can be called from anywhere, requiring an Action that is invoked whenever it’s safe to do so.
Might sound convoluted in writing, so let’s take a look at a use-case, and how it works in practice.
A use-case of NotifyOnInitialised, being used by the UIPerformanceStats class
The PerformanceStats class requires a players’ accessibility options from the save system to know what to display. It’s also part of our persistent hierarchy, so OnEnable is called during the same frames that the singletons would be being initialised, this would have been been a major problem without our notification system.
We’re actively trying to promote a philosophy with our infrastructure here, whilst the project is small enough to justify holding references on master classes to a given collection of class instances, it’s important to consider scale from the very beginning, and logically, a master class such as the Singleton Manager really has no business knowing what a UIPerformanceStats class is or what it’s responsible for.
So whilst our current outlook might seem like overkill for what we have so far, in practice, iteration over our systems has been a breeze; if we need a new singleton, lets add it, if we need access to the data from another system, we rearrange the ordering of initialisation.
All from the benefit of Single Entry Point design - we can mitigate race conditions and simplify project management, paving the way for further development without further complexity. Easy peasy!
COmmand & Con(Sole)Quer
How a DebugConsole has helped cut corners in our development times…
If I write anymore about singletons I might have to consider a new profession, so with that being said, lets get back to talking infrastructure.
I think the biggest challenge we’ve faced with personal projects previously has been the vast requirements needed to test singular features. You need UI, some form of bread crumbing and input management for this UI, you need scene management, input systems, blah blah blah like seriously? I just wanted to build a Networking solution! So I lost my mind trying to do this, not only does building our all the required systems slow initial testing / development to a snails pace, but it’s also terrible for later development as testing specific game states will require genuine playthroughs to reach a given point - what a time sink!
And yeah of course you need to test your games as your players will, not arguing against that, but we’re trying to build out a lobby system here, why do I need to build a socials menu to achieve this?
Introducing our DebugConsole, the current fan favourite among our testers / developers and it’s not without good reason. Joining lobbies, leaving lobbies, resetting save configs, setting localisation locales, all of this and more from a globally accessible UI component.
The debug console
Okay so why is this such a big deal? Well again, it’s saved us a ton of time, testing our lobby networking would have genuinely been impossible without this functionality, especially across different machines - with console commands however, we can copy lobby codes, send them to testers, and have them join specific environments, all from the same UI component.
Additionally, to keep all testers / developers on the same page, users can log issues / update command statuses via a confluence page that is regularly updated with the latest command implementations.
Our current collection of debug commands, each complete with an implementation status, description and any additional parameters required.
Although unnecessary, we’ve learnt from our time working on other projects that sometimes what can be intuitive for an engineer won’t necessarily be the case for those in other disciplines. With this in mind, we’ve spent the time adding additional qualities to our console commands. Parameter hints have helped remind our testers of the structuring of a given command without the need of referencing the confluence page on what else is needed.
We’ve also added colouring indicators to guide developers whilst using the tool. If they’ve used an invalid command key, that key will turn red, indicating that it’s invalid, we also separate keys and arguments with different colours for ease of visibility.
Colouring & Argument indicators
In terms of infrastructure, this barely scratches the surface, but I think the biggest takeaway we’d like to emphasise is simply to plan ahead, what might seem redundant now might save you weeks down the line.
I’ll leave it at that for now, but expect more updates from me regarding CAB moving forwards. Maybe we shoudl talk about Networking, that sounds like fun…
Anyways, its back to work for me, thanks for reading!