
Static classes
This approach involves creating a class that is globally accessible to the entire code base at anytime. Any kind of global manager class is often frowned upon in software engineering circles, partly since the name manager is vague and doesn't say much about what it's meant to do, but mostly because problems can be difficult to debug. Changes can occur from anywhere and at any point during runtime, and such classes tend to maintain state information that other systems rely upon. It is also perhaps the most difficult approach to change or replace since many of our classes might contain direct function calls into it, requiring each to be modified at a future date if it were to be replaced. Despite all of these drawbacks, it is by far the easiest solution to understand and implement.
The singleton design pattern is a common way of ensuring only one instance of a certain object type ever exists in memory. This design pattern is implemented by giving the class a private constructor, a static variable is maintained to keep track of the object instance, and the class can only be accessed through a static property it provides. Singletons can be useful for managing shared resources or heavy data traffic, such as file access, downloads, data parsing, and messaging. A singleton ensures that we have a single entry point for such activities, rather than having tons of different subsystems competing for shared resources and potentially bottlenecking one another.
Singletons don't necessarily have to be globally accessible objects—their most important feature is that only a single instance of the object exists at a time. However, the way that singletons are primarily used in most projects is to be a global access point to some shared functionality, and they are designed to be created once during application initialization, persist through the entire lifecycle of the application, and only be destroyed during application shutdown. As such, a simpler way of implementing this kind of behavior in C# is to use a static class. In other words, implementing the typical singleton design pattern in C# just provides the same behavior as a static class, but takes more time and code to implement.
A static class that functions in much the same way as EnemyManagerComponent, as demonstrated in the previous example, can be defined as follows:
using System.Collections.Generic;
using UnityEngine;
public static class StaticEnemyManager {
private static List<Enemy> _enemies;
public static void CreateEnemy(GameObject prefab) {
string[] names = { "Tom", "Dick", "Harry" };
GameObject enemy = GameObject.Instantiate(prefab, 5.0f *
Random.insideUnitSphere, Quaternion.identity);
Enemy enemyComp = enemy.GetComponent<Enemy>();
enemy.gameObject.name = names[Random.Range(0, names.Length)];
_enemies.Add(enemyComp);
}
public static void KillAll() {
for (int i = 0; i < _enemies.Count; ++i) {
_enemies[i].Die();
GameObject.Destroy(_enemies[i].gameObject);
}
_enemies.Clear();
}
}
Note that every method, property, and field in a static class must have the static keyword attached, which implies that only one instance of this object will ever reside in memory. This also means that its public methods and fields are accessible from anywhere. Static classes, by definition, do not allow any non-static fields to be defined.
If static class fields need to be initialized (such as the _enemies field, which is initially set to null), then static class fields can be initialized inline like so:
private static List<Enemy> _enemies = new List<Enemy>();
However, if object construction needs to be more complicated than this, then static classes can be given a static constructor, instead. The static class constructor is automatically called the moment the class is first accessed through any of its fields, properties, or methods and can be defined like so:
static StaticEnemyManager() {
_enemies = new List<Enemy>();
// more complicated initialization activity goes here
}
This time, we have implemented the CreateEnemy() method so that it handles much of the activity for creating an enemy object. However, the static class must still be given a reference to a Prefab from which it can instantiate an enemy object. A static class can only contain static member variables, and therefore cannot easily interface with the Inspector window in the same way that MonoBehaviours can, therefore requiring the caller to provide some implementation-specific information to it. To solve this problem, we could implement a companion-component for our static class to keep our code properly decoupled. The following code demonstrates what this class might look like:
using UnityEngine;
public class EnemyCreatorCompanionComponent : MonoBehaviour {
[SerializeField] private GameObject _enemyPrefab;
public void CreateEnemy() {
StaticEnemyManager.CreateEnemy(_enemyPrefab);
}
}
Despite these drawbacks, the StaticEnemyManager class illustrates a simple example of how a static class might be used to provide information or communication between external objects, providing a better alternative than using Find() or SendMessage().