What you will get from this page: Tips for how to keep your game code easy to change and debug by architecting it with Scriptable Objects.
Ready to see some of the most realistic graphics in Unity? Onerios are using Unity 2019+ with the HDRP to power their new ArchVizPro Interior pack for Unity. Let's look at software architecture solutions for slightly larger projects. If we use the example of the Ball game, once we start introducing more specific classes into the code–BallLogic, BallSimulation, etc–then we should be able to construct a hierarchy. Unity does not do a full domain reload when exiting play mode. View Unit 3 - Unity architecture.pptx from PHYSICS 135-A at Northwestern University. The architecture of Unity3D Game and editor The Unity editor. Has your code compiled into it. Has your. Unity uses the list of Scenes to determine the order that it loads the Scenes in. To adjust the order of the Scenes, drag them up or down the list. The Platform pane lists all platforms available in your Unity Editor. The list displays the Unity icon next to the name of. Unity Temple is a timeless work of art most worthy of the restoration that was so lovingly done to it. Sylvia Dunbeck, CAF Docent Class of 1987 After years of piecemeal repairs, today this national treasure stands beautifully refurbished, having just undergone a more than two-year, $25 million facelift.
These tips come from Ryan Hipple, principal engineer at Schell Games, who has advanced experience using Scriptable Objects to architect games. You can watch Ryan’s Unite talk on Scriptable Objects here; we also recommend you see Unity engineer Richard Fine’s session for a great introduction to Scriptable Objects. Thank you Ryan!
ScriptableObject is a serializable Unity class that allows you to store large quantities of shared data independent from script instances. Using ScriptableObjects makes it easier to manage changes and debugging. You can build in a level of flexible communication between the different systems in your game, so that it’s more manageable to change and adapt them throughout production, as well as reuse components.
Use modular design:
- Avoid creating systems that are directly dependent on each other. For example, an inventory system should be able to communicate with other systems in your game, but you don’t want to create a hard reference between them, because it makes it difficult to re-assemble systems into different configurations and relationships.
- Create scenes as clean slates: avoid having transient data existing between your scenes. Every time you hit a scene, it should be a clean break and load. This allows you to have scenes that have unique behavior that was not present in other scenes, without having to do a hack.
- Set up Prefabs so that they work on their own. As much as possible, every single prefab that you drag into a scene should have its functionality contained inside it. This helps a lot with source control with bigger teams, wherein scenes are a list of prefabs and your prefabs contain the individual functionality. That way, most of your check-ins are at the prefab level, which results in fewer conflicts in the scene.
- Focus each component on solving a single problem. This makes it easier to piece multiple components together to build something new.
Make it easy to change and edit parts:
- Make as much of your game as data-driven as possible. When you design your game systems to be like machines that process data as instructions, you can make changes to the game efficiently, even when it’s running.
- If your systems are set up to be as modular and component-based as possible, it makes it easier to edit them, including for your artists and designers. If designers are able to piece things together in the game without having to ask for an explicit feature – largely thanks to implementing tiny components that each do one thing only – they can potentially combine such components in different ways to find new gameplay/mechanics, Ryan says that some of the coolest features his team has worked on in their games come from this process, what he calls “emergent design’”.
- It’s crucial that your team can make changes to the game at runtime. The more you can change your game at runtime, the more you can find balance and values, and, if you’re able to save your runtime state back out like Scriptable Objects do, you’re in a great place.
Make it easy to debug:
This one is really a sub-pillar to the first two. The more modular your game is, the easier it is to test any single piece of it. The more editable your game is – the more features in it that have their own Inspector view – the easier it is to debug. Make sure you can view debug state in the Inspector and never consider a feature complete until you have some plan for how you’re going to debug it.
One of the simplest things you can build with ScriptableObjects is a self-contained, asset-based variable. Below is an example for a FloatVariable but this expands to any other serializable type as well.
Everyone your team, no matter how technical, can define a new game variable by creating a new FloatVariable asset. Any MonoBehaviour or ScriptableObject can use a public FloatVariable rather than a public float in order to reference this new shared value.
Even better, if one MonoBehaviour changes the Value of a FloatVariable, other MonoBehaviours can see that change. This creates a messaging layer between systems that do not need references to each other.
Example: Player’s Health Points
An example use case for this is a player’s health points (HP). In a game with a single local player, the player’s HP can be a FloatVariable named PlayerHP. When the player takes damage it subtracts from PlayerHP and when the player heals it adds to PlayerHP.
Now imagine a health bar Prefab in the scene. The health bar monitors the PlayerHP variable to update its display. Without any code changes it could easily point to something different like a PlayerMP variable. The health bar does not know anything about the player in the scene, it just reads from the same variable that the player writes to.
Once we are set up like this it’s easy to add more things to watch the PlayerHP. The music system can change as the PlayerHP gets low, enemies can change their attack patterns when they know the player is weak, screen-space effects can emphasize the danger of the next attack, and so on. The key here is that the Player script does not send messages to these systems and these systems do not need to know about the player GameObject. You can also go in the Inspector when the game is running and change the value of PlayerHP to test things.
When editing the Value of a FloatVariable, it may be a good idea to copy your data into a runtime value to not change the value stored on disk for the ScriptableObject. If you do this, MonoBehaviours should access RuntimeValue to prevent editing the InitialValue that is saved to disk.
One of Ryan’s favorite features to build on top of ScriptableObjects is an Event system. Event architectures help modularize your code by sending messages between systems that do not directly know about each other. They allow things to respond to a change in state without constantly monitoring it in an update loop.
The following code examples come from an Event system that consists of two parts: a GameEvent ScriptableObject and a GameEventListener MonoBehaviour. Designers can create any number of GameEvents in the project to represent important messages that can be sent. A GameEventListener waits for a specific GameEvent to be raised and responds by invoking a UnityEvent (which is not a true event, but more of a serialized function call).
Event system that handles Player death
An example of this is handling player death in a game. This is a point where so much of the execution can change but it can be difficult to determine where to code all of the logic. Should the Player script trigger the Game Over UI or a music change? Should enemies check every frame if the player is still alive? An Event System lets us avoid problematic dependencies like these.
When the player dies, the Player script calls Raise on the OnPlayerDied event. The Player script does not need to know what systems care about it since it is just a broadcast. The Game Over UI is listening to the OnPlayerDied event and starts to animate in, a camera script can listen for it and start fading to black, and a music system can respond with a change in music. We can have each enemy listening for OnPlayerDied as well, triggering a taunt animation or a state change to go back to an idle behavior.
This pattern makes it incredibly easy to add new responses to player death. Additionally, it is easy to test the response to player death by calling Raise on the event from some testing code or a button in the Inspector.
The event system they built at Schell Games has grown into something much more complex and has features to allow for passing data and auto-generating types. This example was essentially the starting point for what they use today.
Scriptable Objects don’t have to be just data. Take any system you implement in a MonoBehaviour and see if you can move the implementation into a ScriptableObject instead. Instead of having an InventoryManager on a DontDestroyOnLoad MonoBehaviour, try putting it on a ScriptableObject.
Since it is not tied to the scene, it does not have a Transform and does not get the Update functions but it will maintain state between scene loads without any special initialization. Instead of a singleton, use a public reference to your inventory system object when you need a script to access the inventory. This makes it easy to swap in a test inventory or a tutorial inventory than if you were using a singleton.
Here you can imagine a Player script taking a reference to the Inventory system. When the player spawns, it can ask the Inventory for all owned objects and spawn any equipment. The equip UI can also reference the Inventory and loop through the items to determine what to draw.
Yes!
Meh.
What you will get from this page: Effective strategies for architecting the code of a growing project, so it scales neatly and with fewer problems. As your project grows, you will have to modify and clean up its design repeatedly. It’s always good to take a step back from the changes you’re making, break things down into smaller elements to straighten them out, and then put all of it together again.
The article is by Mikael Kalms, CTO of Swedish game studio Fall Damage. Mikael has over 20 years of experience in developing and shipping games. After all this time, he’s still keenly interested in how to architect code so that projects can grow in a safe and efficient way.
From simple to complex
Let’s look at some code examples from a very basic Pong-style game my team made for my Unite Berlin talk. As you can see from the image above, there are two paddles and four walls–at the top and bottom, and left and right–some game logic and the score UI. There’s a simple script on the paddle as well as for the walls.
This example is based on a few key principles:
- One “thing” = one Prefab
- The custom logic for one “thing” = one MonoBehavior
- An application = a scene containing the interlinked Prefabs
Those principles work for a very simple project such as this, but we’ll have to change the structure if we want this to grow. So, what are the strategies that we can use to organize the code?
Instances, Prefabs and ScriptableObjects
Firstly, let’s clear away confusion about the differences between instances, Prefabs and ScriptableObjects. Above is the Paddle Component on Player 1’s Paddle GameObject, viewed in the Inspector:
We can see that there are three parameters on it. However, nothing in this view gives me an indication of what the underlying code expects of me.
Does it make sense for me to change the Input Axis on the left paddle by changing it on the instance, or should I be doing that in the Prefab? I presume that the input axis is different for both players, so it probably should be changed on the instance. What about Movement Speed Scale? Is that something I should change on the instance or on the Prefab?
Let's look at the code that represents the Paddle Component.
Parameters in a simple code example
If we stop and think for a bit, we will realize that the different parameters are being used in different ways in our program. We ought to change the InputAxisName individually for each player: MovementSpeedScaleFactor and PositionScale should be shared by both players. Here's a strategy that can guide you in when to use instances, Prefabs and ScriptableObjects:
- Do you need something only once? Create a Prefab, then instantiate.
- Do you need something multiple times, possibly with some instance-specific modifications? Then you can create a Prefab, instantiate it, and override some settings.
- Do you want to ensure the same setting across multiple instances? Then create a ScriptableObject and source data from there instead.
See how we use ScriptableObjects with our Paddle Component in the next code example.
Using ScriptableObjects
Since we have moved these settings to a ScriptableObject of type PaddleData, then we just have a reference to that PaddleData in our Paddle Component. What we end up with, in the Inspector, are two items: a PaddleData, and two Paddle instances. You can still change the axis name and which packet of shared settings each individual paddle is pointing to. The new structure allows you to see the intent behind the different settings more easily.
Splitting up large MonoBehaviors
If this was a game in actual development, you would see the individual MonoBehaviors grow larger and larger. Let’s see how we can split them up by working from what’s called the Single Responsibility Principle, which stipulates that each class should handle one single thing. If applied correctly you should be able to give short answers to the questions, “what does a particular class do?” as well as “what does it not do?” This makes it easy for every developer on your team to understand what the individual classes do. It’s a principle that you can apply to a code base of any size. Let’s look at a simple example as shown in the image above.
This shows the code for a ball. It doesn’t look like much, but on closer inspection, we see that the ball has a velocity that is used both by the designer to set the initial velocity vector of the ball, and by the homemade physics simulation to keep track of what the current velocity of the ball is.
We are reusing the same variable for two slightly different purposes. As soon as the ball starts moving, the information about initial velocity is lost.
The homemade physics simulation is not just the movement in FixedUpdate(); it also encompasses the reaction when the ball hits a wall.
Deep within the OnTriggerEnter() callback is a Destroy() operation. That is where the trigger logic deletes its own GameObject. In large code bases it is rare to allow entities to delete themselves; the tendency is instead to have owners delete things that they own.
There's an opportunity here to break things up into smaller parts. There are a number of different types of responsibilities in these classes–game logic, input handling, physics simulations, presentations and more.
Here are ways to create those smaller parts:
- The general game logic, input handling, physics simulation and presentation could reside within MonoBehaviors, ScriptableObjects or raw C# classes.
- For exposing parameters in the Inspector, MonoBehaviors or ScriptableObjects can be used.
- Engine event handlers, and the management of a GameObject’s lifetime, need to reside within MonoBehaviors.
I think that for many games, it’s worthwhile to get as much code as possible out of MonoBehaviors. One way of doing that is using ScriptableObjects, and there are already some great resources out there on this method.
From MonoBehaviors to regular C# classes
Moving MonoBehaviors to regular C# classes is another method to look at, but what are the benefits of this?
Regular C# classes have better language facilities than Unity’s own objects for breaking down code into small, composable chunks. And, regular C# code can be shared with native .NET code bases outside of Unity.
On the other hand, if you use regular C# classes, then the editor does not understand the objects, and cannot display them natively in the Inspector, and so on.
With this method you want to split up the logic by type of responsibility. If we go back to the ball example, we’ve moved the simple physics simulation into a C# class that we call BallSimulation. The only job it has to do is physics integration and reacting whenever the ball hits something.
However, does it make sense for a ball simulation to make decisions based on what it actually hits? That sounds more like game logic. What we end up with is that Ball has a logic portion that controls the simulation in some ways, and the result of that simulation feeds back into the MonoBehavior.
If we look at the reorganized version above, one significant change we see is that the Destroy() operation is no longer buried many layers down. There are only a few clear areas of responsibility left in the MoneBehavior at this point.
There are more things that we can do to this. If you look at the position update logic in FixedUpdate(), we can see that the code needs to send in a position and then it returns a new position from there. The ball simulation does not really own the location of the ball; it runs a simulation tick based on the location of a ball that is provided, and then returns the result.
Using interfaces
If we use interfaces then perhaps we can share a portion of that ball MonoBehavior with the simulation, just the parts that it needs (see image above).
Let’s look at the code again. The Ball class implements a simple interface. The LocalPositionAdapter class makes it possible to hand a reference to the Ball object over to another class. We don’t hand the entire Ball object, only the LocalPositionAdapter aspect of it.
BallLogic also needs to inform Ball when it is time to destroy the GameObject. Rather than returning a flag, Ball can provide a delegate to BallLogic. That is what the last marked line in the reorganized version does. This gives us a neat design: there is a lot of boilerplate logic, but each class has a narrowly defined purpose.
By using these principles you can keep a one-person project well structured.
Software architecture
Let's look at software architecture solutions for slightly larger projects. If we use the example of the Ball game, once we start introducing more specific classes into the code–BallLogic, BallSimulation, etc–then we should be able to construct a hierarchy:
The MonoBehaviours have to know about everything else because they just wrap up all that other logic, but simulation pieces of the game don't necessarily need to know about how the logic works. They just run a simulation. Sometimes, logic feeds in signals to the simulation, and the simulation reacts accordingly.
It is beneficial to handle input in a separate, self-contained place. That is where input events are generated and then fed into the logic. Whatever happens next is up to the simulation.
This works well for input and simulation. However, you are likely going to run into problems with anything that has to do with presentation, for example, logic that spawns special effects, updating your scoring counters, and so on.
The presentation needs to know what’s going on in other systems but it does not need to have full access to all of those systems. If possible, try to separate logic and presentation. Try to get to the point where you can run your code base in two modes: logic only and logic plus presentation.
Sometimes you will need to connect logic and presentation so that presentation is updated at the right times. Still, the goal should be to only provide presentation with what it needs to display correctly, and nothing more. This way, you will get a natural boundary between the two parts that will reduce the overall complexity of the game that you're building.
Sometimes it is fine to have a class that contains only data, without incorporating all of the logic and operations that can be done with that data into the same class.
It can also be a good idea to create classes that don’t own any data but contain functions whose purpose is to manipulate objects that are being given to it.
What’s nice about a static method is that, if you presume that it doesn’t touch any global variables, then you can identify the scope of what the method potentially affects just by looking at what is passed in as arguments when calling the method. You don’t need to look at the implementation of the method at all.
This approach touches upon the field of functional programming. The core building block there is: you send something to a function, and the function returns a result or perhaps modifies one of the out parameters. Try this approach; you may find that you get less bugs compared to when you do classic object-oriented programming.
You can also decouple objects by inserting glue logic between them. If we take the Pong-style example game again: how will the Ball logic and Score presentation talk to one another? Is the ball logic going to inform the score presentation when something happens with regards to the ball? Is the score logic going to query the Ball logic? They will need to talk to one another, somehow.
Unity Architecture Definition
You can create a buffer object whose sole purpose is to provide storage area where the logic can write things and the presentation can read things. Or, you could put a queue in between them, so that the logic system can put things into the queue and the presentation will read what’s coming from the queue.
A good way to decouple logic from presentation as your game grows is with a message bus. The core principle with messaging is that neither a receiver nor a sender knows about the other party, but they are both aware of the message bus/system. So, a score presentation needs to know from the messaging system about any events that change the score. The game logic will then post events to the messaging system that indicate a change in points for a player. A good place to start if you want to decouple systems is by using UnityEvents - or write your own; then you can have separate buses for separate purposes.
Stop using LoadSceneMode.Single and use LoadSceneMode.Additive instead.
Use explicit unloads when you want to unload a scene–sooner or later you will need to keep a few objects alive during a scene transition.
Stop using DontDestroyOnLoad as well. It makes you lose control over an object’s lifetime. In fact, if you are loading things with LoadSceneMode.Additive, then you won’t need to use DontDestroyOnLoad. Put your long-living objects into a special long-living scene instead.
Another tip that has been useful on every single game I’ve worked on has been to support a clean and controlled shutdown.
Make your application capable of releasing practically all resources before the application quits. If possible, no global variables should still be assigned and no GameObjects should be marked with DontDestroyOnLoad.
When you have a particular order for the way you shut things down, it will be easier for you to spot errors and find resource leaks. This will also leave your Unity Editor in a good state when you exit Play mode. Unity does not do a full domain reload when exiting play mode. If you have a clean shutdown, it is less likely that the editor or any kind of edit mode scripting will show weird behaviors after you have run your game in the editor.
Unity Architecture
You can do this by using a version control system such as Git, Perforce or Plastic. Store all assets as text, and move objects out of scene files by making them into Prefabs. Finally, split scene files into multiple smaller scenes, but be aware that this might require extra tooling.
If you are soon to be a team of, say, 10 or more people then you will need to do some work on process automation.
As a creative programmer you want to do the unique, careful work, and leave as much as possible of the repetitive parts to automation.
Start by writing tests for your code. Specifically, if you're moving things out of MonoBehaviours and into regular classes, then it is very straightforward to use a unit testing framework for building unit tests for both logic and simulation. It doesn't make sense everywhere, but it tends to make your code accessible to other programmers later on.
Testing is not just about testing code. You also want to test your content. If you have content creators on your team, you will all be better off if they have a standardized way to quickly validate content that they create.
Test logic–like validating a Prefab or validating some data which they've input through a custom editor–should be easily available to the content creators. If they can just click a button in the editor and get a quick validation, they will soon learn to appreciate that this saves them time.
The next step after this is to set up the Unity Test Runner so you get automatic retesting of things on a regular basis. You want to set it up as part of your build system, so that it also runs all your tests. A good practice is to set up notifications, so that when a problem does occur your teammates get a Slack or email notification.
Automated playthroughs involve making an AI that can play your game and then log errors. Simply put, any error that your AI finds is one less you have to spend time finding!
In our case, we set up around 10 game clients on the same machine, with the lowest detail settings, and let all of them run. We watched them crashed and then looked at the logs. Every time one of these clients crashed was time saved for us where we did not have to play the game ourselves, or get someone else to do it, to find bugs. That meant that when we did actually play test the game ourselves and with other players we could focus on if the game was fun, where the visual glitches were, and so on.