Architecture
Game Architecture Patterns
Choosing the right architecture early saves hours of refactoring later. This guide covers proven patterns used in MonoGame projects of all sizes, from game jams to commercial releases.
Download the runnable sample project
Use the verified ZIP bundle for this article to review the extracted example files, run the sample host, and repackage the sources without bin or obj folders.
Project structure
A well-organised folder layout makes code easy to navigate as your project grows. Group by feature or system rather than by file type.
MyGame/
|-- Content/ (MGCB content project)
| |-- Textures/
| |-- Audio/
| |-- Fonts/
| `-- Content.mgcb
|-- Core/ (engine-level code)
| |-- GameMain.cs (the Game subclass)
| |-- SceneManager.cs
| |-- ServiceLocator.cs
| `-- InputManager.cs
|-- Scenes/
| |-- MenuScene.cs
| |-- PlayScene.cs
| `-- PauseScene.cs
|-- Entities/
| |-- Player.cs
| |-- Enemy.cs
| `-- Projectile.cs
|-- Components/
| |-- TransformComponent.cs
| |-- SpriteComponent.cs
| `-- ColliderComponent.cs
|-- Systems/
| |-- MovementSystem.cs
| |-- RenderSystem.cs
| `-- CollisionSystem.cs
`-- Program.cs
Understanding the game loop
MonoGame calls Update and Draw in a fixed or variable timestep loop. By default, IsFixedTimeStep is true, targeting 60 updates per second. Use gameTime.ElapsedGameTime to make movement frame-rate independent.
public GameMain()
{
_graphics = new GraphicsDeviceManager(this);
// Fixed timestep (default): Update called 60 times/sec
IsFixedTimeStep = true;
TargetElapsedTime = TimeSpan.FromSeconds(1.0 / 60.0);
// Variable timestep: Update called as fast as possible
// IsFixedTimeStep = false;
}
protected override void Update(GameTime gameTime)
{
// Always multiply by delta time for consistent movement
float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
_position += _velocity * dt;
base.Update(gameTime);
}
Scene management
Scenes (also called screens or levels) encapsulate a distinct phase of your game. A scene manager handles loading, updating, drawing, and transitioning between scenes.
public abstract class Scene
{
protected Game Game { get; }
protected Scene(Game game) => Game = game;
public virtual void LoadContent() { }
public virtual void UnloadContent() { }
public abstract void Update(GameTime gameTime);
public abstract void Draw(SpriteBatch spriteBatch);
}
public class SceneManager
{
private Scene _current;
private Scene _next;
public void SetScene(Scene scene)
{
_next = scene;
}
public void Update(GameTime gameTime)
{
if (_next != null)
{
_current?.UnloadContent();
_current = _next;
_current.LoadContent();
_next = null;
}
_current?.Update(gameTime);
}
public void Draw(SpriteBatch spriteBatch)
{
_current?.Draw(spriteBatch);
}
}
// Usage in Game1:
// _sceneManager.SetScene(new PlayScene(this));
// _sceneManager.Update(gameTime);
// _sceneManager.Draw(_spriteBatch);
Finite state machines
FSMs model behaviour with explicit states and transitions. Use them for AI, animation controllers, and game flow. Keep states small and transitions explicit.
public class StateMachine<T>
{
private readonly Dictionary<T, Action<GameTime>> _updateActions = new();
private readonly Dictionary<T, Action> _enterActions = new();
private readonly Dictionary<T, Action> _exitActions = new();
public T CurrentState { get; private set; }
public void AddState(T state, Action onEnter = null,
Action<GameTime> onUpdate = null, Action onExit = null)
{
if (onEnter != null) _enterActions[state] = onEnter;
if (onUpdate != null) _updateActions[state] = onUpdate;
if (onExit != null) _exitActions[state] = onExit;
}
public void SetState(T newState)
{
if (_exitActions.TryGetValue(CurrentState, out var exit))
exit();
CurrentState = newState;
if (_enterActions.TryGetValue(CurrentState, out var enter))
enter();
}
public void Update(GameTime gameTime)
{
if (_updateActions.TryGetValue(CurrentState, out var update))
update(gameTime);
}
}
// Example:
// var fsm = new StateMachine<PlayerState>();
// fsm.AddState(PlayerState.Idle, onUpdate: IdleUpdate);
// fsm.AddState(PlayerState.Walking, onEnter: StartWalk, onUpdate: WalkUpdate);
// fsm.SetState(PlayerState.Idle);
Component pattern
Instead of deep inheritance trees, attach reusable behaviour components to game objects. Each component handles one concern (rendering, physics, input). The game object delegates Update and Draw to its components.
public abstract class Component
{
public GameObject Owner { get; set; }
public virtual void Update(GameTime gameTime) { }
public virtual void Draw(SpriteBatch spriteBatch) { }
}
public class GameObject
{
public Vector2 Position;
private readonly List<Component> _components = new();
public T AddComponent<T>(T component) where T : Component
{
component.Owner = this;
_components.Add(component);
return component;
}
public T GetComponent<T>() where T : Component
=> _components.OfType<T>().FirstOrDefault();
public void Update(GameTime gt)
{
foreach (var c in _components) c.Update(gt);
}
public void Draw(SpriteBatch sb)
{
foreach (var c in _components) c.Draw(sb);
}
}
// Example components:
public class SpriteRenderer : Component
{
public Texture2D Texture;
public override void Draw(SpriteBatch sb)
=> sb.Draw(Texture, Owner.Position, Color.White);
}
public class KeyboardMover : Component
{
public float Speed = 200f;
public override void Update(GameTime gt)
{
float dt = (float)gt.ElapsedGameTime.TotalSeconds;
var kb = Keyboard.GetState();
if (kb.IsKeyDown(Keys.W)) Owner.Position.Y -= Speed * dt;
if (kb.IsKeyDown(Keys.S)) Owner.Position.Y += Speed * dt;
if (kb.IsKeyDown(Keys.A)) Owner.Position.X -= Speed * dt;
if (kb.IsKeyDown(Keys.D)) Owner.Position.X += Speed * dt;
}
}
Entity-Component-System (ECS)
ECS takes the component pattern further by separating data from logic entirely. Entities are just IDs, components are plain data structs, and systems process all entities with matching components. See the full implementation in the 2D tutorials.
When to use ECS
- Large numbers of similar entities (bullets, particles, NPCs).
- You want cache-friendly data layouts for performance.
- Behaviour needs to be composed from many independent systems.
When to avoid ECS
- Small projects or game jams where simplicity matters more.
- The overhead of the architecture exceeds the benefit.
Service locator
MonoGame’s Game.Services is a built-in service locator. Register shared services (input, audio, camera) and retrieve them anywhere without tight coupling.
// Register in Initialize or LoadContent
Services.AddService<IInputManager>(new InputManager());
Services.AddService<IAudioManager>(new AudioManager());
// Retrieve anywhere that has access to Game
var input = Game.Services.GetService<IInputManager>();
var audio = Game.Services.GetService<IAudioManager>();
// Or build a static wrapper for convenience
public static class Locator
{
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)];
}
Event / message bus
An event bus decouples systems that need to communicate. Publishers fire events; subscribers react. This is useful for scoring, achievements, sound triggers, and UI updates.
public static class EventBus
{
private static readonly
Dictionary<Type, List<Delegate>> _handlers = new();
public static void Subscribe<T>(Action<T> handler)
{
var type = typeof(T);
if (!_handlers.ContainsKey(type))
_handlers[type] = new List<Delegate>();
_handlers[type].Add(handler);
}
public static void Publish<T>(T message)
{
if (!_handlers.TryGetValue(typeof(T), out var list)) return;
foreach (var handler in list)
((Action<T>)handler)(message);
}
public static void Unsubscribe<T>(Action<T> handler)
{
if (_handlers.TryGetValue(typeof(T), out var list))
list.Remove(handler);
}
}
// Usage:
// EventBus.Subscribe<EnemyDefeatedEvent>(e => _score += e.Points);
// EventBus.Publish(new EnemyDefeatedEvent { Points = 100 });
Object pooling
Frequent allocation and garbage collection cause frame-rate stutters. Pool reusable objects like bullets, particles, and enemies to avoid GC pressure.
public class ObjectPool<T> where T : new()
{
private readonly Stack<T> _available = new();
private readonly Action<T> _reset;
public ObjectPool(int initialSize, Action<T> reset = null)
{
_reset = reset;
for (int i = 0; i < initialSize; i++)
_available.Push(new T());
}
public T Get()
{
var item = _available.Count > 0
? _available.Pop()
: new T();
_reset?.Invoke(item);
return item;
}
public void Return(T item) => _available.Push(item);
}
// Usage:
// var bulletPool = new ObjectPool<Bullet>(200, b => b.Reset());
// var bullet = bulletPool.Get();
// bullet.Fire(position, direction);
// ... later when bullet dies:
// bulletPool.Return(bullet);
Choosing the right pattern
There is no single “best” architecture. Match the pattern to your project’s scope and your team’s experience.
| Pattern | Best for | Complexity |
|---|---|---|
| Simple game class | Game jams, prototypes | Low |
| Scene manager | Multi-screen games | Low-Medium |
| Component pattern | Medium projects with varied entities | Medium |
| ECS | Large projects, performance-critical | High |
| Service locator | Decoupled shared services | Low |
| Event bus | Cross-system communication | Low-Medium |
| Object pooling | Frequent create/destroy patterns | Low |
Start simple. Extract patterns as your code demands them. A clean game jam prototype often evolves into a well-architected project over time.