Episode 1: From Concept to Code

In episode 0 I discussed and answered the first major questions I had before starting the project. First, what game do I want to create? In the end, I came up with a game similar to Dwarf Fortress, but with a slightly different setting, some adjusted mechanics and a more modern presentation and user experience. I chose the name "Grim Veil" for now. Second, how do I want to create it? Based on my preferences and know-how, I chose MonoGame as my main framework.

In this episode, I will go deeper into the technical background of Grim Veil. I give you a high-level overview of my architecture and show you some principles it follows. Also, I want to show you the most important classes that form the architecture's basis.

I'm no software architect or professional game developer. So I give you these deep code insights; maybe one of you has some good advice for me to create a better architecture or write better code. Feel free to leave comments if you find room for improvement.

The higher-level control mechanism

To understand the system, that controls my game as a whole, I have to talk a bit about MonoGame itself and how it lets us build games. It's not necessary to explain it in detail. For that, you can check out the MonoGame documentation. What is important here is, that its main class Game1 provides four core methods to run the program:

  1. Initialize() is used for querying required services and loading non-graphic-related content.

  2. LoadContent() is called within Initialize() and is used for loading all other game content.

  3. Update() is the game's main update loop. It's used for updating the state of the game.

  4. Draw() is similar to the Update() method. It's responsible for drawing the game content on the screen.

Initialize() and LoadContent() are only called once at the start of the game, so I will only use them to initialize the core managers and services. Update() and Draw() are much more interesting. Depending on the game's state, different things need to happen there. So I use the Finite State Machine Pattern (FSM) as the main control mechanism. I expect to use this pattern for multiple systems, so I created a small library that holds the base classes for it.

public abstract class State<TState, TStateMachine>
    where TState : State<TState, TStateMachine>
    where TStateMachine : StateMachine<TState, TStateMachine>
{
    protected readonly TStateMachine StateMachine;

    public abstract string StateLogString { get; }

    protected State(TStateMachine stateMachine)
    {
        StateMachine = stateMachine;
    }

    public virtual void OnBegin() {}

    public virtual void OnExit() {}

    protected void ChangeState(TState state)
    {
        StateMachine.ChangeState(state);
    }
}
public abstract class StateMachine<TState, TStateMachine> 
    where TState : State<TState, TStateMachine>
    where TStateMachine : StateMachine<TState, TStateMachine> 
{
    protected TState? CurrentState { get; private set; }

    public void ChangeState(TState newState)
    {
        CurrentState?.OnExit();
        newState.OnBegin();
        CurrentState = newState;
    }

    public string GetStateLog()
    {
        return CurrentState?.StateLogString ?? "No state!";
    }
}

It's not really a part of the FSM pattern to make these classes generic in the way I did. However, when I code within a derived state class, I want to have access to the methods and properties of the corresponding derived state machine class.

Using these classes, I built the main control mechanism: the GameManager class, which is derived from the StateMachine class, and the GameState base class, which is derived from the State class.

public abstract class GameState : State<GameState, GameManager>
{
    protected abstract StateNames StateName { get; }

    protected GameState(GameManager stateMachine)
        : base(stateMachine)
    { }

    public virtual void Update(GameTime gameTime) { }

    public virtual void Draw(SpriteBatch spriteBatch, GameTime gameTime) { }
}
public class GameManager : StateMachine<GameState, GameManager>
{
    public event Action? ExitRequested;

    public GraphicsDeviceManager Graphics { get; }
    public GameWindow Window { get; }

    public void Update(GameTime gameTime)
    {
        try
        {
            CurrentState?.Update(gameTime);
        }
        catch (Exception e)
        {
            OnExitGame();
        }
    }

    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        try
        {
            CurrentState?.Draw(spriteBatch, gameTime);
        }
        catch (Exception e)
        {
            OnExitGame();
        }
    }

    public void OnExitGame()
    {
        ExitRequested?.Invoke();
    }
}

In the Game1 class, I use the GameManager like this:

public class Game1 : Game
{
    private SpriteBatch? _spriteBatch;
    private readonly GameManager _gameManager;

    private readonly Color CLEARCOLOR = new(new Vector3(0, 0, 0.2f));

    public Game1()
    {
        [...]

        _gameManager = new GameManager(graphics, Window);
        _gameManager.ExitRequested += OnExit;
    }

    protected override void Initialize()
    {
        base.Initialize();

        _gameManager.ChangeState(new SplashScreenState(_gameManager));
    }

    protected override void LoadContent()
    {
        _spriteBatch = new SpriteBatch(GraphicsDevice);
    }

    protected override void Update(GameTime gameTime)
    {
        _gameManager.Update(gameTime);

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(CLEARCOLOR);

        if (_spriteBatch != null)
        {
            _spriteBatch.Begin();
            _gameManager.Draw(_spriteBatch, gameTime);
            _spriteBatch.End();
        }

        base.Draw(gameTime);
    }

    public void OnExit()
    {
        Exit();
        Environment.Exit(0);
    }
}

That's it. Based on this I can create as many different game states as I want. In the code above you already can see the SplashScreenState class. This is the first state the game will ever have. Let me also show this as an example.

public class SplashScreenState : GameState
{
    public override string StateLogString => "Splash Screen State";

    protected override StateNames StateName => StateNames.SplashScreen;

    internal SplashScreenState(GameManager stateMachine)
        : base(stateMachine)
    { }

    protected override void Initialize()
    {
        int screenWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
        int screenHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;

        StateMachine.Window.Position = new Point(screenWidth / 2 - Settings.SPLASHSCREEN_WIDTH / 2, screenHeight / 2 - Settings.SPLASHSCREEN_HEIGHT / 2);
        StateMachine.Graphics.PreferredBackBufferWidth = Settings.SPLASHSCREEN_WIDTH;
        StateMachine.Graphics.PreferredBackBufferHeight = Settings.SPLASHSCREEN_HEIGHT;
        StateMachine.Window.IsBorderless = true;

        StateMachine.Graphics.ApplyChanges();
    }

    public override void Update(GameTime gameTime)
    {
        TimeSpan startingTime = gameTime.TotalGameTime;

        while (startingTime.TotalSeconds < Settings.MINIMUM_SPLASHSCREEN_TIME_SECONDS)
            return;

        ChangeState(new StartupLoadingScreenState(StateMachine));
    }
}

You can see, from within GameStates I can control the whole program. Here in Initialize() I even can set game window properties.

How to fire events

I know I haven't shown you any screenshots from the game yet. For that to make sense, I want to have a working scene management system finished. But that requires more explanation than I want these blog posts to be long. So for now I finish things off with the event system I chose to use. It follows the EventBus design pattern.

First I created two simple interfaces.

public interface IEvent { }

internal interface IEventBinding<T> 
    where T : IEvent
{
    public Action<T> OnEvent { get; set; }
    public Action OnEventNoArgs { get; set; }
}

An IEvent instance will act as the event's "signature" and data transfer container. I'll show you right away. An IEventBinding and its concrete implementation EventBinding is the connection between an action and an event of type T.

public class EventBinding<T> : IEventBinding<T>
    where T : IEvent
{
    private Action<T> _onEvent = _ => { };
    private Action _onEventNoArgs = () => { };

    Action<T> IEventBinding<T>.OnEvent
    {
        get => _onEvent; 
        set => _onEvent = value;
    }

    Action IEventBinding<T>.OnEventNoArgs
    {
        get => _onEventNoArgs;
        set => _onEventNoArgs = value;
    }

    public EventBinding(Action<T> onEvent) => _onEvent = onEvent;
    public EventBinding(Action onEventNoArgs) => _onEventNoArgs = onEventNoArgs;
}

Finally, I needed the EventBus itself.

public static class EventBus<T>
    where T : IEvent
{
    private static readonly HashSet<IEventBinding<T>> _bindings = new();

    public static void Register(EventBinding<T> binding) => _bindings.Add(binding);
    public static void Unregister(EventBinding<T> binding) => _bindings.Remove(binding);

    public static void Raise(T @event)
    {
        Debug.WriteLine($"Event fired. Type: {typeof(T).Name}");

        foreach (IEventBinding<T> binding in _bindings)
        {
            binding.OnEvent.Invoke(@event);
            binding.OnEventNoArgs.Invoke();
        }
    }
}

The EventBus collects all EventBindings that are registered to it in a Hashset and invokes their actions when the event is raised from somewhere. Since all these classes live in their own assembly and the EventBus class is static, it is super easy to establish communication between modules that don't know each other. Let me show you an example from the GameManager class.

First, I define a new event type.

public struct ChangeGameStateEvent : IEvent
{
    public StateNames StateName;

    public ChangeGameStateEvent(StateNames stateName)
    {
        StateName = stateName;
    }
}

Don't be confused about StateNames. It's just an Enum that holds all state names to avoid typos. Then, in the GameManager I create and register to the EventBus for this event like this.

    public GameManager(
        GraphicsDeviceManager graphics,
        GameWindow gameWindow)
    {
        Graphics = graphics;
        Window = gameWindow;

        EventBinding<ChangeGameStateEvent> changeGameStateEventBinding = new(
            @event => ChangeState(GameStateFactory.BuildByName(@event.StateName)));
        EventBus<ChangeGameStateEvent>.Register(changeGameStateEventBinding);

        EventBinding<RequestExitGameEvent> requestExitGameEventBinding = new(OnExitGame);
        EventBus<RequestExitGameEvent>.Register(requestExitGameEventBinding);
    }

To avoid confusion again, GameStateFactory is a factory class to create new GameStates by name. The second event RequestExitGameEvent you see can be raised when the game should be closed. An event can be raised simply using this command:

EventBus<ChangeGameStateEvent>.Raise(new ChangeGameStateEvent(StateNames.Ingame_Normal));

Conclusion

I think that's a good start for the game's architecture. Next time, I show you my implementation for scene management. Then I finally can show you screenshots.

So stay tuned and see you in the next episode.

0
Subscribe to my newsletter

Read articles from Kevin “BanditBloodwyn” Eichenberg directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kevin “BanditBloodwyn” Eichenberg
Kevin “BanditBloodwyn” Eichenberg