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

When to avoid ECS

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 classGame jams, prototypesLow
Scene managerMulti-screen gamesLow-Medium
Component patternMedium projects with varied entitiesMedium
ECSLarge projects, performance-criticalHigh
Service locatorDecoupled shared servicesLow
Event busCross-system communicationLow-Medium
Object poolingFrequent create/destroy patternsLow

Start simple. Extract patterns as your code demands them. A clean game jam prototype often evolves into a well-architected project over time.