Tips and tricks for working with Unity3D
- Transfer

I published the first article, “50 Unity Tips” 4 years ago. Despite the fact that most of it is still relevant, much has changed for the following reasons:
- Unity got better. For example, now I can trust the FPS counter. The ability to use Property Drawers has reduced the need to write custom editors. The way to work with prefabs has become less demanding for predefined nested prefabs and their alternatives. Scriptable objects have become more friendly.
- Integration with Visual Studio has improved, debugging has become much simpler and the need for "monkey" debugging has decreased.
- Third-party tools and libraries have become better. A lot of assets have appeared in the Asset Store, simplifying aspects such as visual debugging and logging. Most of the code for our own (free) Extensions plugin is described in my first article (and much of it is described here).
- Improved version control. (But maybe I just learned to use it more efficiently). For example, now you do not need to create multiple or backup copies for prefabs.
- I have become more experienced. Over the past 4 years, I have worked on many Unity projects, including a bunch of game prototypes , completed games like Father.IO , and our main Unity Grids asset .
This article is a version of the original article, revised taking into account all of the above.
Before proceeding to the tips, I will first leave a short note (the same as in the first article). These tips may not apply to all Unity projects:
- They are based on my experience working on projects as part of small teams (from 3 to 20 people).
- Structuring, reusability, code clarity, and other aspects have a price: it depends on the size of the team, the size of the project, and the goals of the project. For example, you will not use all this for a gamejam .
- Using many tips is a matter of taste (there may be different, but still good techniques for any of the tips listed here).
The Unity website also has recommendations for working on projects (however, most of them are aimed at improving the productivity of projects) (all of them are in English) :
- Best practices
- Best practices for physically based content creation https://youtu.be/OeEYEUCa4tI
- 2D Best practices in Unity https://youtu.be/HM17mAmLd7k
- Internal Unity tips and tricks https://youtu.be/Ozc_hXzp_KU
- Unity Tips and Tricks https://youtu.be/2S6Ygq58QF8
- http://docs.unity3d.com/Manual/HOWTO-ArtAssetBestPracticeGuide.html
The working process
1. From the very beginning, decide on the scale and create everything on one scale. If you do not, you may have to redo the assets later (for example, the animation does not always scale correctly). For 3D games, it is probably best to take 1 Unity unit equal to 1 meter. For 2D games that do not use lighting and physics, usually 1 Unity unit equal to 1 pixel (in the "working" resolution) is usually suitable. For UI (and 2D games), select the working resolution (we use HD or 2xHD) and create all assets to scale in this resolution.
2. Make each scene run.This will allow you not to switch between scenes to start the game and thus speed up the testing process. This can be tricky if you use persistent objects that are passed between scene downloads, which are required in all scenes. One of the ways to achieve this is to make the transmitted objects singleton, which will load themselves if they are not in the scene. Singletones are discussed in more detail in another tip.
3. Apply source control and learn how to use it effectively.
- Serialize assets as text. In fact, this will not make scenes and prefabs more compatible, but it will be easier to track changes.
- Master the strategy of sharing scenes and prefabs. Usually, several people should not work on a scene or prefab. In a small team, before starting work on a scene or prefab, it may be enough to ask everyone not to work on them. It may be useful to use physical tokens to indicate who is currently working on the scene (you can work on the scene only if you have the corresponding token on your table).
- Use tags as bookmarks.
- Choose a branching strategy and stick to it. Since it is impossible to make the connection of scenes and prefabs smooth, branching can become quite complicated. Whatever branching method you choose, it should work with your scene and prefab sharing strategy.
- Use submodules with caution. Submodules can be a great way to support reusable code, but there are several dangers:
- Metafiles for different projects are generally not the same. This is usually not a problem for code that does not use MonoBehaviour or scriptable objects, but for MonoBehaviour and scriptable objects using submodules can lead to code loss.
- If you are working on several projects (one or several of which use submodules), then sometimes you may encounter an “avalanche of updates” when you need to perform several iterations of pull-merge-commit-push for different projects in order to stabilize the code in all projects ( and if during this process someone else makes changes, the avalanche can become continuous). One way to minimize this effect is to make changes to the submodules from the projects that relate to them. At the same time, projects using submodules will always have to pull, and they will never have to push.
- Metafiles for different projects are generally not the same. This is usually not a problem for code that does not use MonoBehaviour or scriptable objects, but for MonoBehaviour and scriptable objects using submodules can lead to code loss.
4. Always separate test scenes from code. Commit commits of temporary assets and scripts to the repository and remove them from the project when you finish working with them.
5. Upgrade tools (especially Unity) at the same time. Unity is much better at maintaining connections when opening a project from versions other than the current one, however, connections are still sometimes lost if team members work in different versions.
6. Import third-party assets into a clean project and import a new package for your use from there. When directly importing into a project, assets can sometimes lead to problems:
- Collisions (files or names) may occur, especially for assets that contain files in the root of the Plugins folder , or for those that use Standard Assets in their examples.
- They may be disordered and scatter their files throughout your project. This becomes a particular problem if you decide not to use it and want to remove it.
To make your assets safer, use the following instructions:
1. Create a new project and import the assets.
2. Run the examples and make sure they work.
3. Organize the asset into a more appropriate folder structure. (Normally, I don’t fit the asset to my own folder structure. But I check that all the files are in the same folder and that there are no files in important places that can overwrite existing files of my project.)
4. Run the examples and make sure that they still working. (Sometimes it happened that the asset “broke” when I moved its components, but usually this problem does not arise.)
5. Now delete the components that you do not need (such as examples).
6. Verify that the asset is still compiling and that the prefabs still have all their connections. If there is still something unreleased, test it.
7. Now select all assets and export the package.
8. Import it into your project.
7. Automate the build process. This is useful even in small projects, but in particular it is useful when:
- you need to build many different versions of the game,
- you need to make assemblies to other team members with different levels of technical experience or
- you need to make small changes to the project before you can build it.
For information on how to do this, read Unity Builds Scripting: Basic and advanced capabilities.
8. Document your settings. Most documentation should be in code, but something needs to be documented outside of it. Forcing developers to rummage through code for settings means wasting their time. Documented settings increase efficiency (if documents are kept up-to-date). Document the following:
- Using tags.
- Use of layers (for collisions, culling and raycasting - indicate what layer should be in).
- GUI depth for layers (what should be placed above).
- Scene settings.
- The structure of complex prefabs.
- Selected idioms.
- Build customization.
General Code Tips
9. Put all your code in a namespace. This avoids the code conflict of your own libraries and third-party code. But don't rely on namespaces when trying to avoid code conflicts with important classes. Even if you use other namespaces, do not use the Object, Action, or Event classes as names.
10. Use assertions. Statements are useful for testing invariants in code and help get rid of logical bugs. Claims are available through the Unity.Assertions.Assert class . They check the condition and write a message to the console if it is incorrect. If you do not know why statements can be useful, see The Benefits of programming with assertions (aka assert statements).
11. Do not use strings for anything other than displaying text. In particular, do not use strings to identify objects or prefabs. There are exceptions (there are still some elements in Unity that can only be accessed through a name). In such cases, define strings such as constants in files such as AnimationNames or AudioModuleNames. If such classes become unmanageable, use nested classes to introduce something like AnimationNames.Player.Run.
12. Do not use Invoke and SendMessage. These MonoBehaviour methods call other methods by name. Methods called by name are hard to track in the code (you cannot find “Usages”, and SendMessage has a wide scope, which is even more difficult to track).
You can easily write your own version of Invoke using Coroutine and actions C #:
public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time)
{
return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}
private static IEnumerator InvokeImpl(Action action, float time)
{
yield return new WaitForSeconds(time);
action();
}Then you can use it in MonoBehaviour like this:
this.Invoke(ShootEnemy); //где ShootEnemy - это невозвращающий значения (void) метод без параметров.( Addition: someone suggested using the ExecuteEvent class , part of the Unity event system , as an alternative . So far, I don’t know much about it, but it seems worth exploring it in more detail.)
13. Do not let spawned objects mess up the hierarchy when performance of the game. Set the object in the scene as the parent for them, so that when playing the game it is easier to find objects. You can use an empty game object, or even a singleton (see later in this article) without behavior, to make it easier to access it in code. Name this object DynamicObjects.
14. Be precise when using null as valid values, and avoid them where possible.
Null values are useful when looking for invalid code. However, if you become in the habit of ignoring null, incorrect code will execute successfully and you will not notice errors for a long time. Moreover, it can be declared deep inside the code, since each layer ignores null variables. I try not to use null at all as a valid value.
I prefer the following idiom: do not check for null and let the code fall out when a problem occurs. Sometimes in reusable methods, I check the variable for null and throw an exception instead of passing it to other methods in which it can lead to an error.
In some cases, null may be valid and therefore handled in a different way. In such cases, you need to add a comment indicating the reasons that the value may be null.
A common script is often used for values configured in the inspector. The user can specify a value, but if he does not, the default value will be used. The best way to do this is to use the Optional ‹T› class, which wraps the values of T. (This is a bit like Nullable ‹T›.) You can use a special property renderer to render the check box and display the value box only when the check box is selected. (Unfortunately, it is impossible to use the generic class directly; you need to extend the classes for certain values of T.)
[Serializable]
public class Optional
{
public bool useCustomValue;
public T value;
}In your code, you can use it this way:
health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;Addition: many people tell me that it is better to use struct (does not create garbage and cannot be null). However, this means that you cannot use it as a base class for non-generic classes so that you can use it for fields that you can use in the inspector.
15. If you use Coroutines, learn to use them effectively. Coroutines can be a convenient way to solve many problems. However, they are difficult to debug, and with their help you can easily turn the code into chaos in which no one, even you, can figure it out.
You must understand:
- How to execute coroutines in parallel.
- How to execute coroutines sequentially.
- How to create new coroutines from existing ones.
- How to create custom coroutines using CustomYieldInstruction.
//Это сама корутина
IEnumerator RunInParallel()
{
yield return StartCoroutine(Coroutine1());
yield return StartCoroutine(Coroutine2());
}
public void RunInSequence()
{
StartCoroutine(Coroutine1());
StartCoroutine(Coroutine1());
}
Coroutine WaitASecond()
{
return new WaitForSeconds(1);
}16. Use extension methods to work with components that share a common interface. ( Addition: It seems that GetComponent and other methods now also work for interfaces, so this advice is redundant) Sometimes it’s convenient to get components that implement a specific interface or find objects with such components.
The implementation below uses typeof instead of the generic versions of these functions. Generic versions do not work with interfaces, and typeof does. The method below wraps it in generic methods.
public static TInterface GetInterfaceComponent(this Component thisComponent)
where TInterface : class
{
return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}17. Use extension methods to make the syntax more convenient. For instance:
public static class TransformExtensions
{
public static void SetX(this Transform transform, float x)
{
Vector3 newPosition =
new Vector3(x, transform.position.y, transform.position.z);
transform.position = newPosition;
}
...
}18. Use the softer GetComponent alternative. Sometimes forcibly adding dependencies via RequireComponent can be unpleasant, it is not always possible or acceptable, especially when you call GetComponent on a foreign class. Alternatively, the following GameObject extension can be used when the object should throw an error message if it is not found.
public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
T component = obj.GetComponent();
if(component == null)
{
Debug.LogError("Ожидается компонент типа "
+ typeof(T) + ", но он отсутствует", obj);
}
return component;
}19. Avoid using different idioms to do the same thing. In many cases, there are various idiomatic ways of doing things. In such cases, select one idiom and use it for the entire project. And that's why:
- Some idioms are poorly compatible. Using one idiom directs development in a direction not suitable for another idiom.
- Using one idiom for the entire project allows project participants to better understand what is happening. At the same time, the structure and code become more understandable and the likelihood of errors is reduced.
Examples of idiom groups:
- Coroutines are finite state machines.
- Built-in prefabs - attached prefabs - god-prefabs.
- Data Sharing Strategies.
- Ways to use sprites for states in 2D games.
- The structure of prefabs.
- Spawning strategies.
- Ways to find objects: by type / name / tag / layer / link.
- Ways to group objects: by type / name / tag / layer / array of links.
- Ways to call methods of other components.
- Search for groups of objects / self-registration.
- Control of the execution order (using the Unity execution order setting - yield-logic - Awake / Start and Update / Late Update - manual methods - arbitrary architecture
- The selection of objects / positions / goals in the game with the mouse: the choice manager is local self-government.
- Data storage when changing scenes: through PlayerPrefs or using objects that are not destroyed (Destroy) when loading a new scene.
- Ways of combining (blending, adding and layering) animations.
- Input Processing (Central - Local)
20. Create and maintain your own time class to make working with pauses easier. Wrap Time.DeltaTime and Time.TimeSinceLevelLoad to control pauses and time scales. Using a class requires discipline, but it makes everything a lot easier, especially when executed with various timers (for example, interface animations and game animations).
Addition: Unity supports unscaledTime and unscaledDeltaTime, which make the native time class redundant in many situations. But it can still be useful if scaling global time affects components that you did not write in undesirable ways.
21. User classes requiring updating should not have access to global static time. Instead, they should receive a time delta as a parameter to the Update method. This allows you to use these classes when implementing the pause system described above, or when you want to speed up or slow down the behavior of a custom class.
22. Use a common framework for making WWW calls. In games with a large amount of communication with the server, there are usually dozens of WWW calls. Regardless of whether you use the raw WWW Unity class or the plugin, it will be convenient to write a thin layer on top that will work like a boilerplate.
I usually define the Call method (separately for Get and Post), the CallImpl and MakeHandler coroutines. In essence, the Call method creates using the MakeHandler method a “super hander” from the parser, an on-success and on-failure handler. It also calls the CallImpl coroutine, which forms the URL, makes the call, waits for it to complete, and then calls the “super handler”.
Here's what it looks like:
public void Call(string call, Func parser, Action onSuccess, Action onFailure)
{
var handler = MakeHandler(parser, onSuccess, onFailure);
StartCoroutine(CallImpl(call, handler));
}
public IEnumerator CallImpl(string call, Action handler)
{
var www = new WWW(call);
yield return www;
handler(www);
}
public Action MakeHandler(Func parser, Action onSuccess, Action onFailure)
{
return (WWW www) =>
{
if(NoError(www))
{
var parsedResult = parser(www.text);
onSuccess(parsedResult);
}
else
{
onFailure("Текст ошибки");
}
}
} There are several advantages to this approach.
- It avoids writing a large amount of boilerplate code
- It allows you to process the necessary elements (for example, displaying a loading UI component or handling certain common errors) in the first place.
23. If you have a lot of text, put it in a file. Do not put it in the edit fields in the inspector. Make it so that you can quickly change it without opening the Unity editor, and especially without having to save the scene.
24. If you plan on localizing, separate all the lines in one place. There are several ways to do this. One of them is to define the Text class with a string field of type public for each line, by default, for example, English will be set. Other languages will be child classes and reinitialize fields with language counterparts.
A more complex way (it is suitable for large amounts of text or a high number of languages) is to read the spreadsheet and create logic to select the desired row based on the selected language.
Class design
25. Decide how the inspected fields will be used, and make it a standard. There are two ways: make the fields public, or make them private and mark them as [SerializeField]. The latter is “more correct”, but less convenient (and this method is not very popularized by Unity itself). Whatever you choose, make it a standard so that the developers on your team know how to interpret the public field.
- Inspected fields are public. In this case, public means: “the variable can be safely changed by the designer during the execution of the application. Do not set its value in the code. "
- Inspected fields are private and marked as Serializable. In this case, public means: “you can safely change this variable in the code” (so there will not be very many of them, and there will be no public fields in MonoBehaviours and ScriptableObjects).
26. Never make component variables public unless they need to be configured in the inspector. Otherwise, they will be changed by the designer, especially if it is not clear what they are doing. In some rare cases this cannot be avoided (for example, if some editor script should use a variable). In this case, you need to use the HideInInspector attribute to hide it in the inspector.
27. Use property drawers to make fields more user friendly. Property drawers can be used to configure controls in the inspector. This will allow you to create controls that are most suitable for the type of data and insert protection (for example, limiting the values of variables). Use the Header attributeto organize the fields, and the Tooltip attribute to provide designers with additional documentation.
28. Give preference to property drawers rather than custom editors . Property drawers are implemented by field type, which means they require much less time to implement. They are also more convenient to use repeatedly - after implementation for a type they can be used for the same type in any class. Custom editors are implemented in MonoBehaviour, so they are harder to reuse and require more work.
29. By default, "seal" MonoBehaviours (use the sealed modifier). In general, MonoBehaviours Unity is not very convenient for inheritance:
- The way that Unity calls message methods such as Start and Update complicates how these methods work in subclasses. If you are not careful, the wrong element will be called, or you will forget to call the base method.
- When using custom editors, you usually need to copy the inheritance hierarchy for editors. If someone needs to expand one of your classes, you will need to create your own editor or limit yourself to what you created.
In cases where inheritance is necessary , do not use the Unity message methods if this can be avoided. If you still use them , do not make them virtual. If necessary, you can define an empty virtual function called from the message method, which the child class can override to perform additional actions.
public class MyBaseClass
{
public sealed void Update()
{
CustomUpdate();
... // update этого класса
}
//Вызывается до того, как этот класс выполняет свой update
//Переопределение для выполнения вашего кода update.
virtual public void CustomUpdate(){};
}
public class Child : MyBaseClass
{
override public void CustomUpdate()
{
//Выполняем какие-то действия
}
}This will prevent the class from overriding your code accidentally, but it still allows Unity messages to be enabled. I do not like this order, because it becomes problematic. In the example above, the child class may need to perform operations immediately after the class has completed its own update.
30. Separate the interface from the game logic.Interface components in general should not know anything about the game in which they are used. Pass them the data that you want to display, and subscribe to the events that are checked when the user interacts with the UI components. Interface components must not follow game logic. They can filter the input data, checking their correctness, but the basic rules should not be implemented in them. In many puzzle games, field elements are an extension of the interface, and should not contain rules. (For example, a chess piece should not calculate the moves allowed for it.)
The input information must also be separated from the logic acting on the basis of this information. Use an input controller that informs the actor about the need for movement, the actor decides when to move.
Here is a stripped down example of a UI component that allows the user to select a weapon from a given list. The only thing these classes know about the game is the Weapon class (and only because the Weapon class is a useful source of data that this container should display). The game also knows nothing about the container; she only needs to register the OnWeaponSelect event.
public WeaponSelector : MonoBehaviour
{
public event Action OnWeaponSelect {add; remove; }
//GameManager может регистрировать это событие
public void OnInit(List weapons)
{
foreach(var weapon in weapons)
{
var button = ... //Создаёт дочернюю кнопку и добавляет её в иерархию
buttonOnInit(weapon, () => OnSelect(weapon));
// дочерняя кнопка отображает опцию,
// и отправляет сообщение о нажатии этому компоненту
}
}
public void OnSelect(Weapon weapon)
{
if(OnWepaonSelect != null) OnWeponSelect(weapon);
}
}
public class WeaponButton : MonoBehaviour
{
private Action<> onClick;
public void OnInit(Weapon weapon, Action onClick)
{
... //установка спрайта и текста оружия
this.onClick = onClick;
}
public void OnClick() //Привязываем этот метод как OnClick компонента UI Button
{
Assert.IsTrue(onClick != null); //Не должно происходить
onClick();
}
}31. Separate configuration, status, and supporting information.
- Configuration variables are variables that are configured in the object to define the object through its properties. For example, maxHealth .
- State variables are variables that fully determine the current state of an object. These are variables that need to be saved if your game supports saving. For example, currentHealth .
- Bookkeeping variables are used for speed, convenience, and transition states. They can be entirely determined from state variables. For example, previousHealth .
Having separated these types of variables, you will understand that you can change, what needs to be stored, what needs to be sent / received over the network. Here is a simple example of such a separation.
public class Player
{
[Serializable]
public class PlayerConfigurationData
{
public float maxHealth;
}
[Serializable]
public class PlayerStateData
{
public float health;
}
public PlayerConfigurationData configuration;
private PlayerState stateData;
//вспомогательная информация
private float previousHealth;
public float Health
{
public get { return stateData.health; }
private set { stateData.health = value; }
}
}32. Do not use indexed arrays of type public. For example, do not define an array of weapons, an array of bullets, and an array of particles in this way:
public void SelectWeapon(int index)
{
currentWeaponIndex = index;
Player.SwitchWeapon(weapons[currentWeapon]);
}
public void Shoot()
{
Fire(bullets[currentWeapon]);
FireParticles(particles[currentWeapon]);
}The problem here is rather not in the code, but in the complexity of the error-free setup in the inspector.
Better define a class that encapsulates all three variables, and create an array from it:
[Serializable]
public class Weapon
{
public GameObject prefab;
public ParticleSystem particles;
public Bullet bullet;
}Such code looks nicer, but, more importantly, it is more difficult to make errors when setting up data in the inspector.
33. Avoid using arrays for non-sequence structures. For example, a player has three types of attacks. Each uses the current weapon, but generates different bullets and different behavior.
You can try stuffing three bullets into an array, and then use this type of logic:
public void FireAttack()
{
/// поведение
Fire(bullets[0]);
}
public void IceAttack()
{
/// поведение
Fire(bullets[1]);
}
public void WindAttack()
{
/// поведение
Fire(bullets[2]);
}Enums may look prettier in code ...
public void WindAttack()
{
/// behaviour
Fire(bullets[WeaponType.Wind]);
}... but not in the inspector.
It is better to use separate variables so that the names help you understand what content to write there. Create a class to make everything comfortable.
[Serializable]
public class Bullets
{
public Bullet fireBullet;
public Bullet iceBullet;
public Bullet windBullet;
}This implies that there is no other Fire, Ice, or Wind data.
34. Group the data into serializable classes so that everything looks more convenient in the inspector. Some items may have dozens of settings. Finding the right variable can be a nightmare. To simplify your life, follow these instructions:
- Define separate classes for groups of variables. Make them public and serializable
- In the main class, define public variables for each type defined above.
- Do not initialize these variables in Awake or Start; they are serializable, so Unity will take care of them itself.
- You can specify default values by assigning them in the definition.
This will create groups of variables that are easier to manage in the inspector.
[Serializable]
public class MovementProperties //Не MonoBehaviour!
{
public float movementSpeed;
public float turnSpeed = 1; //указываем значение по умолчанию
}
public class HealthProperties //Не MonoBehaviour!
{
public float maxHealth;
public float regenerationRate;
}
public class Player : MonoBehaviour
{
public MovementProperties movementProeprties;
public HealthPorperties healthProeprties;
}35. Make non-MonoBehavior classes Serializable, even if they are not used for public fields. This will allow you to view the class fields in the inspector when it is in Debug mode. This also works for nested classes (private or public).
36. Try not to change the data configured in the inspector in the code. The variable configured in the inspector is a configuration variable, and it must be treated as a constant when running the application, and not as a state variable. If you follow this rule, it will be easier for you to write methods that reset the state of the component to the original one, and you will clearly understand what the variable does.
public class Actor : MonoBehaviour
{
public float initialHealth = 100;
private float currentHealth;
public void Start()
{
ResetState();
}
private void Respawn()
{
ResetState();
}
private void ResetState()
{
currentHealth = initialHealth;
}
}Patterns
Patterns are ways to solve common problems using standard methods. Robert Nystrom ’s book, “Game Programming Patterns” (available for free online), is a valuable resource for understanding how patterns are applicable to solving problems that arise when developing games. There are many such patterns in Unity itself: Instantiate is an example of a prototype pattern; MonoBehaviour is a version of the “template method” pattern, the UI and animation uses the “observer” pattern, and the new animation engine uses state machines.
These tips relate to the use of patterns specifically in Unity.
37. Use singleton (“loner” pattern) for convenience. The following class will automatically make a singleton any class that inherits it:
public class Singleton : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
//Возвращает экземпляр этого синглтона.
public static T Instance
{
get
{
if(instance == null)
{
instance = (T) FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("В сцене нужен экземпляр " + typeof(T) +
", но он отсутствует.");
}
}
return instance;
}
}
} Singletones are useful for managers, such as ParticleManager , AudioManager, or GUIManager .
(Many programmers are opposed to classes, vaguely called XManager, because it indicates that the class name is bad or that it has too many tasks unrelated to each other. In general, I agree with them. However, there are only a few managers in games , and they perform the same tasks in games, so these classes are actually idioms.)
- Do not use singletones for unique instances of non-manager prefabs (e.g. Player). Adhere to this principle so as not to complicate the hierarchy of inheritance and the introduction of certain types of changes. Better store links to them in GameManager (or in a more suitable God class ;-)).
- Определите свойства static и методы для переменных и методов public, которые часто используются за пределами класса. Это позволит вам писать GameManager.Player вместо GameManager.Instance.player.
As explained in other tips, singletones are useful for creating default spawn points and objects transferred between scene downloads and storing global data.
38. Use state machines to create different behaviors in different states or to execute code when changing states. A light state machine has many states, and for each state you can specify the actions that are performed when you enter or are in a state, as well as the update action. This will make the code cleaner and less error prone. A good sign that the state machine is useful to you: the code of the Update method contains if or switch constructs that change its behavior, or variables like hasShownGameOverMessage.
public void Update()
{
if(health <= 0)
{
if(!hasShownGameOverMessage)
{
ShowGameOverMessage();
hasShownGameOverMessage = true; //При респауне значение становится false
}
}
else
{
HandleInput();
}
}With more states, this type of code can become confusing; the state machine will make it much clearer.
39. Use fields of type UnityEvent to create the observer pattern in the inspector. The UnityEvent class allows you to bind methods that receive up to four parameters in the inspector using the same UI as events in Buttons. This is especially useful when working with input.
40. Use the observer pattern to determine when a field value changes. The problem of executing code only when changing a variable often arises in games. We created a standard solution to this problem using the generic class, which allows registering events of variable changes. The following is a health example. Here is how it is created:
/*Наблюдаемое значение*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };Now you can change it anywhere without checking its value in every place, for example, like this:
if(hit) health.Value -= 10;When health drops below 0, the Die method is called. Detailed discussions and implementation see in this post .
41. Use the Actor pattern for prefabs. (This is a “non-standard” pattern. The basic idea is taken from the presentation of Kieran Lord.)
Actor is the main component of the prefab. Usually this is the component that provides the "individuality" of the prefab, and the one with which the higher level code will most often interact. Actor often uses other components - helpers - for the same object (and sometimes for child objects) to do its job.
When you create a button object through the Unity menu, a game object with Sprite and Button components (and a child with the Text component) is created. In this case, the actor will be Button. The main camera also usually has several components (GUI Layer, Flare Layer, Audio Listener) attached to the Camera component. Camera is an actor here.
For the actor to work properly, other components may be required. You can make a prefab more reliable and useful using the following attributes of an actor component:
- Use RequiredComponent to specify all the components that an actor needs in the same game object. (An actor can then always safely call GetComponent without having to check if the returned value is null.)
- Используйте DisallowMultipleComponent, чтобы избежать прикрепления нескольких экземпляров того же компонента. Актор всегда сможет вызвать GetComponent, не беспокоясь о поведении, которое должно быть, когда прикреплено несколько компонентов).
- Используйте SelectionBase, если у объекта-актора есть дочерние объекты. Так его будет проще выбрать в окне сцены.
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
...//
}42. Use random and patterned data stream generators. (This is a non-standard pattern, but we find it extremely useful.)
The generator is similar to a random number generator: it is an object with the Next method, called to get a new element of a certain type. During the design process, generators can be modified to create a wide range of patterns and various types of randomness. They are useful because they allow you to store the logic of generating a new element separately from the part of the code where the element is needed, which makes the code much cleaner.
Here are some examples:
var generator = Generator
.RamdomUniformInt(500)
.Select(x => 2*x); //Генерирует чётные числа от 0 до 998
var generator = Generator
.RandomUniformInt(1000)
.Where(n => n % 2 == 0); //Делает то же самое
var generator = Generator
.Iterate(0, 0, (m, n) => m + n); //Числа Фибоначчи
var generator = Generator
.RandomUniformInt(2)
.Select(n => 2*n - 1)
.Aggregate((m, n) => m + n); //Случайные скачки с шагом 1 или -1
var generator = Generator
.Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
.Where(n >= 0); //Случайная последовательность, увеличивающая среднееWe have already used generators to spawn obstacles, change background colors, procedural music, generate a sequence of letters to create words, as in word games and much more. Using the following design, generators can be successfully used to control coroutines that repeat at varying intervals:
while (true)
{
//Что-то делаем
yield return new WaitForSeconds(timeIntervalGenerator.Next());
}Read this post to learn more about generators.
Prefabs and Scriptable Objects
43. Use prefabs for everything. The only game objects in the scene that are not prefabs (or parts of prefabs) should be folders. Even unique objects that are used only once must be prefabs. This makes it easy to make changes that do not require a scene change.
44. Bind prefabs to prefabs; Do not bind instances to instances. Links to prefabs are saved when you drag the prefab into the scene, links to instances are not. Linking to prefabs where possible reduces the cost of setting up the scene and reduces the need for scene changes.
Wherever possible, establish connections between instances automatically. If you need to link instances, establish the links programmatically. For example, the prefab Player can register itself in the GameManager when it starts, or the GameManager can find the prefab Player when it starts.
45. Do not make grids with the roots of prefabs if you want to add other scripts. When creating a prefab from a grid, first make the grid parent an empty game object, and let it be the root. Bind scripts to the root, not to the grid. So it will be easier for you to replace the grid with another grid without losing the values configured in the inspector.
46. Use scriptable objects, not prefabs, for the transferred configuration data.
If you do so:
- the scenes will be smaller
- you cannot mistakenly make changes to one scene (to the prefab instance)
47. Use scriptable objects for these levels. Level data is often stored in XML or JSON, but using scripted objects instead has several advantages:
- They can be edited in the Editor. It will be easier to verify the data, and this method is more convenient for non-technical designers. Moreover, you can use custom editors to make editing even easier.
- You will not need to worry about reading / writing and parsing data.
- Separation and embedding, as well as managing the resulting assets will become easier. So you can create levels from building blocks, and not from a massive configuration.
48. Use scriptable objects to configure behavior in the inspector. Scripted objects are usually associated with configuration data, but they also allow you to use “methods” as data.
Consider a scenario in which you have an Enemy type, and each enemy has a set of SuperPowers. You can make them ordinary classes and get their list in the Enemy class, but without a custom editor you cannot configure the list of different superpowers (each with its own properties) in the inspector. But if you make these superpowers assets (implement them as ScriptableObjects), then you will succeed!
Here's how it works:
public class Enemy : MonoBehaviour
{
public SuperPower superPowers;
public UseRandomPower()
{
superPowers.RandomItem().UsePower(this);
}
}
public class BasePower : ScriptableObject
{
virtual void UsePower(Enemy self)
{
}
}
[CreateAssetMenu("BlowFire", "Blow Fire")
public class BlowFire : SuperPower
{
public strength;
override public void UsePower(Enemy self)
{
///программа использования суперсилы blow fire
}
}When using this pattern, do not forget about the following restrictions:
- Скриптуемые объекты не могут надёжно быть абстрактными. Вместо этого используйте конкретные базовые глассы и выдавайте NotImplementedExceptions в методах, которые должны быть абстрактными. Также можно определить атрибут Abstract и отметить им классы и методы, которые должны быть абстрактными.
- Generic скриптуемые объекты не могут быть сериализированы. Однако можно использовать generic базовые классы и сериализировать только подклассы, определяющие все generic.
49. Use scriptable objects to specialize prefabs. If the configuration of two objects differs only in some properties, then usually two instances are inserted into the scene and these properties are set in instances. It is usually better to create a separate class of properties, which may differ between the two types, a separate class of the scripted object.
This provides more flexibility:
- You can inherit from the specialization class to create specific properties for different types of objects.
- Setting the scene becomes much safer (you just select the desired scriptable object instead of setting all the properties to make the object of the desired type).
- During application execution, it is easier to manage these objects through code.
- If there are several instances of two types, you will be sure of the constancy of their properties when making changes.
- You can split sets of configuration variables into sets that can be mixed and matched.
Here is a simple example of such a setup.
[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
public float maxHealth;
public float resotrationRate;
}
public class Actor : MonoBehaviour
{
public HealthProperties healthProperties;
}With a large number of specializations, you can define a specialization as a regular class, and use their list in a scriptable object associated with a suitable place where you can apply it (for example, in GameManager ). To ensure its safety, speed and convenience, a little more “glue” is required; The following is an example of the smallest possible use.
public enum ActorType
{
Vampire, Wherewolf
}
[Serializable]
public class HealthProperties
{
public ActorType type;
public float maxHealth;
public float resotrationRate;
}
[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")]
public class ActorSpecialization : ScriptableObject
{
public List healthProperties;
public this[ActorType]
{
get { return healthProperties.First(p => p.type == type); } //Небезопасная версия!
}
}
public class GameManager : Singleton
{
public ActorSpecialization actorSpecialization;
...
}
public class Actor : MonoBehaviour
{
public ActorType type;
public float health;
//Пример использования
public Regenerate()
{
health
+= GameManager.Instance.actorSpecialization[type].resotrationRate;
}
}50. Use the CreateAssetMenu attribute to automatically add a ScriptableObject creation to the Asset / Create menu.
Debugging
51. Научитесь эффективно использовать инструменты отладки Unity.
- Добавляйте объекты context в конструкции Debug.Log, чтобы знать, где они генерируются.
- Используйте Debug.Break для паузы игры в редакторе (например, это полезно, когда вы хотите выполнить условия ошибки и вам нужно исследовать свойства компонента в этом кадре).
- Используйте функции Debug.DrawRay и Debug.DrawLine для визуальной отладки (например, DrawRay очень полезна при отладке причин «непопадания» ray cast).
- Используйте для визуальной отладки Gizmos. Можно также использовать gizmo renderer за пределами mono behaviours с помощью атрибута DrawGizmo.
- Используйте инспектор в режиме отладки (чтобы видеть с помощью инспектора значения полей private при выполнении приложения в Unity).
52. Learn how to efficiently use the debugger of your IDE. See, for example, Debugging Unity games in Visual Studio .
53. Use a visual debugger that draws graphs of changes in values over time. It is extremely convenient for debugging physics, animations, and other dynamic processes, and especially for irregularly occurring errors. You can see this error on the graph and track other variables that change at the time of the error. Also, visual inspection makes certain types of strange behavior obvious, such as values that are too often changed or deviations for no apparent reason. We use Monitor Components , but there are other visual debugging tools.
54. Use the convenient recording in the console.Use the editor extension, which allows color-coded output by category and filter the output according to these categories. We use Editor Console Pro , but there are other extensions.
55. Use Unity testing tools, especially for testing algorithms and math code. See, for example, the Unity Test Tools tutorial or the Unit testing at the speed of light with Unity Test Tools post .
56. Use Unity testing tools to run rough tests. Unity testing tools are not only suitable for formal tests. They can also be used for convenient “rough” tests, which are performed in the editor without starting a scene.
57. Use keyboard shortcuts to take screenshots. Many bugs are related to visual display, and it is much easier to report them if you can take a screenshot. An ideal system should have PlayerPrefs counters so that screenshots are not overwritten. Screenshots do not need to be saved in the project folder so that employees do not accidentally commit them in the repository.
58. Use keyboard shortcuts to print snapshots of important variables. They will allow you to register information when unexpected events that can be investigated occur during the game. The set of variables of course depends on the game. Typical errors that occur in the game can be tips for you. For example, the position of the player and enemies or the “state of thinking” of the AI actor (say, the way he is trying to follow).
59. Implement debugging options to simplify testing. Examples:
- Unlock all items.
- Disable enemies.
- Turn off the GUI.
- Make a player invulnerable.
- Disable the entire gameplay.
Be careful not to accidentally commit debugging options to the repository; changing these options can confuse other developers in the team.
60. Define the constants for the debug hotkeys, and store them in one place. Debug keys, unlike game input, are usually not processed in one place. To avoid hotkey conflicts, first of all define constants. An alternative is to process all the keys in one place, regardless of whether they have debugging features or not. (The disadvantage of this approach is that this class may only need additional object references for this).
61. For procedural mesh generation, draw or spawn small spheres at the vertices. This will allow you to make sure that the vertices are in the right places and they are the right size before you start working with triangles and UVs to display grids.
Performance
62. Be careful with general design and structure guidelines for performance.
- Often such tips are myth-based and not tested by tests.
- Sometimes recommendations are verified by tests, but tests are of poor quality.
- It happens that tips are tested with quality tests, but they are unrealistic or applicable in a different context. (For example, you can simply prove that using arrays is faster than generic lists. However, in the context of a real game, this difference is almost always insignificant. You can also add that if the tests were conducted on equipment other than the target devices for you, their results may be in your case are useless.)
- Sometimes the advice is right, but already outdated.
- Sometimes a recommendation is helpful. But there may be a need for a compromise: sometimes slow, but completed on time games are better than fast, but lagging. Highly optimized games may be more likely to contain tricky code that delays release.
- It’s useful to consider performance tips to find the sources of true problems faster than the process described above.
63. As soon as possible, begin to regularly test the game on target devices. Devices have different performance characteristics; don't let them give you surprises. The sooner you learn about problems, the more effectively you can solve them.
64. Learn how to use the profiler effectively to track the causes of performance problems.
- Если вы незнакомы с профайлингом, изучите Введение в профайлер.
- Научитесь определять собственные кадры (с помощью Profiler.BeginFrame и Profiler.EndFrame) для более точного анализа.
- Узнайте как использовать платформенный профайлинг, например, встроенный профайлер для iOS.
- Научитесь выполнять профайлинг в файл во встроенных проигрывателях и отображать данные в профайлере.
65. If necessary, use a third-party profiler for more accurate profiling. Sometimes the Unity profiler cannot provide a clear picture of what is happening: it may run out of profile frames, or deep profiling slows down the game so much that the test results do not make sense. In this case, we use our own profiler, but you can find alternative ones in the Asset Store.
66. Measure the effect of performance improvements. When making changes to improve performance, measure it to make sure that the change really improves performance. If the change is unmeasured or insignificant, discard it.
67. Do not write less readable code to improve performance. Exceptions:
- You have a problem found in the code by the profiler, you measured the improvement after the change, and the improvement is so good that it’s worth reducing the usability of the support.
OR - You know exactly what you are doing.
Naming Standard and Folder Structure
68. Follow the documented naming convention and folder structure. Thanks to the standardized naming and folder structure, it’s easier to search for objects and understand them.
Most likely, you will want to create your own naming convention and folder structure. Here is one for an example.
General principles for naming
- Call a spade a spade. The bird should be called Bird.
- Выбирайте имена, которые можно произнести и запомнить. Если вы делаете игру про майя, не называйте уровень QuetzalcoatisReturn (ВозвращениеКетцалкоатля).
- Поддерживайте постоянство. Если вы выбрали имя, придерживайтесь его. Не называйте что-то buttonHolder в одном случае и buttonContainer в другом.
- Используйте Pascal case, например: ComplicatedVerySpecificObject. Не используйте пробелы, символы подчёркивания или дефисы, с одним исключением (см. раздел «Присвоение имён для различных аспектов одного элемента»).
- Не используйте номера версий или слова для обозначения степени выполнения (WIP, final).
- Не используйте аббревиатуры: DVamp@W должен называться DarkVampire@Walk.
- Используйте терминологию дизайн-документа: если в документе анимация смерти называется Die, то используйте DarkVampire@Die, а не DarkVampire@Death.
- Оставляйте наиболее конкретное описание слева: DarkVampire, а не VampireDark; PauseButton, а не ButtonPaused. Например, будет проще найти кнопку паузы в инспекторе, если не все названия кнопок начинаются со слова Button. [Многие предпочитают обратный принцип, потому что так группировка визуально выглядит более очевидной. Однако имена, в отличие от папок, не предназначены для группировки. Имена нужны для различения объектов одного типа, чтобы можно было находить их быстро и просто.]
- Некоторые имена образуют последовательности. Используйте в этих именах числа, например, PathNode0, PathNode1. Всегда начинайте нумерацию с 0, а не с 1.
- Не используйте числа для элементов, не образующих последовательность. Например, Bird0, Bird1, Bird2 должны называться Flamingo, Eagle, Swallow.
Присвоение имён для различных аспектов одного элемента
Use underscores between the main name and the part that describes the “aspect” of the element. For instance:
- GUI Button States EnterButton_Active, EnterButton_Inactive
- Textures DarkVampire_Diffuse, DarkVampire_Normalmap
- Skyboxes JungleSky_Top, JungleSky_North
- LOD groups DarkVampire_LOD0, DarkVampire_LOD1
Do not use this convention to distinguish between different types of elements, for example, Rock_Small, Rock_Large should be called SmallRock, LargeRock.
Structure
The scene diagram, project folder, and script folder should have a similar template. Below are examples that can be used.
Folder structure
MyGame
Helper
Design
Scratchpad
Materials
Meshes
Actors
DarkVampire
LightVampire
…
Structures
Buildings
…
Props
Plants
…
…
Resources
Actors
Items
…
Prefabs
Actors
Items
…
Scenes
Menus
Levels
Scripts
Tests
Textures
UI
Effects
…
UI
MyLibray
…
Plugins
SomeOtherAsset1
SomeOtherAsset2
...
Структура сцены
Main
Debug
Managers
Cameras
Lights
UI
Canvas
HUD
PauseMenu
...
World
Ground
Props
Structures
...
Gameplay
Actors
Items
...
Dynamic Objects
Script Folder Structure
Debug
Gameplay
Actors
Items
...
Framework
Graphics
UI
...