Mistakes Unity novices make, based on first-hand experience

Илья Кудинов
Bumble Tech
Published in
15 min readApr 6, 2018

Hi, all. It’s me again, Ilya Kudinov, QA engineer at Badoo. Today I am not going to talk about testing but about gamedev. No, this isn’t something we do at Badoo; developing computer games is a hobby of mine.

Industry professionals, don’t be harsh in your judgement. All the advice given in this article is addressed to beginner-level developers who have decided to try their hand at Unity. Lots of the advice relates to development in general, but I will try to add things which are Unity-specific. If you have better advice than what I am offering, feel free to leave a comment and I will update the article.

I have dreamed of developing games from when I was a child. It was probably way back in 1994 when I was given my first Nintendo console and I thought to myself, “Wouldn’t it be cool, if thith toy had all sorth of great extra thtuff…”. In middle school, I started to learn programming and, together with my friend, I made my first games which you could actually play (oh, how we loved them!). When I was a student at college, my friends and I made ambitious plans about transforming the industry introducing something completely new …

In 2014, I started to study Unity and finally ACTUALLY began making games. However, the downside was that I had never worked as a programmer. I didn’t have experience in real, corporate development (prior to that I did everything ‘on the hoof’ and no one, apart from me, could make anything of my code). I knew how to program, but I didn’t know how to do it well. The sum total of my knowledge of Unity and C# was limited to the modest number of official tutorials available at that time. My favourite approach is to make mistakes and learn from them. And I certainly made enough mistakes.

Today I am going to tell you about some of these mistakes and I will show you how to avoid them. If only I had known all this three years ago!

In order to understand all the terms used in the material, all you need to do is follow 1 or 2 official Unity tutorials in advance. And, you need to have some idea about programming.

Don’t stick all the logic for the object into one MonoBehaviour

Oh, that MonsterBehaviour class in our debut game! 3200 lines of spaghetti code on a bad day. Every time I had to come back to this class, it sent a chill down my spine and I always tried to put it off for as long as I could. When I did get round to refactoring it, just over a year after it was created, I not only broke it down into a base class and several derived classes, but I also moved several blocks of functionality into separate classes, which I added to objects straight from the code with the help of gameObject.AddComponent(), so I didn’t have to change the prefabs which had already accumulated.

Before:
A monster-like MonsterBehaviour class which saved all the personal settings for the monsters, determining their behaviour, animation, progress, navigation and everything else.

After:
MonsterComponent abstract class, from which all other components are derived and which connects them, and, for example, performs base optimisation by caching results of gameObject.GetComponent<T>();

MonsterStats class, into which the game designer feeds the parameters for the monsters. It saves them, changes them along with the level and sends them to other classes on request;

MonsterPathFinder class which finds paths and saves the data generated in static fields for optimisation of the algorithm;

MonsterAttack abstract class with derived classes under various forms of attack (weapon, claws, magic…) which control everything related to the combat behaviour of the monster — timings, animation, use of special techniques;

● And lots more classes performing all sorts of specific logic.

During the course of several hours of work I was able to cut out several hundred lines of difficult-to-support code and save hours of annoyingly having to comb through bad code.

So, does my advice boil down to not writing gigantic classes? (“Thanks for that, captain!”)

No. My advice is this: break your logic down into atomic classes before they get large. To start off with, let your objects have 3–4 meaningful components of 10 lines of code each, but navigating them will be no more complicated than in the case of a single class 50 lines long — with the benefit that, as the logic develops further, you won’t end up in the same situation I was in. As a side-effect there will also be more opportunities to re-use code — for example, a component responsible for health and injury may be assigned to a player, an opponent and even to obstacles.

Smart term — Interface segregation principle.

Don’t forget about OOP (object-oriented programming)

However simple it might seem, at first glance, to design objects in Unity (“Programming with a mouse, how despicable!”), there is no need to underestimate this part of the development work. Oh yes! I did underestimate it. So, let me proceed point-by-point:

● Inheritance. It is always nice to be able to move some general logic for several classes into a general base class. Sometimes it makes sense to do so early on, if the objects are ‘ideologically’ similar, even if they don’t yet have shared methods. For example, trunks at floor level and decorative torches on the walls didn’t initially have anything in common. However, when we started to develop the mechanism for lighting and extinguishing the torches, what we had to do was to move the mechanism for the player interacting with the torches and for showing tips in the interface from the trunks into the general class. I should have guessed this straightaway. I also have a general base class for all objects which is an add-in for MonoBehaviour — with a whole heap of useful new functions.

● Encapsulation. I am not even going to explain how useful it can be for the scopes to be set up correctly. It simplifies work, reduces the probability of making a silly mistake and makes it easier to debug… In this regard it is also helpful to be aware of two directives — [HideInInspector] which hides the component’s public fields in the inspector — those which are not a good idea to manage in the objects (incidentally, if possible, it is worth avoiding public fields altogether. This is bad practice. Instead it is better to use property. Thanks, Charoplet, for the reminder!) — and [SerializeField] which, in contrast, displays private fields in the inspector (which can be very useful for more convenient debugging).

● Polymorphism. Here the issue is solely about aesthetics and keeping code concise. One of my favourite things for supporting polymorphism in C# is universal templates. For example, I wrote these simple and convenient methods for pulling a random element from any class from List<T> (and I do this very often):

protected T GetRandomFromList<T>(List<T> list)
{
return list[Random.Range(0, list.Count)];
}
protected T PullRandomFromList<T>(ref List<T> list)
{
int i = Random.Range(0, list.Count);
T result = list[i];
list.RemoveAt(i);
return result;
}

At the same time C# is a dear old thing that it allows you not to multiply these parameters. The following two lines will work identically:

List<ExampleClass> list = new List<ExampleClass>();
ExampleClass a = GetRandomFromList<ExampleClass>(list);
ExampleClass a = GetRandomFromList(list);

Smart term — Single responsibility principle.

Study Editor GUI

I started to do this significantly later than I should have. I can’t even describe how much this could help with all the work both for the programmer and for the game designer. Besides custom inspectors for individual attributes and entire components, Editor GUI may be used for a huge number of things. Creating separate editor tabs for viewing and changing game SAVE files, for editing scenarios, for creating levels… the possibilities are endless! And the potential time savings are simply awe-inspiring.

Think about localisation from the very start

Think about localisation, even if you are not certain that you will be translating the game into other languages. Trying to ram localisation into a project which is already fully-formed is unbearably painful. There are all sorts of different ways of achieving localisation and storing translations. It is a shame that Unity is not itself capable of moving all the strings to a separate file, so that localisation can occur ‘off the shelf’ and without access to the rest of the application code (such as, for example, in Android Studio). You will have to write this system yourself. Personally, I use two solutions for this — even though they are not very refined.

Both are based on my own class, TranslatableString:

[System.Serializable] 
public class TranslatableString
{
public const int LANG_EN = 0;
public const int LANG_RU = 1;
public const int LANG_DE = 2;

[SerializeField] private string english;
[SerializeField] private string russian;
[SerializeField] private string german;
public static implicit operator string(TranslatableString
translatableString)
{
int languageId = PlayerPrefs.GetInt("language_id");
switch (languageId) {
case LANG_EN:
return translatableString.english;
case LANG_RU:
return translatableString.russian;
case LANG_DE:
return translatableString.german;
}
Debug.LogError("Wrong languageId in config");
return translatableString.english();
}
}

It contains a lot of strings with error protection and checks to ensure that fields have been completed. I have removed these for ease-of-reading. The translations can be saved as an array, however for a range of reasons I opted for separate fields.

The ‘magic’ is all about the method of indirect conversion to string form. Thanks to this, at any point in the code you can submit a query along the following lines:

TranslatableString lexeme = new TranslatableString();
string text = lexeme;

— and straightaway in the ‘text’ string you will obtain the translation you require, depending on the language selected in the player settings. Thus, in most places, when adding localisation, you don’t even need to change the code — it will simply continue working with strings, as before.

The first localisation option is very simple and is suitable for games with very few strings and which are all located in UI. We simply add the following component to each object with a UnityEngine.UI.Text component needing to be translated:

public class TranslatableUIText : MonoBehaviour
{
public TranslatableString translatableString;
public void Start()
{
GetComponent<UnityEngine.UI.Text>().text = translatableString;
}
}

We complete all the translation strings in the inspector — and voilà, it’s ready!

For games with a greater number of lexemes I use a different approach. I have a Singleton object, LexemeLibrary, which saves a chart in the form “id lexemes” => “serialised TranslatableString”, from which I then obtain lexemes in the locations I require them. This library can be completed however suits you: manually in the inspector, via a custom interface (“Hello, Editor GUI!”) or by exporting/importing CSV files. The final option also works fine with outsource translators, but it requires a bit more work to avoid errors.

Incidentally, here’s something useful: the player’s system language (basically, their localisation preferences) may be obtained, for example, with the help of the following code:

void SetLanguage(int language_id)
{
PlayerPrefs.SetInt("language_id", language_id);
}
public void GuessLanguage()
{
switch (Application.systemLanguage) {
case SystemLanguage.English:
SetLanguage(TranslatableString.LANG_EN);
return;
case SystemLanguage.Russian:
SetLanguage(TranslatableString.LANG_RU);
return;
case SystemLanguage.German:
SetLanguage(TranslatableString.LANG_DE);
return;
}
}

Smart term — Dependency inversion principle.

Write detailed logs!

It may seem unnecessary, but some of my games now log practically every single event, no matter how small. On the one hand this clogs up the Unity console (which, unfortunately, is not able to perform any convenient filtering). On the other hand, you can open the source log files in any piece of software you want and can view the logs and use them to compile whatever reports suit you, which can help you to optimise the application and look for anomalies and their causes.

Create self-contained entities

I did some foolish things. Let’s say we want to save the settings for various levels of a game:

public struct Mission
{
public int duration;
public float enemyDelay;
public float difficultyMultiplier;
}
public class MissionController : Singleton<MissionController>
{
public Mission[] missions;
public int currentMissionId;
}

The component, MissionController, sits inside a given object, contains the settings for all the missions of a game and is accessible from anywhere in the code via MissionController.Instance.

Singleton<> is a simple class I designed to address the only instance of a class via ClassName.Instance without the necessity of searching for a corresponding GameObject.

My initial approach was as follows: Mission only saves parameters, but MissionController deals with all other requests. For example, to obtain the top score for a player at a given level, I used methods along the following lines:

MissionController.GetHighScore(int missionId)
{
return PlayerPrefs.GetInt("MissionScore" + missionId);
}

It might seem like everything is working fine. However, there were more and more of these methods, entities grew, proxy methods appeared in other classes … It was like spaghetti hell. That was why, in the final analysis, I decided to move all the methods for working with missions into the actual Mission structure and started to obtain the high scores for a mission, for example, in the following manner:

MissionController.GetCurrentMission().GetHighScore();

This made the code much easier to read and more convenient to support.

Don’t be afraid to use PlayerPrefs

Lots of sources say that you should be very careful using PlayerPrefs and, whenever possible, you should serialise data yourself and write them to your own files. I used to work hard to prepare my own format for a binary file for each entity to be saved. I don’t do that anymore.

The PlayerPrefs class saves “key => value” pairs to a file system. It does so while working identically on all platforms, simply saving its files in different locations.

It is not a good thing to always write data (and read data) in PlayerPrefs: regular requests to the disk are not good for anyone. However, you can write a simple, but rational system, which allows you to avoid doing so.

For example, you can create a single SAVE object where all the settings and player data are saved:

[System.Serializable]
public struct Save
{
public string name;
public int exp;
public int[] highScores;
public int languageId;
public bool muteMusic;
}

Let’s write a simple system which will perform lazy initialisation for this object. On receiving the first request, it reads it from PlayerPrefs; it then writes it to the variable; and, then, in the case of subsequent requests, it will use the variable in question. The system will record all the changes to this object and only if essential will it save it back to PlayerPrefs (for example, when exiting the game and if key data is changed).

In order to manipulate an object, such as a string for PlayerPrefs.GetString() and PlayerPrefs.SetString(), it is sufficient to use serialisation to JSON:

Save save = newSave;
string serialized = JsonUtility.ToJson(newSave);
Save unserialized = JsonUtility.FromJson<Save>(serialized);

Keep an eye on the objects in the scene

So, you have launched your game. It is working — and you are happy. You play it for 15 minutes and put it on pause to check out that strange warning message on the console… OMG, WHY DO I HAVE 745 OBJECTS ON THE ROOT LEVEL IN MY SCENE??? HOW AM I EVER GOING TO FIND ANYTHING???

It is very tricky to wade through all that rubbish. So, try to follow two rules:

Place all the objects created by Instantiate() into some kind of object structure. For example, in the scene I now always have a GameObjects object with sub-object categories, into which I place everything I create. To avoid human error, in most cases I have add-ins for Instantiate() like InstantiateDebris()which place the object in question straight into the appropriate category.

Remove objects which are no longer required. For example, some of my add-ins have the request Destroy(gameObject, timeout); with a time-out set in advance for each category. So, I don’t need to worry about cleaning up such things as patches of blood on the walls, bullet-holes or ammunition which has flown off into oblivion …

Avoid GameObject.Find()

This is a very resource-intensive function for finding objects. It is also tied to the name of the object, which needs to be changed each time in at least two places (in the scene and in the code). The same can be said of GameObject.FindWithTag() (I would recommend not using tags at all — you can always find more convenient ways of determining the object type).

If you really have to do so, then make sure you cache each query to the variable, so that you don’t have to do so more than once. Or just connect objects via the inspector.

But there is a more refined way of doing it. You can use a class which is a repository of links to objects which registers each object which potentially might be required. You can save a GameObjects meta-object (from the previous tip) to this class (repository). And you can use transform.Find() to search for required objects in the class (repository) in question. This is far better than looking for the required object by addressing a query to each object in the scene, requesting its name, and then still ending up with an error because you recently renamed the object in question.

Incidentally, the Transform component implements the IEnumerable interface, so it is easy to circumvent all the object’s subsidiary objects as follows:

foreach (Transform child in transform) {        
child.gameObject.setActive(true);
}

Important: unlike most other object search functions, transform.Find() even returns objects which are currently inactive (gameObject.active == false).

Agree the image format with an artist

Especially if you are the artist. Especially if the artist has never previously worked on games or IT projects generally.

I am not in a position to give advice on the textures for 3D games I haven’t looked into that in any depth yet. What I know is that it is important to instruct the artist to save images with POT dimensions (Power of two, so that each side of the image is two to the power of something, 512х512 or 1024х2048), so that they are easier to compress by the engine and don’t take up precious megabytes (particularly important for mobile games).

Luckily, I have a lot of sorry tales to tell about sprites for 2D games.

● Combine sprites of a single type (and particularly separate sprites for a single animation) into a shared image. If you need 12 sprites of 256х256 pixels in size, you don’t need to save 12 images — it is much easier to make a single 1024х1024 pixel image and to arrange the sprites on the grid with a given side measuring 256 pixels in size and use the automatic system for breaking a texture down into sprites. You will be left with four free spaces; but that is not a problem, as you might need to add more images of the same type. Important: if there are not enough slots for sprites left, tell your artist to increase the canvas to a new size which is also an exponential power of two — only in a rightwards and upwards direction. That way you won’t have to manage metadata for the sprites you already have — they will remain at the same coordinates. UPD by KonH: instead of manually arranging sprites it is easier to use the internal utility, SpritePacker. I haven’t touched it yet myself, so for the moment I can’t tell you anything more (:

● Make sure you draw all the sprites for the project to a single scale, even if they end up on different textures. You cannot imagine how much time I spent adjusting the ‘pixels per unit’ values for various monster sprites, so that their sizes matched up in the game environment. Now for each texture I have an unused image of the main character to check how the dimensions match up. It’s not complicated — and saves so much time and nervous energy!

● Line up all the sprites of a single type vis-à-vis a single shared pivot. Ideally the centre of the image or the midpoint of one of the sides. For example, all the sprites for the player’s weapon should be positioned in the slot (or in a separate image) in such a way that the place where the player is going to grasp the weapon is positioned right at the centre. Otherwise you will have to manually position the pivot in the editor. This is inconvenient, and you could forget to do so, leaving the character holding a spear by its pointed end or an axe by its blade. The character would look stupid.

Set up milestones

What does that mean? A milestone is the stage a project is at, at a given moment in time, when it has achieved goals that have been set and is ready to move on to the next stage. Or not.

This was probably our biggest mistake when working on the debut project. We had set a lot of goals and set about achieving them straight away. There was always something which remained unfinished, and at no point were we able to say, “Now the project is really ready,” because we always wanted to add something more to the functionality.

Don’t do that. The best way to develop a game is to know for certain which set of features you want to have when you have finished, and not to waver from that. But this is painfully rare if we are talking about major industrial development. Games are often developed and modernised during the process of development. How can we know when it is time to stop?

Put together a version timetable (milestones). So that each version is a finished game in its own right: so that they aren’t any temporary ‘plugs’, ‘crutches’ or unrealised functionality. So that at each milestone you can say, “This is where we are going to finish,” and release a quality product (or lock it away in a cupboard forever).

Conclusion

I was pretty foolish three years ago, right? I hope you won’t repeat my mistakes and you will save a lot of time and nervous energy. And if you have been afraid to start developing games until now, perhaps I have been able to motivate you to give it a go.

P. S. I am considering writing a tutorial along the lines of “Making a game for a hackathon from scratch in 24 hours”, which would allow someone with no knowledge of Unity or programming skills to write their first game. There aren’t that many tutorials of this kind in available in Russian. What do you think: is it worth a try?

--

--