Episode 2: Creating a scene management system

Last time I created a control mechanism for my game that is based on the "Finite State Machine" design pattern. Also, I implemented an EventBus to let separate modules communicate without the need to couple them together. Building on that, my next step is creating a system to manage scenes.

Scenes in game development are used to organize and manage the content and logic of a game. They are similar to scenes in a movie or play, representing specific settings or contexts. For example, one scene might be the main menu, another a level in the game, and another a cutscene. Separating all this content into different scenes makes managing and developing each part of the game independently easier.

Base

First I had to figure out what I even wanted. To do this, I broke it down into a few simple questions and answered them:

  • What is a Scene in my game?
    A scene is a collection of drawable and updatable objects belonging to the same context (called "scene").
    Updating the scene means to update all updatable objects within the scene.
    Drawing the scene means drawing all drawable objects within the scene.
    I want to be able to dynamically add and remove objects to the scene

  • Where do I want to place the scene management concerning the other modules?
    I want a scene management that works completely independently from the state management of the game or any other module.

That was enough to create the important classes: IUpdatable, IDrawable, Scene and SceneManager.

public interface IUpdatable
{
    public void Update(GameTime gameTime);
}
public interface IDrawable
{
    public Rectangle Rectangle { get; }

    public void Draw(SpriteBatch spriteBatch);
}

Using these interfaces I can create object classes. Depending on which interface the object implements, I can define if the object is updated every frame and/or drawn on the screen.

Now for the scene itself.

public class Scene
{
    public Dictionary<object, IUpdatable> Updateables = new();
    public Dictionary<object, IDrawable> Drawables = new();

    public bool NoUpdatables => Updateables.Count == 0;
    public bool NoDrawables => Drawables.Count == 0;
    public bool IsEmpty => NoUpdatables && NoDrawables;

    public string? Name { get; set; }

    public void AddObject(object key, object @object)
    {
        if (@object is IUpdatable updateable)
            Updateables.Add(key, updateable);
        if (@object is IDrawable drawable)
            Drawables.Add(key, drawable);
    }

    public void RemoveObject(object key)
    {
        if (Updateables.ContainsKey(key))
            Updateables.Remove(key);
        if (Drawables.ContainsKey(key))
            Drawables.Remove(key);
    }

    public void Update(GameTime gameTime)
    {
        foreach (KeyValuePair<object, IUpdatable> pair in Updateables)
            pair.Value.Update(gameTime);

        if (!IsEmpty)
            return;

        if (InputManager.IsKeyPressed(Keys.Escape))
            EventBus<RequestExitGameEvent>.Raise(new RequestExitGameEvent());
    }

    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        foreach (KeyValuePair<object, IDrawable> pair in Drawables)
            pair.Value.Draw(spriteBatch);

        if (Settings.SHOWDEBUGINFO)
            WriteDebugInfo(spriteBatch, gameTime);
    }

    private void WriteDebugInfo(SpriteBatch spriteBatch, GameTime gameTime)
    {
        [...]
    }
}

As you can see in AddObject(), you can add any object to the scene. As long as it implements one of the interfaces above, it's inserted into one of the Dictionaries. Update() and Draw() go through the corresponding dictionaries and call Update() and Draw() of each object. I also added some handy properties, debug texts, and the ability to exit the game if the scene is empty.

Let's check out the SceneManager.

public class SceneManager
{
    private readonly Dictionary<SceneNames, Scene> _cachedScenes = new();

    private Scene? _activeScene;

    public SceneManager()
    {
        EventBinding<ChangeActiveSceneEvent> changeActiveSceneEventBinding = new(OnChangeActiveScene);
        EventBus<ChangeActiveSceneEvent>.Register(changeActiveSceneEventBinding);
    }

    public void Update(GameTime gameTime)
    {
        _activeScene?.Update(gameTime);
    }

    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        _activeScene?.Draw(spriteBatch, gameTime);
    }

    private void OnChangeActiveScene(ChangeActiveSceneEvent @event)
    {
        if (_cachedScenes.TryGetValue(@event.SceneName, out Scene? loadedScene))
        {
            _activeScene = loadedScene;
            return;
        }

        if (SceneBuilder.TryBuildByName(@event.SceneName, out Scene? newScene))
        {
            AddScene(@event.SceneName, newScene);
            _activeScene = newScene;
            return;
        }

        Debug.WriteLine("Cannot get scene!");
    }

    private void AddScene(SceneNames stateName, Scene scene)
    {
        _cachedScenes.TryAdd(stateName, scene);
    }
}

I think the code is simple enough. The SceneManager holds the currently active scene and all scenes that were created before during runtime. Additionally, it reacts to the "ChangeActiveSceneEvent" by loading a new scene. Finally, it updates and draws the active one.

The SceneBuilder provides all scenes in the game.

public class SceneBuilder
{
    private static ContentManager _contentManager = null!;
    private static bool _initialized;

    private static readonly int _screenWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
    private static readonly int _screenHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;

    public static void Initialize(ContentManager contentManager)
    {
        _contentManager = contentManager;
        _initialized = true;
    }

    public static bool TryBuildByName(SceneNames stateName, out Scene? scene)
    {
        ContentPool.LoadContentByStateName(stateName, _contentManager);

        scene = stateName switch
        {
            SceneNames.SplashScreen => SplashScreen(),
            SceneNames.StartupLoadingScreen => StartupLoadingScreen(),
            SceneNames.MainMenu => MainMenuScreen(),
            SceneNames.IngameLoadingScreen => IngameLoadingScreen(),
            SceneNames.Ingame_Normal => InGameScene(),
            _ => null
        };

        if (scene == null)
            Debug.WriteLine("Scene unknown!");

        return scene != null;
    }

    public static Scene SplashScreen()
    {
        [...]
    }

    public static Scene StartupLoadingScreen()
    {
        [...]
    }

    public static Scene MainMenuScreen()
    {
        [...]
    }

    private static Scene IngameLoadingScreen()
    {
        [...]
    }

    public static Scene InGameScene()
    {
        [...]
    }
}

Maybe I'll add a scene building based on external files like JSON or XML, but for now, I hardcode all scenes. To fill them I created some UI element classes like Button, Label, or Image.

Lastly, I made a crucial addition to the GameState class.

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

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

    public override void OnBegin()
    {
        if (AssociatedSceneName != null)
            EventBus<ChangeActiveSceneEvent>.Raise(new ChangeActiveSceneEvent(AssociatedSceneName.Value));

        Initialize();
    }

    protected virtual void Initialize() { }

    public virtual void Update(GameTime gameTime) { }
}

Every GameState now can have an associated scene name. If it's not null, the "ChangeActiveSceneEvent" is fired to let the SceneManager load the new scene.

Result

Using ChatGPT and some GIMP skills, the result of all this trouble I went through, looks like this.

First, we see this splash screen for some seconds.

Image of the current splash screen

Then the game switches to fullscreen and the first loading screen shows up. Currently, I just show it for 2 seconds. Later in development, I'll place the loading routines here.

Image of the first loading screen

After that, there comes the main menu. Right now, only the "New Game" and the "Quit" buttons work.

Image of the main menu

When pressing "New Game", we finally come to the in-game scene. It's still empty of course, but that will change soon.

Project architecture

In the meantime, I added some more smaller assemblies to the solution. Not all of them are useful at the moment. I added them for later. They contain some useful functionalities, for example, implementations and base classes of some design patterns.

When collapsing the groups, the layered architecture becomes recognizable.

  1. Game mechanics layer
    This layer contains the concrete game's code. At the moment, there is state management, scene management, and input management. Also, I already added an assembly for the game world.

  2. Game object layer
    Here are all the classes that represent game objects. Right now, we have Button, Image and Label.

  3. Framework layer
    This layer contains core classes that are useful for MonoGame games. Examples are the IUpdatable and IDrawable interfaces, extensions for the SpriteBatch, and some general Event types. I also put some constant global content here.

  4. Core layer
    Here we find code that is useful for all kinds of C# software, like implementations of design patterns, extensions of general C# classes, etc.

That's all I have for now.

Next time, I implement the first iteration of the game world. Ideally, I have the first in-game screenshots for you then.

See you!

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