Assigning references to pre-existing objects

A simple approach to the problem of inter-object communication is to use Unity's built-in serialization system. Software design purists tend to get a little combative about this feature since it breaks encapsulation; it makes any field (the C# term for a member variable) marked private act in a way that treats it like a public field. However, it is a very effective tool for improving development workflow. This is particularly true when artists, designers, and programmers are all tinkering when the same product, where each has wildly varying levels of computer science and software programming knowledge, and with some of whom would prefer to stay away from modifying code files. Sometimes, it's worth bending a few rules in the name of productivity.

Whenever we create a public field in MonoBehaviour, Unity automatically serializes and exposes the value in the Inspector window when the component is selected. However, public fields are always dangerous from a software design perspective. These variables can be changed through code at anytime from anywhere, making it hard to keep track of the variable and it's liable to introduce a lot of unexpected bugs.

A better solution is to take any private or protected member variable of a class and expose it to the Inspector window with the [SerializeField] attribute. The value will then behave like a public field with respect to the Inspector window, allowing us to change it through the Editor interface for convenience, but will keep the data safely encapsulated from other parts of our code base. 

For example, the following class exposes three private fields to the Inspector window:

using UnityEngine;

public class EnemyCreatorComponent : MonoBehaviour {
[SerializeField] private int _numEnemies;
[SerializeField] private GameObject _enemyPrefab;
[SerializeField] private EnemyManagerComponent _enemyManager;

void Start() {
for (int i = 0; i < _numEnemies; ++i) {
CreateEnemy();
}
}

public void CreateEnemy() {
_enemyManager.CreateEnemy(_enemyPrefab);
}
}
Note that the   private  access specifiers shown in the preceding code are redundant keywords in C# since fields and methods default to   private  unless specified otherwise. However, it is often best practice to be explicit about the intended access level.

Looking at this component in the Inspector window reveals three values, initially given default values of 0 or null, which can be modified through the Editor interface:

We can drag and drop a Prefab reference from the Project window into the Enemy Prefab field revealed in the Inspector window.

Note how Unity automatically takes a camel-cased field name and creates a convenient Inspector window name for it.   _numEnemies  becomes   Num Enemies,   _enemyPrefab  becomes   Enemy Prefab, and so on.

Meanwhile, the _enemyManager field is interesting because it is a reference to a specific MonoBehaviour class type. If a GameObject is dragged and dropped into this field, then it will refer to the component on the given object as opposed to the GameObject itself. Note that if GameObject does not contain the expected MonoBehaviour, instance, then nothing will be assigned to the field.

A common usage of this component reference technique is to obtain references to other components attached to the very same GameObject a component is attached to. This is an alternative means of caching components with zero cost, as discussed in the section entitled Cache component references, earlier in this chapter.

There is some danger to using this method. Much of our code would assume that a Prefab is assigned to a field that is used like a Prefab and GameObject is assigned to a field that refers to an instance of GameObject. However, since Prefabs are essentially GameObjects, any Prefab or GameObject can be assigned to a serialized GameObject reference field, which means we could assign the wrong type by accident.

If we do assign the wrong type, then we could accidentally instantiate a new GameObject instance from an existing one that was previously modified, or we could make changes to a Prefab, which would then change the state of all GameObjects instantiated from it. To make matters worse, any accidental changes to a Prefab become permanent since Prefabs occupy the same memory space whether Playmode is active or not. This is the case even if the Prefab is only modified during Playmode.

Therefore, this approach is a very team-friendly way of solving the problem of inter-object communication, but it is not ideal due to all of the risks involved with team members accidentally leaving null references in place, assigning Prefabs to references that expect an instance of GameObject from the scene or vice versa.

It is also important to note that not all objects can be serialized and revealed in the Inspector window. Unity can serialize all primitive data types (int, float, string, and bool), various built-in types (Vector3, Quaternion, and so on), enum, class, struct, and various data structures containing other serializable types such as List. However, it is unable to serialize static fields, readonly fields, properties, and dictionaries.

Some Unity developers like to implement the pseudo-serialization of dictionaries via two separate lists for keys and values, along with a Custom Editor script, or via a single list of   struct objects, which contain both keys and values. Both of these solutions are a little clumsy, and are rarely as performant as a proper dictionary, but they can still be useful.

Another solution to the problem of inter-object communication is to try and make use of globally accessible objects to minimize the number of custom assignments we need to make.