Your game runs fine. Then you try to add a pause menu, and suddenly you are wiring the same reference through six scripts, your enemy code somehow imports your UI code, and changing one sound call means touching ten files. That is not a gameplay problem. It is a game architecture problem, and the pattern you pick for how your systems find and talk to each other decides how much pain every future feature costs. This guide walks through the three architecture approaches most game developers actually use, with simple parallel examples in C# for Unity and GDScript for Godot, so you can see exactly what each one looks like and choose the right one for your project.
Key Takeaways
- Singleton managers: one global instance per system (GameManager, SoundManager, InputManager). Easiest to write, fastest to wire up, but they spread global state and get hard to change later.
- Service locator / injection: a single Root registers services (SoundService, InputService, DataService) that the rest of the game asks for through an interface. More setup, far easier to swap and test.
- Event bus / signals: systems fire events and react to them without ever referencing each other. The most decoupled option, ideal for score, UI, and achievements, but harder to trace.
- No single winner: small jam games are fine with singletons, mid-size projects benefit from a service locator, and event buses shine wherever many systems react to the same moment.
- Most real games mix all three: a couple of singletons for truly unique systems, a service locator for swappable ones, and an event bus for broadcast moments.
Why Game Architecture Matters
Architecture is about one question: when system A needs system B, how does it find it? The answer you pick controls coupling, which is how tightly your systems depend on each other. Tightly coupled code is fast to write and miserable to change. Loosely coupled code takes more thought up front and saves you from rewrites later.
The three approaches below sit on a spectrum from most coupled to least. Singletons give global access with the tightest coupling, a service locator decouples through interfaces, and an event bus removes direct references entirely. The same four systems show up in every example so you can compare them directly: a game manager, a sound system, an input system, and a data system.
Approach 1: Singleton Managers
A singleton is a class with exactly one instance and a global access point. This is the pattern behind the classic GameManager, SoundManager, InputManager, and DataManager setup. Any script, anywhere, can call SoundManager.Instance.PlaySound() without holding a reference. That convenience is the whole appeal.

Singleton in C# (Unity)
public class SoundManager : MonoBehaviour
{
public static SoundManager Instance { get; private set; }
[SerializeField] private AudioSource _source;
private void Awake()
{
// Enforce a single instance
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
public void PlaySound(AudioClip clip)
{
_source.PlayOneShot(clip);
}
}
// Anywhere in the project:
SoundManager.Instance.PlaySound(jumpClip);
The Awake method sets the static Instance and destroys any duplicate that appears, so the global access point always points at one object. DontDestroyOnLoad keeps it alive across scene changes.
Singleton in GDScript (Godot)
Godot has singletons built in through the Autoload system, so you write less boilerplate. Create sound_manager.gd, then register it under Project Settings, Globals, Autoload with the name SoundManager. Godot loads it before any scene and makes it globally accessible by that name.
# sound_manager.gd (registered as Autoload "SoundManager")
extends Node
var _player: AudioStreamPlayer
func _ready() -> void:
_player = AudioStreamPlayer.new()
add_child(_player)
func play_sound(stream: AudioStream) -> void:
_player.stream = stream
_player.play()
# Anywhere in the project:
SoundManager.play_sound(jump_sound)
⚡ Quick tip: Never call free() or queue_free() on a Godot Autoload. The engine manages its lifetime, and freeing it manually will crash your game.
Use Singletons When
- You are in a game jam or small project and want to move fast.
- The system is genuinely unique and global, like an audio manager.
- You want zero wiring to reach a system from anywhere.
Watch Out For
- Hidden global state that makes code hard to reason about.
- Tight coupling, so changing one manager can break many callers.
- Hard unit testing, since you cannot easily swap in a fake.
- Assuming one of something, like a single player, that you later need two of.
Approach 2: Service Locator and Injection
A service locator keeps the convenience of global access but hides each system behind an interface. Instead of calling a concrete SoundManager, your code asks a central registry for an ISoundService and gets back whatever was registered. A single startup object, often called Root, registers the real services once. The rest of the game never knows which concrete class it is talking to.
The payoff is that you can swap implementations without touching call sites. Register a SilentSoundService during tests, or a ConsoleSoundService on a platform without audio, and every caller keeps working.
Service Locator in C# (Unity)
public interface ISoundService
{
void PlaySound(AudioClip clip);
}
public class SoundService : ISoundService
{
private readonly AudioSource _source;
public SoundService(AudioSource source) => _source = source;
public void PlaySound(AudioClip clip) => _source.PlayOneShot(clip);
}
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> _services = new();
public static void Register<T>(T service) => _services[typeof(T)] = service;
public static T Get<T>() => (T)_services[typeof(T)];
}
// Root registers services once at startup:
ServiceLocator.Register<ISoundService>(new SoundService(audioSource));
// Any system asks for the interface:
ServiceLocator.Get<ISoundService>().PlaySound(jumpClip);
Service Locator in GDScript (Godot)
In Godot, a single Autoload named Services can act as the Root. It builds each service in _ready and exposes them as properties. Systems reach them through Services.sound, Services.input, and Services.data, so the wiring lives in exactly one place.
# services.gd (registered as Autoload "Services")
extends Node
var sound: SoundService
var input: InputService
var data: DataService
func _ready() -> void:
sound = SoundService.new()
input = InputService.new()
data = DataService.new()
add_child(sound)
add_child(input)
add_child(data)
# Any system asks the Root:
Services.sound.play_sound(jump_sound)
This is also the cleanest home for a data system. If you are building one, our game data manager guide walks through a DataService that centralizes saving, loading, and runtime state, which slots straight into this Root.

Use a Service Locator When
- Your project is past the prototype stage and growing.
- You want to swap implementations for tests or different platforms.
- You like global access but want interfaces instead of concrete types.
Watch Out For
- Temporal coupling: a service must be registered before anyone uses it.
- Hidden dependencies, since reading a class does not reveal what it needs.
- It is still global access, so discipline matters.
Approach 3: Event Bus and Signals
An event bus flips the relationship around. Instead of system A finding system B and calling it, system A announces that something happened, and any system that cares reacts. The publisher never holds a reference to the subscriber. This is the observer pattern, and it produces the loosest coupling of the three.
It shines for broadcast moments. When an enemy dies, the score system, the UI, the achievement tracker, and the audio system might all need to react. With a singleton you would call each one by hand. With an event bus, the enemy fires one event and walks away.
Event Bus in C# (Unity)
using System;
public static class EventBus
{
public static event Action<int> EnemyDied;
public static void RaiseEnemyDied(int points) => EnemyDied?.Invoke(points);
}
// Publisher: the enemy announces, then forgets
public class Enemy : MonoBehaviour
{
private void Die() => EventBus.RaiseEnemyDied(10);
}
// Subscriber: the score system reacts
public class ScoreManager : MonoBehaviour
{
private int _score;
private void OnEnable() => EventBus.EnemyDied += AddScore;
private void OnDisable() => EventBus.EnemyDied -= AddScore;
private void AddScore(int points) => _score += points;
}
The key discipline is subscribing in OnEnable and unsubscribing in OnDisable. Forgetting to unsubscribe is the most common event bus bug, because a destroyed object that is still subscribed causes errors or memory leaks.
Event Bus in GDScript (Godot)
Godot has this pattern built in through signals. Put a global signal on an Autoload named Events, and any node can emit it or connect to it. This is the idiomatic Godot way to decouple systems, and it needs no custom bus code at all.
# events.gd (registered as Autoload "Events")
extends Node
signal enemy_died(points: int)
# Publisher: the enemy emits, then forgets
func die() -> void:
Events.enemy_died.emit(10)
# Subscriber: the score system connects in _ready
var score := 0
func _ready() -> void:
Events.enemy_died.connect(_on_enemy_died)
func _on_enemy_died(points: int) -> void:
score += points
Use an Event Bus When
- Many systems need to react to the same moment.
- You want publishers that know nothing about their listeners.
- You are wiring up score, UI updates, achievements, or analytics.
Watch Out For
- Hard-to-trace flow, since you cannot click through to see who reacts.
- Forgotten unsubscribes that cause leaks or errors after objects die.
- Overuse, which turns simple direct calls into invisible spaghetti.
Honorable Mentions: DI Frameworks and ECS
Two more approaches are worth knowing once the three above feel comfortable. Dependency injection frameworks like Zenject or VContainer for Unity automate the wiring a service locator does by hand, passing dependencies into constructors so nothing reaches for a global. They add power and a learning curve, and they suit larger teams.
Entity Component System (ECS) is a different way of thinking entirely. Instead of objects that own their behavior, you store data in components and run logic in systems that process them in bulk. Unity has its DOTS and Entities packages for this, and it is built for performance at scale. It is a bigger commitment than a coupling pattern, so reach for it when you have thousands of objects to update, not as your first architecture.
Which Approach Should You Use?
| Approach | Coupling | Testability | Best For | Main Risk |
|---|---|---|---|---|
| Singleton | High | Hard | Jam games, prototypes, truly unique systems | Global state that is hard to change |
| Service Locator | Medium | Good | Growing projects, swappable systems | Hidden dependencies, register-before-use |
| Event Bus | Low | Good | Broadcast moments: score, UI, achievements | Flow is hard to trace |
The honest answer is that most shipped games use all three. A couple of singletons handle the truly unique systems, a service locator manages the swappable ones, and an event bus carries the broadcast moments. Start simple. A jam game with three singletons is completely fine. Reach for a service locator when wiring starts to hurt, and add an event bus the first time you find yourself calling four systems by hand after one event.
If you are still choosing between languages for any of this, our breakdown of GDScript vs C# in Godot covers the trade-offs, and the complete game development roadmap shows where architecture fits in the bigger picture of finishing a game.
Gear for Long Coding Sessions
Refactoring an architecture is the kind of work that eats whole afternoons. A keyboard that is comfortable for thousands of keystrokes and a monitor with room for code plus a running game both pull their weight here. These are the picks most /SKILL readers code on, with live prices as of June 2026.
Akko 5075 V3
A gasket-mount board that feels great for long typing sessions, currently on a limited-time deal.
Glorious GMMK 2 TKL
Hot-swappable switches so you can tune the feel without soldering.
BenQ RD280U 28.2″ 4K
A 3:2 programming monitor with extra vertical space for long files and stack traces.
Between coding sessions, Berry Finds tracks real-time Amazon deals on thousands of everyday products across home, kitchen, beauty, and more so you never overpay on the stuff you buy regularly.
Frequently Asked Questions
What is the best architecture pattern for a small game?
For a small game or game jam, singleton managers are usually the right call. A GameManager, SoundManager, and InputManager are quick to write and easy to reach from anywhere. The downsides of singletons mostly bite on larger projects, so the trade-off favors speed when the scope is small.
Are singletons bad in game development?
Singletons are not bad, they are overused. They spread global state and tight coupling, which makes large projects hard to change and test. Used sparingly for genuinely unique systems like audio, they are perfectly fine. The problem starts when most of your codebase reaches for them directly.
What is the difference between a singleton and a service locator?
A singleton couples your code to one concrete class with a global Instance. A service locator hides each system behind an interface and lets a central registry return whichever implementation was registered. The locator costs more setup but lets you swap implementations for tests or different platforms without changing call sites.
How does Godot handle singletons and event buses?
Godot uses the Autoload system for singletons. You register a script under Project Settings, Globals, Autoload and access it by name from anywhere. For an event bus, you put global signals on an Autoload and emit or connect to them, which is the idiomatic Godot way to decouple systems without custom bus code.
Can I use more than one architecture pattern in the same game?
Yes, and most games do. A typical setup uses a few singletons for unique systems, a service locator for swappable ones, and an event bus for broadcast moments like an enemy dying. Mixing patterns by purpose is normal and usually better than forcing one pattern everywhere.
Summary
Game architecture comes down to how your systems find and talk to each other. Singleton managers give the fastest global access with the tightest coupling, a service locator trades a little setup for swappable, testable systems behind interfaces, and an event bus removes direct references so many systems can react to one moment. The C# and GDScript examples above are deliberately small so you can drop them into a project and feel the difference.
Pick by project size and need rather than dogma. Start with singletons, move swappable systems behind a service locator as the project grows, and lean on an event bus or Godot signals wherever a single event needs to reach a crowd. For more hands-on builds, our RTS combat system guide and Godot 4 card game guide show these patterns at work in real systems.