
Good afternoon, Habr. On the air again, I, Ilya Kudinov, QA-engineer of Badoo. In my spare time, I develop toys on Unity 3D and decided to write an article about one of the problems our team faced as an experiment. I am the main developer, and our game designer in the "coffin saw" digging in my code for any purpose (division of labor is one of the greatest achievements of civilization), it means my duty is to provide him with all the necessary controls and settings gameplay in the form of user-friendly visual interfaces. The benefit of Unity itself has quite convenient (khe-khe) ready-made interfaces and a number of methods for their expansion. And today I will tell you about some tricks that make the life of our game designer easier and more convenient, and they allow me not to beat my head against the keyboard after each of his requests. I hope they can help some novice teams or those who just missed these moments while studying Unity.
I will say right away that our team is still actively learning and developing, even though we have already released our debut game. And if the “deadlines do not burn”, then I prefer to understand some things myself, rather than contact experts and various best practices. Therefore, something from what I have said may not be optimal or trite. I would be very happy if in such cases you tell me more convenient solutions in the comments and personal messages. Well, in general, the information here is more of a basic level.I write the code for Unity exclusively in C #, so all the calculations in the article will be in this language.
Singleton objects
The architecture of any game often includes various classes of managers, factories and helpers who do not need physical representations in the game world. Ideally, you could implement them with classes with static methods, do not create any
GameObjects in the scene for their work, and quietly use the code of the form
GameController.MakeEverybodyHappy () . However, this approach has two significant disadvantages in our case:
- to change any parameters, game designers will have to climb directly into the code, but they really dislike it;
- It will be more difficult to use links to any assets in Unity (prefabs, textures, etc.), since you have to load them through Resources.load () , and such code is much more difficult to maintain than the links that can be created through the Unity interface.
Solution to the problem? Inherit your
MonoBehaviour classes and create an object in the scene for each of them. Cons of this approach? When accessing these objects, you will have to use perverse calls like
FindObjectOfType <GameController> () or even
GameObject.Find ("GameController"). GetComponent <GameController> () . No self-respecting developer wants to do this at every step. Additional problems and possible errors begin to arise when it is necessary to transfer such objects between scenes or when several objects with such a class appear (or their absence).
So, we need some kind of mechanism that will allow us to receive an object of interest without any magic and to control that at the time of each appeal in our scene there will be one and exactly one object of this class.
Our solution is as follows (the “backbone” of the class I once found on the Internet and slightly modified for my own convenience):
')
using UnityEngine; public class Singleton<T> : MonoBehaviour where T : MonoBehaviour { private static T _instance; public void Awake() {
How to use it? We inherit our classes from
Singleton , specifying ourselves in the template:
public class GameController : Singleton<GameController>

In the future, we will be able to access the fields and methods of our class as
GameController.Instance.HarassPlayer () anywhere in the code. To create links to assets, it is enough to add this component to any object in the scene and customize it in the usual way (and save it to the prefab for fidelity). We use the Library object at the root of the scene to store all
Singleton classes, the tuning of which our game designer may need so that he does not have to look for them throughout the whole scene.
Of course, this implementation in no way implements the Singleton methodology, and my class is called so only because it allows you to generally maintain the architecture created within this methodology.
The alternative is to write your own inspector panels, which will allow you to store data and even references to assets in real static classes, but the proposed option is simpler and no less convenient.Custom fields inspector class
So, the game designer got his visual interfaces and started creating gameplay. And he quickly began to hate me, an innocent developer, for all the inconveniences that piled on him. An array of serialized objects? Multidimensional arrays? Why is it all so inconvenient to tune? No matter how hard you try to make a universal and expandable system on the side of the code, your game designer would prefer to see the minimum number of drop-down lists and even more so the arrays of elements with names like
Element 73 . But can we do something about it?
Actually we can. Suppose we have difficulty settings in our game, and at the moment there are three options. But perhaps it will be more. Therefore, we are looking into the future and, to simplify the further POSSIBLE increase in the number of difficulties, we create such a wonderful class of “dependent-on-complexity-ints” and replace usual ints in the right places with it:
[System.Serializable] public class DifDepInt { public int[] values = {0, 0, 0}; static public implicit operator int (DifDepInt val) { return val.Get(); } public int Get() { return values[GameConfig.Instance.difficulty]; } }
DifDep means Difficulty Dependant. Of course, from the point of view of architecture, it would be better to make a DifDep <T> template instead of this specific class, which accepts any data types, but unfortunately I have not found a way to create custom editor fields for templates.So, we are satisfied with ourselves, we were able to introduce varying parameters into the game without much difficulty. But our game designer, who needs to set it all up, for some reason is not happy. We should ask him what is going on ... Oh, that's it!
Yes, definitely, it is not very intuitive and convenient. Let's make it look different! To do this, we will use the above Unity “bun” - the ability to create custom inspectors to display various classes. This is a tricky system that allows you to do almost anything, but it is not so easy to understand at first glance (from the very beginning it scared me away, and so for a while we did suffer with the standard inspector, but in the end the moment truth has come).
So, we write the following code:
#if UNITY_EDITOR using UnityEditor; [CustomPropertyDrawer(typeof(DifDepInt))] public class DifDepIntDrawer : PropertyDrawer { int difCount = 3; public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) { EditorGUI.BeginProperty(position, label, property); Rect contentPosition = EditorGUI.PrefixLabel(position, label); contentPosition.width *= 1 / difCount; float width = contentPosition.width; SerializedProperty values = property.FindPropertyRelative ("values"); for (int i = 0; i < difCount; i++) { EditorGUI.PropertyField (contentPosition, values.GetArrayElementAtIndex(i), GUIContent.none); contentPosition.x += width; } EditorGUI.EndProperty(); } } #endif
Let's see what is happening here. The
#if compiler
directive UNITY_EDITOR tells Unity that it should compile this class only during development in the editor. Otherwise, it will try to collect this code when building the build of the game, and the
UnityEditor module is completely unavailable there, and this can cause confusing errors.
[CustomPropertyDrawer (typeof (DifDepInt))] tells Unity that it needs to use the code provided below instead of the standard to render the fields of the
DifDepInt class. You can specify such directives as many as you like for all
DifDep classes that you need - the custom editor code itself is written in such a way that it accepts any classes that have an array of elements called values, so this class serves me both
int and
float , and even
Sprite and
GameObject .
We overload the
OnGUI () method, which is engaged in drawing the field editing area in the inspector. Unity calls it sometimes several times per frame - this needs to be kept in mind. Do not forget to leave the methods
EditorGUI.BeginProperty () and
EditorGUI.EndProperty () , without them your code will not work correctly.
The rest of the code is fairly intuitive if you look into the Unity documentation. Instead of magic with
contentPosition, you can use the rendering methods from the
EditorGUILayout class, and not the
EditorGUI , but they do not always behave in an obvious way and in some bad cases it is more expensive to deal with them.
Why did we do this? See, what a beauty!
This is definitely more convenient than what it was. The possibilities offered by this functionality are almost endless - you can display the most complex structures in the most convenient way for editing. But do not think that the game designer will be grateful to you. He will take it for granted, I guarantee you (:
Custom editors whole class
OK, we have learned how to draw individual fields, but can we draw something that covers the whole class? Of course! For example, we set the parameters for all grades of a single type of weapon like this:
In addition to editing the fields, there is also a calculator, the values ​​in which change automatically when the weapon's parameters change (in fact, they are read-only, they have the form of inputs for consistency and ease of alignment).
How to do something like that? Very simple and similar to what we did before! I will demonstrate with a simple example — adding a simple DPS calculator before all the other fields in the monster behavior class:
#if UNITY_EDITOR using UnityEditor; [CustomEditor(typeof(EnemyBehaviour), true)] public class EnemyCalculatorDrawer : Editor { public override void OnInspectorGUI() { EnemyBehaviour enemy = (EnemyBehaviour)target; float dps1, dps20; dps1 = enemy.damageLeveling.Get(1) / enemy.getAttackDelay(1); dps20 = enemy.damageLeveling.Get(20) / enemy.getAttackDelay(20); GUIStyle myStyle = new GUIStyle (); myStyle.richText = true; myStyle.padding.left = 50; EditorGUILayout.LabelField("<b>Calculator</b>", myStyle); EditorGUILayout.LabelField("DPS on level 1: " + dps1.ToString("0.00"), myStyle); EditorGUILayout.LabelField("DPS on level 20: " + dps20.ToString("0.00"), myStyle); EditorGUILayout.Separator(); base.OnInspectorGUI(); } } #endif
The situation is very similar: first we tell Unity about the desire to replace the renderer for this class with the help of the directive
[CustomEditor (typeof (EnemyBehaviour), true)] . Then we override the
OnInspectorGUI () method (yes, this time not
OnGUI () , because the developer must suffer), we write our custom logic in it (the field named
target inherited from the class
Editor contains the reference to the displayed object as
Object ) and then call
base.OnInspectorGUI () so that Unity renders all other fields as usual.
GUIStyle allows us to change the appearance of the displayed data. In this case, I used the methods from
EditorGUILayout simply because there was absolutely no need to worry about even positioning.
The result is as follows:
Accordingly, in this way you can draw anything in the inspector, even though the graphs of the dependence of damage and survival on the level and stage of the game.
All sorts of stuff
Of course, you can do a lot of other things to save the eyes and brains of your colleagues. Unity offers a whole set of directives to turn a
public- field sheet into a structured whole. The most important of these is, of course,
[HideInInspector] , which allows you to hide the
public field from the inspector. And you no longer need to yell: “Please, do not touch these checkboxes, they are official!”, And then you still have to figure out for hours why all the monsters suddenly began to walk backwards. In addition, there are still nice things like
[Header ("Stats")] , which allow you to display a neat title in front of a block of fields, and
[Space] , which simply makes a small indent between the fields, helping to break them up into semantic groups. All three of these directives need to be written immediately before declaring a
public field (if you put
[Header ()] in front of a private field, Unity will not swear, but will not display any header).
A small hint: if your serializable object has a
string in itself called a
name , then when you put several such objects into a public array, its “name” will be displayed instead of the non-intuitive
Element X in the inspector.
And one more useful piece of advice: even if some custom object lies in your scene and is the only representative of its own kind, it still makes sense to make a prefab out of it. Then, in the case of joint work on the project, there will be no conflicts due to the simultaneous editing of the scene: the edits made to the prefab instance and applied using the Apply button will not affect the scene file.
Any project, on which more than one person works, forces its participants to sacrifice something for the sake of others, especially when the field of activity of each of them is very different. Development of computer games is a matter for specialists from many different areas. In a small cohesive team, the duty of everyone is to try to do everything in order to minimize the total sacrifices. So, it takes a couple of hours to study custom editors or any other techniques that seem to be not very important from the point of view of developing a code — a wonderful infusion into your project that can save more than one hour of work and millions of nerve cells of your colleagues. Comrades, software developers, let's live together with those for whom your code is like poems in traditional Chinese for you.
Comrade software developers who are fluent in traditional Chinese - a deep bow to you and apologies for such prejudices.