📜 ⬆️ ⬇️

Unity newbie bugs tested in their own skin

Hi, Habr. It's me again, Ilya Kudinov, QA-engineer from the company Badoo. But today I’ll tell you not about testing (I’ve already done this on Monday ), but about game devs. No, we don’t do that at Badoo, developing computer games is my hobby.

Industry professionals, do not judge strictly. All the tips in this article are for beginner developers who decide to try their hand at Unity. Many tips can be attributed to the development as a whole, but I will try to add Unity-specificity to them. If you can advise something better than I suggest - write in the comments, I will update the article.

I dreamed of developing toys since childhood. Probably already back in 1994, when they gave me my first Dendy, I thought: “ How would it be if there were more cool things in this igluchka ... ” In high school, I began to learn programming and together with He made his first playable crafts (oh, how we loved them!). At the institute, my friends and I made Napoleonic plans for a fundamental change in the industry with the help of our completely new topic ...
')
And in 2014, I started to learn Unity and finally, REALLY started to make games. However, the trouble is: I have never worked as a programmer. I had no experience of real corporate development (before that I did everything “on my knee”, and, apart from me, no one would understand my code). I could program, but I couldn’t do it well. All my knowledge of Unity and C # was limited to the official tutorials that were scarce at that time. And my favorite way to learn about the world is to make mistakes and learn from them. And I have done plenty of them.

Today I will talk about some of them and show you how to avoid them (oh, if I knew all this three years ago!)

In order to understand all the terms used in the material, it is enough to first go through one or two official Unity tutorials. Well, and have at least some idea of ​​programming.

Do not thrust all the logic of the object into one MonoBehaviour


Ah, my class MonsterBehaviour in our debut game! 3200 lines of spaghetti code in his worst days. Every need to return to this class gave me a slight shiver, and I always tried to postpone this work as long as I could. When I got to his refactoring a little more than a year after its creation, I not only broke it into a base class and several heirs, but also made several blocks of functionality into separate classes, which I added to objects directly from the code using gameObject.AddComponent() , so I did not have to change the already accumulated prefabs.

It was:
the monster class MonsterBehaviour , which kept in itself all the personal settings of the monsters, which determined their behavior, animation, leveling, finding the way and everything.

It became:


In just a few hours of work, I was able to cut a few hundred lines of hard-to-maintain code and save hours of nervous digging in go-bad code.

What, the essence of my advice is not to write giant classes, thank you, cap ? Not. My advice: split your logic into atomic classes before they become large. First, let your objects have three or four meaningful components with ten lines in each code, but it will be no more difficult to navigate them than in one of the 50 lines, but with the further development of logic you will not find yourself in a situation like me. At the same time there are more opportunities to reuse the code - for example, the component responsible for health and taking damage, you can stick to the player, and opponents, and even obstacles.

Clever term - Interface segregation principle .

Do not forget about OOP


No matter how simple at first glance the design of objects in Unity ( “Programming with the mouse, fuuuu” ) may seem, one should not underestimate this component of the development. Yes, yes, I underestimated. Straight by points:


 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 such a sweetheart that allows you not to produce these parameters, and these two calls will work identically:

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

Clever term - Single responsibility principle .

Explore the GUI Editor


I did it much later than it was worth. I have already written an article on how this can help both a programmer and a game designer in developing. In addition to custom inspectors for individual attributes and whole components, the GUI Editor can be used for a huge number of things. Create separate editor tabs to view and edit SAVE files of the game, to edit scenarios, to create levels ... The possibilities are endless! And the potential time savings are just amazing.

Think localization right from the start.


Even if you are not sure that you will translate the game into other languages. To force localization into an already formed project is intolerable pain. You can think of a variety of ways to localize and store translations. It is a pity that Unity is not able to independently carry out all the lines in a separate file that can be localized out of the box and without access to the rest of the application code (as, for example, in Android Studio). You will have to write such a system yourself. Personally, I use two solutions for this, albeit not very elegant ones.

Both are based on my own TranslatableString class:

 [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 still has a bunch of lines with error protection and field completeness checks, now I have removed them for readability. You can store translations as an array, but for a number of reasons I still chose separate fields.

All the “magic” is in the method of implicit conversion to a string. Thanks to him, you can call something like this anywhere in the code:

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

- and immediately receive the necessary translation in the text string, depending on the current language in the player’s settings. That is, in most places when adding localization, you don’t even have to change the code - it will just continue to work with strings, as before!

The first localization option is very simple and suitable for games where there are very few lines, and all of them are located in the UI. We simply add each object with a translatable UnityEngine.UI.Text component.

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

We fill in all the translation lines in the inspector - and voila, that's it!

For games where there are more tokens, I use a different approach. I have a Singleton LexemeLibrary LexemeLibrary that stores a map of the form “id tokens” => “serialized TranslatableString ”, from which I get the tokens in the right places. You can fill this library in any convenient way: with pens in the inspector, through a custom interface (hello, Editor GUI) or by exporting / importing CSV files. The latter option works great with outsourced translators, but it takes a little more work to avoid mistakes.

By the way, a useful thing - the language of the player’s system (in fact, its localization preferences) can be obtained using, for example, 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; } } 

Clever term - Dependency inversion principle .

Write detailed logs!


This may seem redundant, but now some of my games write to the log almost every sneeze. On the one hand, this wildly clutters the Unity console (which, unfortunately, does not know how to do any convenient filtering), on the other hand, you can open the source log files in any convenient software for viewing logs and compile any reports convenient to you, which will help to be engaged both in application optimization, and search of anomalies and their reasons.

Create self-contained entities


I did stupid things. Suppose we want to somehow store the settings of the 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 MissionController component sits in an object, contains settings for all game missions, and is accessible from anywhere in the code via MissionController.Instance .
About my class Singleton can be read in the already mentioned article .

My initial approach was this: Mission stores only parameters, and MissionController handles all other requests. For example, to get a better player account at a certain level, I used methods like

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

It would seem that everything is working properly. But then such methods became more and more, entities grew, proxy methods appeared in other classes ... In general, spaghetti-hell came. Therefore, in the end, I decided to put all the methods for working with missions into the Mission structure itself and began to receive mission records, for example, in the following way:

 MissionController.GetCurrentMission().GetHighScore(); 

which made the code much more readable and easy to maintain.

Do not be afraid to use PlayerPrefs


Many sources say that PlayerPrefs should be used very carefully and on every possible occasion, instead, serialize the data yourself and write them into your own files. Previously, I diligently prepared my own binary file format for each saved entity. Now I do not do that.

The PlayerPrefs class is PlayerPrefs to storing “key => value” pairs in the file system, and it works the same on all platforms, it simply stores its files in different places.

Constantly writing data to the PlayerPrefs fields (and reading them) is bad: regular disk queries do not do any good to anyone. However, you can write a simple but reasonable system that will help to avoid this.

For example, you can create a single SAVE object that stores all the settings and player data:

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

We write a simple system that does lazy initialization of this object (reads it from PlayerPrefs on the first request, puts it in a variable and on further requests already uses this variable), writes all changes to this object and saves it back to PlayerPrefs only when necessary ( when you exit the game and change key data).

To manipulate such an object as a string for PlayerPrefs.GetString() and PlayerPrefs.SetString() , it is enough to use JSON serialization:

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


Watch out for objects in the scene.


Here you have launched your game. It works, you rejoice. Played it for about 15 minutes, paused to check this curious vorning in the console ... VALUE, WHY IN 717 OBJECTS IN THE CORN ??? HOW TO ME ANYTHING TO FIND ???

Understanding this garbage is very difficult. Therefore, try to follow two rules:
Put all objects created through Instantiate() into some object structures. For example, I now always have a GameObjects object in a scene with category sub-objects into which I put everything I create. In order to avoid human error, in most cases I have add-ins over Instantiate() like InstantiateDebris() , which immediately put the object in the desired category.
Delete objects that are no longer needed. For example, some of my add-ins have a call Destroy(gameObject, timeout); with pre-assigned timeout for each category. Thanks to this, I don’t have to worry about cleaning things like blood stains on the walls, bullet holes, projectiles flying into infinity ...

Avoid GameObject.Find ()


Very expensive in terms of resources function to search for objects. Yes, and it is tied to the name of the object, which must be changed every time at least in two places (in the scene and in the code). The same can be said about GameObject.FindWithTag() (I would generally suggest not using tags - you can always find more convenient ways to determine the type of object).

If it is very impatient, be sure to cache each call into a variable in order not to do it more than once. Or even make the connection of objects through the inspector.

But you can do and more elegantly. You can use the class - the repository of references to objects, into which each potentially necessary object is registered, save the GameObjects meta-object from the previous board and search for the necessary objects in it via transform.Find() . All this is much better than polling each object in the scene about its name in search of the necessary, and then still fall with an error, because you recently renamed this object.

By the way, the Transform component implements the IEnumerable interface, which means that you can conveniently bypass all the child objects of the object in this way:

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

Important: unlike most other functions for searching for objects, transform.Find () returns even disabled (gameObject.active == false) objects at the moment.

Agree with the artist about the image format


Especially if the artist is yourself. Especially if the artist has never worked on games and IT projects in general before.

I will not be able to give a lot of texture advice for 3D games - I haven’t dug myself deep into it yet. It is important to teach the artist to save all the pictures with POT dimensions (Power Of Two, so that each side of the picture is a power of two, for example, 512x512 or 1024x2048), so that they are more efficiently compressed by the engine and do not occupy precious megabytes (which is especially important for mobile games).

But I can tell a lot of sad stories about sprites for 2D games.


Install Milestone


What it is? For good, Mylston (milestones - stones that used to set every mile along the road to mark distances) is a certain state of the project when it has reached its current goals and can move on to further development. Or maybe not go.

Perhaps it was our main mistake when working on a debut project. We set a lot of goals and went to all at once. Something was always left unfinished, and we could not say: “But now the project is really ready!” Because we constantly wanted to add something else to the existing functionality.

Don't do that. The best way to develop a game is to know the exact set of features and not to deviate from it. But it hurts rarely when it comes to large-scale industrial development. Games are often developed and upgraded right in the development process. So how to stay in time?

Make a plan of versions (Miles). So that each version was a complete game: so that there were no temporary stubs, crutches and unrealized functionality. So that it would be no shame on any Mailtown to say: “We’ll finish it!” And release (or close forever in the closet) a quality product.

Conclusion


I was stupid three years ago, right? I hope you will not repeat my mistakes and save a lot of time and nerves. , , .

PS “ ”, Unity . . , ?

Source: https://habr.com/ru/post/345710/


All Articles