It’s not something a lot of new developers to Unity think about, but it’s still an incredibly important topic. It’s about coding practices, and it can make your life much easier.
The idea behind coding practices is that you keep in mind a set of guidelines when you write your code. These guidelines are often designed to keep your code maintainable, extensible, and readable, which in turn helps reduce the pain of refactoring.
How many times have you wanted to change some code in your project, which turned out to be much harder than you thought because there’s a bunch of classes relying on that code – it’s like when you go to unplug your keyboard, and you find that the back of your computer is a horror-inducing tangled mess of cables and there’s almost no hope of picking out just the one.
So, here’s some general guidelines to help you keep code untangled and tidy.
Single Responsibility
A given class should be, ideally, responsible for just one task.
The idea behind Single Responsibility is that each class does its own little task, and then you can solve larger goals by combining these little tasks together. If that sounds like I’m describing components, that’s because the two work perfectly together.
Unity being designed around separation of functionality into reusable parts, Single Responsibility is perhaps the most important principle of the ones I’m going to present in this post.
To give an example of this principle, let’s say we have a Player class. Player handles input, physics, weapons, health, and inventory.
That’s a rather long list of things the Player is responsible for. We should break this into multiple different components that each do just one thing.
PlayerInput – responsible for handling input
PlayerPhysics – responsible for driving physics simulation
WeaponManager – responsible for managing player weapons
Health – responsible for player health
PlayerInventory – responsible for player inventory
Ah, much better. Now, these classes are all largely dependent on each other, and that still poses some issues. Let’s take a look at resolving this.
Dependency Inversion
If a class relies on another class, abstract that reliance.
The idea behind Dependency Inversion is that whenever one class calls into another class, we should replace that call with some kind of abstraction to better insulate the classes from each other.
Most preferable is to replace the class dependence with an interface. So class A instead of calling methods in class B, actually calls methods of ISomeInterface which class B implements.
Another way is to introduce an abstract base class which class B inherits from. So class A treats class B as the base class instead of directly relying on class B itself.
The least preferable reliance, of course, is to have class A directly relying on class B. This is the one we want to avoid.
So in our previous example, those components may have a fairly tight coupling with each other. PlayerInput may rely on the presence of PlayerPhysics, WeaponManager may rely on the presence of Inventory, and so on.
Let’s abstract these.
IActorPhysics – Interface for classes which can accept input and presumably affect physics simulation
IDamageable – Interface for classes which can accept damage
IInventory – Interface for classes which can store and retrieve items
Now our PlayerPhysics implements IActorPhysics, our Health implements IDamageable, and our PlayerInventory implements IInventory.
So then PlayerInput just relies on the presence of some IActorPhysics, WeaponManager relies on the presence of some IInventory, and perhaps something which deals damage to our player relies on the presence of some IDamageable.
Dependency Inversion helps reinforce Single Responsibility to some extent. PlayerInput shouldn’t care how the player moves, nor should it care what is going to move the player. Just that there’s something which promises that functionality.
Another thing to keep in mind is that SendMessage can accomplish the same goals in some cases.
Modularization
I’ve personally seen code like this a lot in Unity:
Copy codeC#
switch( behavior )
{
case 1: // some behavior
case 2: // another behavior
case 3: // yet another behavior
case 4: // the last behavior
}
1
2
3
4
5
6
7
Sure it works, but we can do better. This code is practically screaming to be modularized. Each of those behaviors would be a module, and then “behavior” goes from being an enum or what have you to just being an instance of a module (lets say our module in this case is a delegate, although it could just as easily be a class or struct). So then that code just becomes:
Copy codeC#
behavior();
1
And somewhere else you can assign the current module like:
Copy codeC#
MyClass.behavior = someBehavior;
1
Where presumably someBehavior is a function (or, if our module is a class, an instance of that class, or whatever)
This is particularly useful if we’re making something like a state machine or a menu system. While we could have a big enum of all of the possible states, or of all of the possible menus, etc it makes a lot of sense to have state and menu be modules we can swap in and out. That way, we don’t have to keep adding new entries to the enum, and we don’t have to keep expanding our switch case statement every time we want to add something.
Conclusion
These are not by any means the only coding practices available. But, they are some very important ones to follow.
You don’t always have to follow them – sometimes, it’s just overkill and doesn’t make sense. But, do exercise critical thinking and determine whether your code could benefit from them (chances are, it can). Remember, it’s usually not about saving yourself a few lines of code now – it’s about saving yourself a headache later.
blog.theknightsofunity.com/unitytoolbag-library-overview/