With a programming write-up like this I think it’s really important to start with some caveats. First as someone reading this, you’re probably a programmer. My best advice for programmers is that you should try and gather as many perspectives and ideas about programming as you can.
Being informed from a lot of different places, whether that’s classes, blogs, textbooks, forums, respected developers, streamers, or whatever, will give you more reference points to consider when solving problems and making hard choices. Second is that you should always be critical of any opinion or dogma about programming before you internalize it.
Here are some questions I ask myself when learning something new:
- Does the person saying it have experience to back it up?
- Do I have enough domain knowledge to have an opinion on this?
- If you jump to rejecting an idea is it because of logic or a predisposition?
- Is this technique appropriate for my project or do I just think it’s cool?
The rest of this post will be a mix of design patterns I've found useful, implementation details from Sausage Sports Club, and a look at some successes and failures of the codebase I'll be mostly taking forward.
Game State Stack
At a high level, Sausage Sports Club is a state machine controlled through a stack of GameState objects. Each GameState implements an API of functions called when switching between states and during Unity’s internal update.
This API defines allows each GameState to optionally define these functions:
- OnEnter/Exit — called when added/removed from the stack
- OnPause/Unpause — called when state is added/removed above this state
- OnUpdate — called during Unity’s internal update
This structure is useful because it gives an explicit entry and exit point for every state of the game. This avoids a lot of potential bugs that can happen during cleanup when switching between modes and scenes. Pretty much all you need to do is ensure anything spawned or setup in OnEnter gets cleaned up or reset in OnExit. For Sausage Sports Club, this also allows me to share most of the code used to setup different sports matches. For example, almost all the flow of setting up a Soccer match is the same as setting up any other sports mode.
This seems to me like a common pattern, especially for games with a lot of discrete states and my implementation is heavily inspired by my work on Battle Chef Brigade where they use the same stack structure. My biggest innovation on this pattern is allowing the user to optionally pass in an object GameStateParams, which holds any data for use in the state, when calling ChangeState. Any GameState can define it’s own subclass to add extra data to pass along through this API.
The next level down in Sausage Sports Club’s architecture is probably Actors/Locomotion. Similar to the GameState state machine, each moving character is controlled by a state machine.
Actors use a similar API to GameState called PhysicsState which defines the same functions mentioned above as well as the following:
- OnInitialize —passes reference to the statemachine owning EntityPhysics
- OnCollisionEnter/Exit — passes in OnCollisionEnter/Exit events
- OnTriggerEnter/Exit — passes in OnTriggerEnter/Exit events
- OnFixedTick — called during Unity’s internal FixedUpdate
This structure is useful for the same reasons listed above. Easy cleanup, shared code between similar entities, and additionally it’s easy to prototype and add new types of locomotion or other states. This is also based on Battle Chef Brigade’s implementation and adds an optional PhysicsStateParam argument to ChangeState. The big difference from GameStates here is that PhysicsStates require some source of input to control the Actor’s locomotion, which I implement as a neighbor component derived from ActorInput. That component defines an API of accessors for all the different inputs used in the game, then different derived classes like PlayerManualInput or PlayerAIInput.
For player controlled input, I use InControl as a layer to get inputs out of hardware without having to directly interface with Unity’s not great built-in input system. I wholeheartedly recommend using InControl as it comes with support for pretty much all commonly used input devices (PS4, Xbox One, PS3, Xbox 360, Switch and many 3rd party random controllers). It also comes with source code, is well documented and easily extensible. Before Patrick had a chance to add official Switch support I was able to add my own Joy-Con support without much trouble. It’s also very straightforward to implement input rebinding, which I’m proud to say is a feature in Sausage Sports Club.
For AI controlled input, I’m really not doing anything fancy. The PlayerAIInput class defines some util routines for jumping, dashing, and finding a route to a target position using the navmesh (if available). Then for each mode I add a derived class like PlayerAISumo or PlayerAIOverworld that runs logic to determine a target position in every Update. The AI for each mode is always straightforward and I just go step by step through what a real player would think and where they would want to be based on the state of all the other players and actors. For example, in Soccer an AI just wants to get between the nearest ball and their goal and then kick the ball towards the other goal.
The character controller is the thing I worked on the most by far in Sausage Sports Club. It was the first thing I added and the thing I was tweaking even into the last cert submission.
Here are all the techniques I used to make the Sausage Sports Club character controller feel tight and responsive:
- I use Unity’s built-in physics to resolve collisions but calculate velocity and rotation each FixedUpdate by manually integrating acceleration, drag and gravity based on around a dozen tweakable values. Players are capsule colliders with a small sphere collider sticking out at the bottom to keep players from getting stuck on uneven surfaces.
- A useful trick I picked up from Battle Chef Brigade is tracking player-controlled and uncontrolled velocities separately and combining them when calculating velocity. This lets you separate code that decays hit forces over a few frames from the code that turns player input into movement.
- To check whether a player is grounded, I raycast down and then spherecast down as a backup in case we’re hanging off a ledge. If moving down, I also use OnCollisionEnter as an additional backup check. To avoid players tunneling through geometry when hit super hard, I shoot a ray in our intended move direction and reject any force that would push us into walls or the ground.
- I let the player jump for the first 0.3 seconds after they walk off a ledge and also buffer jump input made in the 0.2 seconds before a player lands on the ground to help match player’s expectations and weak reaction time. These two quality of life features are very important for any platformer and players will complain that jumping feels unresponsive without them.
- The dash/kick move instantly changes the players move direction to match your sticks input direction instead of using the direction you’re facing. This better matches player intent and feels way better despite it taking your player a few frames to turn and face the new move direction.
- After the first half second of a hit react I ramp up the weight of player input, and even allow dash and stomp presses, so players can start recovering from a hit react even while still flying through the air.
- I noticed players were accidentally jumping while dashing and stomping while dashing and turned those into advanced techniques that let you jump farther and do an angled stomp respectively. This increased the skill ceiling for advanced players a bit and made jumping, dashing, and stomping all feel a bit more loose and free.
- I keep all variables used to control player movement, jumping, hit forces, flopping and anything else that needs to be finely tuned on a ScriptableObject so changes can be made and saved at runtime. It’s really important to make iterating on these values fast and fun, so you do it as often as possible. I have so many tweakables here and change them so often that selecting this asset is mapped to Alt+T in my project.
One of the hooks of Sausage Sports Club is that characters have long floppy necks players can swing around with the right stick on a controller (or the gyro on a single Joy-con). This is useful during gameplay for finessing sports balls around and avoiding hits, but is mostly a tool for players to make each other laugh.
The character’s necks are a chain of 10 rigidbodies with fixed joints constraining each joint to it’s parent. The flopping works by applying a physics force to the rigidbody at the top of this joint chain. The feel of floppiness was based around a physics timestep of 0.166666ms or 60fps to match target framerate. I found the feel extremely early on and should have tried to match it with fewer joints, carefully tweaked Configurable Joints, and even a higher timestep.
In the end, I didn’t make those changes because I always thought I was very close to finishing and also because my understanding of PhysX was earned slowly through many prototypes, tweaks, and bug fixes.
I use Unity’s built-in UI systems and TextMeshPro for everything. They worked pretty well most of the time, but also both had weird issues that were pretty annoying and I had to workaround.
Here’s some of my major gripes with Unity UI and Text Mesh Pro:
- Unity’s UI system has a solution for resizing a panel to match the size of its contents, but lacks a straightforward (and effective) way of manually calling for the panel to resize, so there’s a few panels in the game that get into a slightly weird layout state occasionally.
- Unity’s built-in components for masking UI elements don’t work on world space UI at certain view angles, especially if you’re changing the material queue of different elements. Until I’d learned how to use Stencil shaders and took the time to understand the Stencil buffer’s role in Unity’s UI system I couldn’t figure out a workaround. My (non-ideal) solution was to add a material/shader that requires a specific value to all elements in the panel, then have mask rect that writes that required stencil value.
- Each UI panel is a separate object and has a component to control its state. None of the UI panels are aware of each other except rare cases when one panel opens another. When not in use, UI panels are set inActive. Any UI panels with dynamic content spawn and rebuild their layout OnEnable. This can cause hitches in especially complex menus and I could’ve offloaded spawning of submenu content until opened, but in each case I chose to keep things simple to avoid potential bugs.
- Unity’s UI performance was also a bottleneck that blocked Switch optimization and required me to convert all UI in the game to TextMeshPro. Specifically, any Text components using the auto size feature would force the associated font atlas to be rebuilt when first enabled if using a different size than the existing atlas.
- For the first 90% of development I avoided using tweening libraries and wrote coroutines to manage every moving part of every menu. This was because of several bad experiences with external libraries and less than great asset store assets. In retrospect, I should’ve have started using a library earlier because my coroutine-per-tween way of doing things was slower work and much more error prone. I think the codebase would be 30% smaller if I converted all my coroutine tweens, which means 30% less code to create bugs in.
- For the first 60% of the project all my UI looked pretty terrible. Through a lot of experimentation and iteration I found a few sets of colors and patterns that reinforced the games themes and then used those everywhere. That made my UI feel cohesive throughout the game, which was most of the battle. Adding frames, shadows, and subtle movement also went a long way in making these screens feel finished.
Anywhere there’s one instance of an object that needs to be accessed in many places I use a singleton, which is derived from my generic singleton base class. I have 15–20 of these probably, most used are PlayerManager, GameManager and UIManager.
This is a pretty common and well documented pattern so I don’t have much to say about, except that whenever entering play mode or opening the game I first load to an empty scene containing all these managers used to manage and run the game.
Anywhere more than one part of the game cares about a piece of state, I use the LensManager paradigm which I picked up working on Battle Chef Brigade. The naming isn’t super clear, I know, so let’s go through an example. Consider a screen fading system where multiple parts of the game want the screen to either be faded black or not.
With this paradigm, each system adds a LensHandle request that holds the intended fade setting to a LensManager. When receiving each request, the LensManager internally evaluates based on C# func passed in on initialization and then caches the determined state result. This lets anyone check the state without having to evaluate all the requests each time. The LensManager even has an evaluate callback other systems can subscribe to know whenever that state changes.
By the time I started making Sausage Sports Club the migration of indies to consoles had already started. It’s an obvious revenue stream, so I planned from the start to put the game on as many platforms as I can. With that in mind and having recently done some contract work getting games running on consoles, I knew it would be best to keep variable platform code as separate from game code as I could.
There’s a few ways to do that, but my version is having an abstract Platform base class that defines an API to use in game code and to have a derived class for each platform that implements those functions as needed. This fits into Sausage’s hierarchy with a PlatformServices Singleton that adds the current platform’s associated Platform derived component to its gameObject on Awake. Then anywhere in game code the platform API can be accessed through the Singleton instance’s CurrentPlatform member.
I should note that because Sausage Sports Club is local multiplayer and doesn’t have any online-connected features like leaderboards, so the only things in my version are Initialization, Save/Load, Cloud Saves, and Achievements. One thing to keep in mind when writing this sort of API is that these different features will be asyncronous or at least happen over multiple frames so it’s best to build Coroutines into your logic, so you can wait on operations if needed
Save data is mostly pretty straightforward in Sausage Sports Club. Save and Load are called on a Singleton manager PlatformServices and create a JSON object then manually runs over all managers that implement the ISaveable interface. That interface only requires implementation of Save and Load functions that take the JSON object as an argument and apply/read out data as each manager requires. At the start of loading we ask our platform to open the save file and give us the contents as a JSON object, and at the end of saving we write our filled JSON object to a new file and overwrite the existing save. The most annoying part of this is having to manually pack and unpack data we need in both the Save and Load functions. In the future I may write an attribute that automates that part. With that implementation I’d just add [Saveable] above a property to make it automatically serialize and then PlatformServices would search all Singletons for fields with that property.
The most weird part of the save system is how saves are represented and used in the game. Some data in the game (Character, Skin, and Arena unlocks) should obviously be persistent and only need to be unlocked once, while some other data (Hats, Name, and Input Rebinding) makes more sense attached to individual players. To support this, I store individual player data in a PlayerSave object and track those under a GlobalSave object and both implement the ISaveable interface I mentioned. The way players can create a new player save or swap between existing ones is by changing their name on the player select. If you’ve been playing for a while and there’s data associated with your current name when you create a new one, your progress is copied over to the new name.
This organization of data has some interesting wrinkles. It’s weird and handles complex edge cases, which makes it hard to communicate. My solution there is to just skip explaining it. If 2 players play together and unlock a bunch of hats, then resuming playing another day- this time signing in in opposite order, it’s possible they won’t notice their unlocked hats have swapped. If they do notice, they’ll probably just exchange controllers. This is a little weird, but awesome because I’ve avoided forcing new players to learn about save data, player names or any other knowledge not needed to get into the game and have fun.
A lot of folks say it’s important to not leave this until the end of your project and that it will be very rough if you do. I did the opposite of that and it went pretty OK despite there being well over 10,000 words in the game. Implementing and then fixing all the bugs that popped up probably took 2 weeks of full-time work with the occasional fix popping up down the line.
Here are some things that made my process of supporting multiple languages less miserable:
- All my dialogue was stored in a single data structure and almost all of it was stored in prefabs. This meant I could write an function to scrub prefabs recursively for any dialogue properties and give them a localization term.
- All dialogue in the game gets read from the same routine, so there was only one place to ensure dialogue was piped in correctly.
- Only a few places in the game have dynamic text where a name or phrase is substituted into a line, so my solution for substituting text didn’t need to be super robust.
- I used i2Localization which is AFAIK the premiere Unity-friendly localization tool. I could write a lot about things I like and don’t like about it, but suffice it to say it’s a complete no-brainer if you’re localizing a Unity game.
Here are things that made this process more miserable:
- There were A LOT of places in code where I hard coded lines into tutorial or one-off scripted sequences that were annoying to track down. I ended up having to search for all strings with regex and manually looking through them one by one.
- Many many menus have text assigned in code. I was able to squash a lot of those by writing a tool to swap in the LocalizeText function anywhere ‘.text = ‘ was used, but there were another 50 or so spots I had to fix things manually.
- Localization is expensive for text heavy games! I’ve heard quotes between $0.04 and $0.10 per unique word which can add up very quickly across multiple languages. Although the game functionally supports localization, I’m waiting to see how it does and how many translations I can afford before spending the money.
- Related: Do the research on which languages are most beneficial to translate. Some will suggest EFIGS (English, French, Italian, German, Spanish) as an obvious first choice, but there’s a higher ratio of Chinese and Russian players on Steam than any of those besides English.
Although it’s possible to throw in localization towards the end, I’d suggest being aware of how localization basics work as early as possible. The most important thing is that every unique string in your game will be associated with a localization term/ID that will be used to find the matching string for the currently active language. Just that bit of knowledge will stop you from adding lots of raw strings in code and representing strings in lots of different ways.
After 3 years building and living in this codebase solo, let’s run through the code related successes and failures.
First off, what are the successes?
- For most of this project I worked on other projects as a contractor. I’ve always made a point to ask lots of questions about how projects are structured and the motivation behind that architecture. Any time I worked on some project that handled a common problem well, I took that lesson and applied it to my game. Being willing to rewrite or restructure any part of the game as better ways come along has been great for the project. This philosophy also applies to the rest of making the game in that I’m always willing to cut things if they don’t actually make the project better.
- Something key in making this work is that I take maintaining code seriously. Who knows when I’ll need to search through the project’s history to find an old prototype or track down when a bug was introduced? With that in mind, I organize my project well, carefully pick variable names, add helpful comments when needed, and write lengthy commit messages. Additionally, I’m careful to avoid early optimizations since that tends to complicate and obfuscate code that might change and I try my best to only add external dependencies that are lightweight and include source code.
- The next biggest success in this project is that I took time to get proficient at writing editor tools. In Unity at least, writing tools requires learning a totally different API and facing another set of pitfalls from general gameplay programming. It took about a week of deliberate practice and then some time hitting all bumps to learn enough to be self sufficient here. Some things I wrote custom inspectors or tools for include placing hats on characters, previewing camera settings, game preferences, dialogue editing, and lots more. If you don’t have time set aside to learn this API you can get started by adding the ContextMenu attribute to functions, which will expose them in that component’s right click menu.
- Another big success is that I managed to keep platform-specific code out of game code. I went into detail about how I did that in the Multi-Platform section above, but it was valuable to build this API so adding new platforms just requires adding a new Platform class and implementing the abstract functions. There were also a few spots where I needed to hide a button or show an instruction label on only one platform and so just added an #ifdef, but those were exceptions to the rule.
And here’s some of the bad stuff:
- Through the first half of development, I didn’t give enough consideration to how prototypes would affect scope and add value before spending time on them. I spent weeks working on a visual node editor to help author quests before realizing it would cost way more time than it would save. I spent weeks prototyping additional game modes and powerups that I found exciting without considering how they’d fit into the game or how much value they’d add. I got better at this after the addition of Adventure Mode which I evaluated seriously and had a sobering impact on scope.
- Until very late in the project when I hired someone to do QA, I didn’t prioritize testing new features and added lots of bugs all the time. This meant my convention builds were often riddled with bugs and that getting the game through console certification was an arduous process. I only started changing my ways after staying up late making changes and introducing tons of obvious issues thereby wasting my contractor’s time.
- In general I relied on OOP and inheritance a bit too much and made actors, critters, players, and their physics states are a bit too intertwined. This introduced a lot of indirection and made navigating code confusing and often made what should have been obvious issues more difficult to debug. Especially in older parts of the codebase, I made systems and actors more intertwined and aware of each other than they need to be.
- Having GameStates and PhysicsStates be represented as MonoBehaviours on gameObjects that are always active and often using coroutines created a lot of hard to track down bugs whenever I didn’t clean things up fully.
- When optimizing, I switched players, vfx, actors, and match objectives to use object pooling and created tons of bugs that could have been avoided if I had spent time writing a system to ensure underlying state was reset.
Thanks for reading, I hope you found this valuable. If you’d like to see more content like this please support my work by checking out Sausage Sports Club and sharing it with a friend. It’s coming to Steam and Nintendo Switch very soon on July 19th and pre-orders are up on the US e-shop now!