The first semester of capstone at Champlain College requires taking on tech debt. Teams compete under very tight deadlines for a chance at the second semester of development. The teams are evaluated on the product they bring to the judges, which encourages doing whatever it takes to get something that looks good, even if it is running on a giant steaming pile of spaghetti code.
In the second semester of development, teams that make it through, have more time to turn their prototype into a fully functional game. This often requires ripping out the precarious scaffolding and replacing it with a more solid foundation that can bear the weight of a full-length game. Sometimes, this process feels like you are moving backward, but it is essential to avoiding major problems as the game grows. Making it through greenlight requires going into technical dept, and the time has come to pay it back with interest.
As the UI/UX programmer, my first task was to overhaul the user interface. In my initial test runs, I couldn’t figure out if the unlabeled numbers on the screen represented health or enemies killed. The unlabeled slider bar could have been a flashlight indicator or it could have been ammo. I am no graphic designer, but I did my best to build a UI that indicates to the player what is what. It worked, the vast majority of QA testers say they intuitively understand the new UI.
Dry Rot!
However, while modifying the UI, I found the programmatic equivalent of dry rot in the walls: coupling and singletons! Everything was coupled to the UI system, including things that shouldn’t be. That meant if I changed a variable type in a UI script, the entire win state of the game would break.
Single Responsibility Principle
Let’s take the gun UI as an example. Currently the “Gun Master Script” handles both shooting and UI updates. This is not ideal. The Single Responsibility Principle (SRP) states that each object should only be responsible for a single functionality. It’s a bad idea to have the UI and the shooting functionality mixed since it means that I, a UI programmer, am touching the same code as the gameplay and AI programmers. Do you really want your UI programmer messing up your shooting code? That introduces the potential for more issues and merge conflicts. It also means that if the gameplay programmer David decides to replace the shooting script with a new version, all the UI code must be rewritten as well.
Communication Between Classes
Having a gameplay class update its UI introduces many problems, so let’s separate the UI code into its class. Now the single responsibility principle is observed. However, the UI class still needs to get information about how many bullets have been shot to update the ammo counter. How can it get this information?
Direct References?
One option is to make the current ammo count public and give the UI class a reference to the gun ammo script or vice versa. The problem with this approach is it tightly couples the UI and the gun class. If there is an issue with one, the other will break. It also allows a silly UI programmer (me) to modify the bullet count in unexpected ways, potentially resulting in unlimited or negative ammo. Object-oriented programmers tend to be suspicious bastards by nature. No outsiders should see, or god forbid modify anything they don’t absolutely need to.
Creating reference between the two classes requires finding the reference and this is likely to break and start spraying null reference errors everywhere. You could serialize the reference and pass it in from the inspector, like classic dependency injection, but you will soon get tired of passing in 50 objects every time you make a new scene. I sure am! Another approach is to search for the object that hosts the gun script by name and use GetComponent to find what you are looking for. As long as you do this in the start function and cache the results, the performance implications aren’t horrific. However, as soon as someone decides to rename the player to “Morgan” instead of “player” everything will break, and debugging it will be a nightmare. Creating references between coupled classes in Unity is just a high-class headache.
Singletons everywhere!
Another option for communication between classes, commonly used in Unity, is the Singleton pattern. This simple design pattern uses a static variable to create a single reference point to the class. The static variable is initialized to a non-static instance of the class, and any other instance is destroyed. This design pattern is the first one learned by every new Unity programmer. It makes everything so simple! Instead of worrying about dependencies, you can simply do GameManager.instance and everything works. Unfortunately, like many things in life, singletons are too good to be true.
Singletons have a lot of the same problems as directly referencing other classes. They may seem decoupled, but they actually tightly tie unrelated code together. If the singleton is changed or deleted, many things will break, making debugging a nightmare. A singleton is a global variable and has all the same problems as any other global. Anyone can modify it at any time meaning tracking down issues is harder. If you want to know more about why singletons are a terrible idea check out this chapter in the Game Programming Design Patterns book.
Even though most programmers know singletons are a bad idea, they use them anyway. They are just so simple and convenient! They are the way we learned to do things freshman year and relearning a different way is hard. Developing in Unity can result in bad habits. It feels so much easier and lighter than more traditional programming that it can lead devs to let their guard down and ignore best practices. Ultimately, pivoting away from singletons isn’t easy, but it will be worth it. You will find that following object-oriented best practices will result in a game that is easier to modify and debug.
Use an Event System to Decouple
The solution: an event system. You can read more about this pattern in this excellent chapter of the Game Programming Design Patterns book. My modified version allows any class to send an event. It doesn’t care who receives it. It just sends that event out into the wind. A listener can subscribe to any event it cares about and respond however it wants. Neither class knows or cares about the other. And, as long as the event is still sent, the entire gun class could be replaced without breaking any UI code.