Structuring Your Unity Code For Production - Important Best Practices
It’s no secret that Unity is one of the most used game engines. It wasn’t so long ago, after all, that the company’s CEO claimed that half of all games are built on Unity.
There are many reasons for its popularity, but perhaps the most obvious is its ease of access. Ease of access is a double edged sword.
On one hand, Unity is a very easy engine to work with. It has a huge community, an Asset Store rich with a lot of useful assets, and there are a ton of resources to get you started.
On the other hand, a lot of the code samples and tutorials you find online only work for prototypes. But when it’s time to build a finished product, the messiness of such code could quickly pile up.
It wasn’t until my first year of working on actual production code that I started to pick up on these best practices. Years later, I’m still collecting tricks from the smarter developers I work with.
These tips are based on mistakes I see very frequently. If you’re self-taught or if you’ve just started your journey with Unity, these few tips might be helpful in getting you on the right track when working on a proper game project.
Organize your project (and use Namespaces!)..Don’t skip this!
Building a full project means every script you add needs to make sense in its context.
There are many reasons to do this. Discussing why is a lot like discussing why libraries have a system to categorize their books.
If you’re writing a game for production, you’re likely to spend a lot of time on it. During which, you might end up writing hundreds of scripts. Organizing these scripts allows you to understand the relationship between them, forces you to think of the context of each script, and saves you time on the long-term.
How does it save you time, you ask? Because a year into your project, you’ll still need to revisit some of your older code. Having it be properly organized allows you to spend less time with deciphering, but also allows you to have a far easier time debugging it. It also means that when someone new joins the project, they won’t have to get acquainted with how your mind palace works VS having a more logical approach that two people can easily understand.
But how should you organize your project?
That will all depend on what you’re making and how you think your features relate to each other. But generally, as straight forward as you can.
MainCategory/SubCategory/FeatureScript
Here’s a quick example:
Namespaces are also a great tool of organization. You can have scripts with similar names under different namespaces, but it also translates your folder structure into actual architecture. It should correspond directly to your folder location (just swap out “Scripts” with your game’s package name)
namespace GameName.Player.Controls
{
...
}
But for such a structure to make sense, you need to understand one important principle..
Class as an entity, not as a behaviour
Your classes (scripts) should not represent behaviours, but rather concepts.
Why?
When you learn Unity, you’ll find plenty of examples of classes called MoveToPlayer or *Shoot. *The issue with this is that it frames the class as a method, when it should be a concept or an entity.
This is great for prototypes, but it’s not practical when designing a system, which you’ll need when making anything larger than a prototype.
Why, you might ask?
Think of the structure we talked about in the previous point. Where would you place a *MoveToPlayer *script? In a big project, so many of these behaviours could repeat for different entities under different systems with different variations in the logic. You’ll have to repeat some of this disconnected logic, you’ll make the game harder to debug, and you’re going to break organization conventions.
But most importantly, you’ll make it harder to decouple your code because it’s stateless.
Take a look at this example. This script moves an object towards a player if the player is close enough.
using UnityEngine;
public class MoveToPlayer : MonoBehaviour
{
public Transform player;
public float speed;
public float maxDistance;
void Update()
{
if (Vector3.Distance(player.position, transform.position) < maxDistance)
{
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, player.position, step);
}
}
}
Think of a situation where you now want to implement sound and animation.
You’ll have to do one of the following:
-
Put the animation and sound code directly in this class, making it carry more responsibilities than what it was designed to do
-
Replicate the same condition in another script and put the animation or sound code there instead, repeating your code and thus having to keep track of more things if you want to make changes
-
Accessing this script somewhere else to determine if the object is moving towards the player, but then you’ll have to deduce the state of the object according to multiple similarly non-contextual behaviour scripts
So how do you write your features in this case?
Enter..
Structure with states and events
Simply, your object or enemy or whatever you’re writing is always in a specific state. This state is self-contained and worries only about its own logic. It doesn’t really communicate directly with any other system unless it depends on it, so it remains decoupled. However, it fires events, such as “I stopped walking!” informing other systems that are currently listening.
Generally you’d have a class that handles animation for this object, another that handles sound, and others that would respond to events fired from the current active state. The architecture for that might look like this:
In the example mentioned in the previous point, the behaviour where an enemy moves towards the player will happen in a state, and all the state has to do is invoke the action that the animation system is subscribed to, without even worrying about knowing what’s subscribed to that event.
OnStartedMovingTowardsPlayer?.Invoke();
This is especially helpful because being this decoupled means you can plug as many systems as you want to your classes without having to change them a lot of the time.
A very good example of this is plugging an achievments system late in development. Let’s say an achievement is triggered when a player collects a certain item. If items fire an event when they get collected, you can easily plug that into your newly created achievements system.
public class Achievements : MonoBehaviour
{
//..More code here
private void Awake()
{
Item.OnCollectedItem += HandleItemCollected;
}
private void OnDestroy()
{
Item.OnCollectedItem -= HandleItemCollected;
}
private void HandleItemCollected(Item item)
{
if (item is SpecialItem)
{
UnlockSpecialItemsAchievement();
}
}
private void UnlockSpecialItemsAchievement()()
{
//..Unlock achievement
}
}
What does this accomplish again?
If your achievements system was removed later on, the game will still work
You should think of how your features and system relate to the rest of the code. Here, an achievment system observes the state of the game. Could it be removed later on or excluded from other platforms? Yes. Then making concrete pieces of your gameplay system reference it might result in a harder setup.
Modular code
Think of what providing this event does VS implementing other features directly in your item class.
The item doesn’t need to know about your sound class, your animation class, what other entity picks it up or what it does with it.
If you start thinking about problems you’ve had in the past, you’ll find that this approach actually saves you a lot of spaghetti code.
Conclusion
Making a game could become a headache to manage. This shouldn’t come as a surprise. But if you structure your code right and follow the same conventions, the process should be far easier. Of course, how you do this depends completely on the requirements of your project. Good luck!