Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
Third and final Blood Runs Cold tech postmortem series. Source code included this time! This time, I talk about the lightweight messaging system we developed that helped us keep decoupling and still iterate quickly.
This is the third and final post on the Blood Runs Cold tech postmortem series. You can read about UI on part 1 and about the PlayerProfile and our data-driven ItemDatabase on part 2. All of these were originally published on my personal blog.
On the first article, I took a few joking jabs at StrangeIOC. This is partially because I had bad experiences with it back in Legends of Honor and partially because I do think it’s fundamentally broken on its purpose. To seasoned Flash developers, it’s a good way to “terraform” Unity to behave more like RobotLegs. To all Unity developers I spoke to back then, it feels more like… Zerg creep. A lot of boilerplate code, callstacks that scroll for ages, having to bunny hop through 3, 4, 5 classes to find where something happens midst all the subdivisions in the codebase… Speed of iteration is Unity’s strongest point, and using Strange felt like I was losing it.
"Let's put Robotlegs into Unity!"
This is for sure not a criticism to IOC as a concept itself, it’s just to the way it’s implemented in that specific library versus how Unity works; it feels very “perpendicular” in a way. Most tech people (myself included) fall into the trap of “oh, but look at all the awesome stuff we COULD do if we used this!”, and end up making the core of a codebase an external dependency and never really using any of the “could haves”, but still having to live with all the rest.
That said, if you look at most individual features in there, they are conceptually pretty awesome. And by far, the best one is Signals. Signals in Strange are your way to pass messages and trigger Commands. However, they do come with all the rest in case you want to use them.
That’s the long story short: I wanted something like Strange's Signals, minus the clutter. So that’s what I set out to do.
The whole thing started out using this CSharpMessenger out of the Unity Wiki, originally made by Magnus Wolffelt. Special thanks to Max Knoblich for code review and putting the elbow grease into making our Signals layer and Messenger into a single thing after we postponed it for so long. Also thanks to Ashwin Sudhir for the idea of adding anonymous function asserts. And obviously to WilzZz for StrangeIOC – even though it and I don’t have chemistry, it’s a very well thought-of lib.
I like decoupling because it actually allows you to do MORE nasty stuff. It’s ok to do nasty stuff, as long as you can easily clean it up, and a good codebase is one where you can easily delete chunks of, or trigger a full rewrite of a part without affecting anyone else. As game programmers, we have to iterate quickly and still deliver quality, so there’s this balance you have to be keeping all the time. Decoupling allows you to focus on intent and parallelize things.
One of the most comfortable ways of doing that is making the different modules in game code communicate through messages: one module simply requests or warns of something, and one or more other modules will react to that.
A quick example: if you’re programming a shooter, you could make the scoring code when you kill a monster directly on the weapon code (raycast, if hit, kill monster, add score to some score manager, play audio etc)… That’s a valid approach, but what if you change audio APIs? Or if your score can’t be kept on some Singleton anymore?
The alternative would be your monster screaming “ARGH!” by itself upon death, and then the relevant systems would listen to that: the audio system would play the death sound, the scoring system would add a few extra points etc. This way, if you ever removed any of the systems that act upon the monster’s swan song, none of the other parts of the code would need to be touched.
Originally, I started checking if I could use Strange’s Signals in a more standalone way, but it depended on the whole Context and Injection flows, so I quickly realized it would be more work than rolling out our own. Production was starting, and we needed a messaging system as proof of concept, so I did the sensible thing and… just picked something up from the Unity Wiki and modified it to our needs. It wasn't bad per se, but there was one thing that really killed me: it was string based, so just like Unity’s native SendMessage, it was completely type unsafe – we were from the get go on a countdown for it to break, but it allowed us to work in parallel. Even though we were using consts instead of raw strings, still very shortly it faltered: people were accidentally passing the wrong parameters, so code silently wouldn't trigger and bugs were hard to track down.
I wanted to improve it fully from the get go, but as early planning took a lot of my time in meeting rooms, we had to have a quicker solution. The plan was writing a type-safe layer on top of the CSharpMessenger, testing it out, setting the API in stone and then unifying both things whenever possible.
Using a temporary messaging system was positive, as everybody could work in parallel from day 0 and get used/test the architectural approach we were building. However, everything has its price, and after I did the first pass on Signals, I had one fun evening after everyone was gone to refactor every call from Messenger to our Signals. The full revision to unify the Messenger code together with the Signals layer ended up being made by Max months later, but at least the API was exactly the same, so no additional refactoring was needed.
Internally, Signals are just delegates with one, two or three parameters. The very core of the whole system is simply a static Dictionary<Type, ISignal>. Yes, that’s all.
To create a Signal, you simply extend from one of the base classes and define the parameter types (up to 3):
public class EndGameSignal : ASignal {} public class ScoreSignal : ASignal<string, int> {}
One big advantage for having individual classes for individual signals is that you can search for all references of a given Signal, and always find out where it’s dispatched or being listened to.
Using the system involves fetching the signals and adding or removing listeners to those callbacks, or dispatching them. You access the Signals by calling the Get<T>() method, passing in the type. This is what it looks like internally:
public static SType Get<SType>() where SType : ISignal, new() { Type signalType = typeof(SType); ISignal signal; if (signals.TryGetValue (signalType, out signal)) { return (SType)signal; } return (SType)Bind(signalType); }
We try to fetch the given signal of a type and return it if found, but if it isn’t, the system does the binding of the signal for you. I wanted to be able to run the hooking up or dispatching code at any point, with no binding setup required, so it all happens internally. The binding itself is creating an instance of the Signal via Activator. Since we verify if the binding exists via the Dictionary, we only have that called once per execution per Signal, so there’s no performance hit.
One thing we enforce however is not using anonymous functions. First because as with every normal event you need to sanitize your listeners and remove them when they’re no longer necessary, second because it’s way more readable to have a declared function in the class for treating them anyway.
The thing that took the most getting used to was the syntax: it felt pretty weird in the beginning, but it was the best sweet spot I could achieve at the time; in the end I got used to it pretty quickly. The real advantage of our system is that addressing by type and the and syntax ensure that you can’t pass wrong parameters, and it even enables auto-complete suggestions:
Signals.Get<EndGameSignal>().Dispatch(playerName, score);
In theory, you can fire and hook up signals at any part of your code. However, the rule of thumb we used was: is this thing supposed to be broadcasted to EVERYTHING in the game? If so, use Signals. If not, use regular events or direct method calls. One thing I saw a lot on StrangeIOC codebases is using signals for everything, even tightly coupled module parts (ie: parts that don’t really make sense to exist one without the other), and that was just unnecessary overhead.
Before the cleanup, the system was a layer that bound the Signal to a string because the underlying Messenger class used strings. In the end we kept the string hashes and string binding methods because it allowed us to do some data-driven binding via class names and fancy editors: in our case specifically, we had a dropdown selection for parameterless Signals that could be bound to Playmaker events.
When I posted this on reddit, fellow redditor massivebacon asked why not just plain Actions<> on a static class. Truth is, that's a perfectly valid solution as well, and pretty much that's what the lib is internally. Most features like type safety and auto-complete-friendliness come out of the box, and you could organize the actions into sub-classes (instead of classes into namespaces, like in our case). The biggest advantage ended up being the data-binding with Playmaker, and also the "mental mapping" that if something was a Signal, it was being broadcasted, so you would need a bit more cautious (as opposed to when we used plain Events in tightly-coupled code). Performance-wise, it's just an extra Dictionary lookup, which is not really expensive, and the Reflection steps only happen once each. We were also setting groundwork so that, if in the future we needed extra features, like Signal-Command binding, a system and common grammar were already in place. Not to mention skipping that one extra nullcheck all the time warms my heart a bit as well.
At the end of the day, throughout production we had no issues and the 2 or 3 extra advantages made a difference in our workflow for the better - which doesn't mean it's empirically better than using plain Actions, just that for our project, specifically, it was.
Signals was another cornerstone of our architecture and I think that it worked out well in the end. Unlike the PlayerProfile and ItemDatabase (which are relatively specific to a subset of games) and the UI system (which is better for projects already in production), Signals is something that I use even when prototyping. Having the code not even compiling if you made a mistake on the parameters for a cross-module call is super handy when you have the attention span of a goldfish like me, not to mention the ease of simply having features enabled or disabled without anything (or anyone) else depending on them.
We did some pretty cool other tech, from the tiny productivity improvements like a generic reorderable list property drawer that we used pretty much everywhere, or all the cool custom Playmaker stuff we made for the designers that allowed them to set up the whole narrative and story playthroughs without us ever having to integrate anything for them. That’s not even considering all the wizardry that I had zero to do with, like the custom postprocessor stack and mobile-friendly PBR shader that Ashwin, our graphics programmer, cooked up, or all the pipeline tools made by our tech artist, Malte Dreschert, that tamed the impossible 2.5D pipeline we had.
As a tech team, the example we set was that the more you enable your artists and designers, the more you can spend doing coding – I guess that’s how we managed to be the first team to ship a mobile game with the whole company tech stack (including the early experimental modules) in a relatively short time.
This concludes my post series on the tech we used in BRC. Hopefully, it can bring some insight and spark some ideas to your projects. The good news is that unlike the other systems I talked about, I rewrote Signals from scratch at home, which means I can share it.
The bad news is: I couldn't secretly fit the terms “The Analog Kid”, “Digital Man” and “New World Man” in this post – but all the other song titles from Rush’s Signals are in the text somewhere though (don’t judge, I’m on vacation and I’m a big nerd in more than one front).
Signals is open source. Head over to my GitHub to download it if you’re interested (it’s a single file) and feel free to hit me up on the comments section or twitter if you need any help.
Read more about:
Featured BlogsYou May Also Like