Unity Best Practices
Making a game is very hard. There are a million sides to work on. However, it is possible to code in a certain way to avoid problems in the long run.
Enable Nullables
Issue
When handling objects in Unity, it is possible to get a NullReferenceException in certain
methods. This is could come from a bunch of sources:
- Object getting destroyed at the wrong time
- Components not existing in certain situations
- Mods "breaking" your content
In short, it is not a good sign. It shows only the intended use case was considered.
Solution
A good solution is to allow nullables by default. This will let the IDE know that a certain field may be null, which would force to consider this case.
The easiest way of adding this is to add -nullable:enable to the csc.rsp file.
Unity and the IDE should then notify you that certain fields may have a null reference.
// Before Nullables
HealthBar target;
target.SetHealth(10); // NullReferenceException if target is not assigned
// After Nullables
HealthBar? target;
if (target != null)
target.SetHealth(10); // Safe
target?.SetHealth(10); // Short and SafeA good habit is to mark every field as nullable by default, and only remove it when it's guaranteed the field will always be assigned.
csc.rsp inside Assets. It should then recompile
the project.Condense Parameters
Issue
As a functionality grows, there might be a method that needs more and more parameters. It starts at 3 parameters, but it grows until it reaches 9. This makes the method less maintainable, and the problem will only grow more and more:
void InitV1(int health, int maxHealth, int speed)
{
// ...
}
// Several days after
void InitV2(int health, int maxHealth, int speed, int defense, int shield, int maxSlugCount)
{
// ...
}Solution
When a method starts to have too many parameters, you can put them into a class. The class (or struct) whose only job is to hold data and process it if needed.
struct PlayerStats
{
int health;
int maxHealth;
int speed;
}
void InitV1(PlayerStats stats)
{
// ...
}
// Several days after
struct PlayerStats
{
int health;
int maxHealth;
int speed;
int defense;
int shield;
int maxSlugCount;
}
void InitV2(PlayerStats stats)
{
// ...
}The signature stays the same, even if more properties are added. This allows to not bleed the requirements onto
every method that touch it. They also only need to receive a PlayerStats to pass it along.
It is also possible to have utility methods in the data. Using the example above, it is maybe interesting to know how much total health the player has, combining the health and the shields.
struct PlayerStats
{
int maxHealth;
int shield;
int GetTotalHealth()
{
return maxHealth + shield;
}
}Drop MonoBehaviour (when possible)
Issue
A habit I see often when making games is to make everything a MonoBehaviour. This stems from
developers thinking this is needed to interact with the game. However, it is untrue. Making everything a MonoBehaviour has a few downsides:
- Creates a worse scene hierarchy, littering it with unnecessary objects
- Makes heavier instances
- Locks the class into a single ecosystem
Solution
Consider using plain C# classes instead. Not everything needs to be a component in the world. For example, object pools
could be done without being present in the world. They simply manage ìnstances of IDisposable,
not even knowing if they are objects, other classes or something else.
A good rule of thumb is to ask the following question: Can the class do its work without
Unity's lifecycle methods? If so, it might not need to be a MonoBehaviour.
Another good example are Behaviour Trees. In essence, they are just a bunch of classes linked together. Let's see how it would work with both methods:
// === Using MonoBehaviour ===
class Tree : MonoBehaviour
{
// ...
}
class Node : MonoBehaviour
{
// ...
}
// Hierarchy:
// |- tree
// | |- Node1
// | |- Node2
// === Using Plain C# classes ===
class TreePlayer : MonoBehaviour
{
// ...
}
class Tree
{
// ...
}
class Node
{
// ...
}
// Hierarchy:
// |- tree playerThis makes the code more portable. With the example of the trees, it would be possible to extract the framework into
a basic .dll file.