The singleton pattern is widely used in game development. Its core promise is simple: it guarantees that exactly one instance of a given type exists and can be accessed globally.
A typical skeleton of a Unity-style singleton looks like this:
public class Singleton<T>
{
private static T s_instance;
public static T Instance
{
get
{
if (s_instance == null)
{
var obj = new GameObject();
s_instance = obj.AddComponent<T>();
}
return s_instance;
}
}
protected virtual void Awake()
{
InitializeSingleton();
}
private void InitializeSingleton()
{
if (s_instance == null)
{
s_instance = this as T;
DontDestroyOnLoad(gameObject);
enabled = true;
}
else if (s_instance != this)
{
enabled = false;
gameObject.SetActive(false);
Destroy(gameObject);
}
}
}
By using a static field, this pattern…
The singleton pattern is widely used in game development. Its core promise is simple: it guarantees that exactly one instance of a given type exists and can be accessed globally.
A typical skeleton of a Unity-style singleton looks like this:
public class Singleton<T>
{
private static T s_instance;
public static T Instance
{
get
{
if (s_instance == null)
{
var obj = new GameObject();
s_instance = obj.AddComponent<T>();
}
return s_instance;
}
}
protected virtual void Awake()
{
InitializeSingleton();
}
private void InitializeSingleton()
{
if (s_instance == null)
{
s_instance = this as T;
DontDestroyOnLoad(gameObject);
enabled = true;
}
else if (s_instance != this)
{
enabled = false;
gameObject.SetActive(false);
Destroy(gameObject);
}
}
}
By using a static field, this pattern ensures that Singleton<T>.Instance always points to the same object. This property fits many game-wide systems very well. Common examples include InventorySystem in an RPG, ScoreSystem in a tower defense game, or a backend service such as CloudSyncService that is responsible for communicating with remote databases. In these cases, having more than one instance would cause ambiguity and could easily break normal game flow.
Singletons are often introduced because they appear to reduce code complexity. Consider a tower defense game with a ScoreSystem that tracks points and a CurrencySystem that manages player balances. Suppose an Enemy class is responsible for requesting score and currency rewards when it dies.
If ScoreSystem and CurrencySystem are not singletons, the Enemy class might look like this:
public class Enemy
{
private ScoreSystem _scoreSystem;
private CurrencySystem _currencySystem;
...
public void Initialize(
ScoreSystem scoreSystem,
CurrencySystem currencySystem,
...
)
{
_scoreSystem = scoreSystem;
_currencySystem = currencySystem;
...
}
private void OnDeath()
{
_scoreSystem.AddScore(score);
_currencySystem.AddCurrency(worth);
}
}
This design requires an explicit initialization step performed by some other class, as well as fields that store references to the relevant system instances. As the number of systems grows, this wiring logic becomes increasingly fragile and difficult to manage.
If ScoreSystem and CurrencySystem are singletons, the Enemy class can be simplified:
public class Enemy
{
private void OnDeath()
{
Singleton<ScoreSystem>.Instance.AddScore(score);
Singleton<CurrencySystem>.Instance.AddCurrency(worth);
}
}
No explicit initialization is required, because Singleton<T>.Instance is globally accessible from anywhere.
That said, singletons do not eliminate tight coupling between classes. In both versions of the example above, Enemy still depends directly on ScoreSystem and CurrencySystem. For small projects, this hard dependency is often acceptable. In larger projects, however, it can quickly grow into an unmanageable web of implicit connections.
Another important limitation is initialization. Like any other class, a singleton often requires setup beyond its basic lifetime enforcement. For example, InventorySystem must load save data and reconstruct the player’s items from the previous session. This initialization is essential for correct gameplay logic and must be completed before any other system attempts to access Singleton<InventorySystem>.Instance. Failing to control this initialization order can lead to subtle and difficult bugs.
In future posts, I will discuss how to mitigate these limitations and how to build a cleaner, more scalable infrastructure on top of singletons.