Learning Day 6


All in on Singletons

I decided that the tools I have created should all be singletons. It makes sense as I want them available globally and to maintain a constant state between game objects.

Settings.cs

I brought settings fully into line with the singleton pattern.

This involved several steps

  • Create a static instance property for the object type named _instance
  • Make sure all other properties are private instanced
  • Create a Getter property to return the instance or throw an error if it doesn't exist
  • Create an Initialize static method to create the object
  • Remove static from all other methods so they are instanced.
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace TargetPractice.Tools;
public class Settings
{
    private static Settings _instance;
    private GraphicsDeviceManager _graphics;
    private DisplayMode _displayMode;
    private Game _game;
    private Dictionary<string, string> GlobalSettings;
    private Settings(Game game)
    {
        _game = game;
        var settings = ReadSettings();
        _graphics = (GraphicsDeviceManager)game.Services.GetService(typeof(IGraphicsDeviceManager));
        _displayMode = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode;
        ApplyVideoSettings(settings);
    }
    public static Settings Instance
    {
        get
        {
            if (_instance == null)
            {
                throw new Exception("Settings not initialized");
            }
            return _instance;
        }
    }
    public static void Initialize(Game game)
    {
        if (_instance == null)
        {
            _instance = new Settings(game);
        }
        else
        {
            throw new InvalidOperationException("Settings already initialized");
        }
    }
    private Dictionary<string, string> ReadSettings()
    {
        var settings = new Dictionary<string, string>();
        var lines = File.ReadAllLines("Content/settings.ini");
        foreach (var rawLine in lines)
        {
            var line = rawLine.Split('#', 2)[0].Trim();
            if (string.IsNullOrWhiteSpace(line)) continue;
            var keyValue = line.Split('=', 2);
            if (keyValue.Length == 2)
            {
                settings[keyValue[0].Trim()] = keyValue[1].Trim();
            }
        }
        GlobalSettings = settings;
        return settings;
    }
    private void ApplyVideoSettings(Dictionary<string, string> settings)
    {
        switch (settings["Mode"])
        {
            case "FullScreen":
                ConfigureGraphicsFullScreen();
                break;
            case "BorderlessFullScreen":
                ConfigureBorderlessFullScreen();
                break;
            case "Windowed":
                ConfigureGraphicsWindowed(int.Parse(settings["Width"]), int.Parse(settings["Height"]));
                break;
            default:
                ConfigureGraphicsWindowed();
                break;
        }
    }
    private void ConfigureGraphicsFullScreen()
    {
        Console.WriteLine("Configuring full screen");
        _game.Window.AllowUserResizing = false;
        _graphics.IsFullScreen = true;
        _graphics.PreferredBackBufferWidth = _displayMode.Width;
        _graphics.PreferredBackBufferHeight = _displayMode.Height;
        _graphics.ApplyChanges();
    }
    private void ConfigureBorderlessFullScreen()
    {
        Console.WriteLine("Configuring borderless full screen");
        _game.Window.AllowUserResizing = false;
        _game.Window.IsBorderless = true;
        _graphics.IsFullScreen = true;
        _graphics.PreferredBackBufferWidth = _displayMode.Width;
        _graphics.PreferredBackBufferHeight = _displayMode.Height;
        _graphics.ApplyChanges();
    }
    private void ConfigureGraphicsWindowed(int width = 800, int height = 480)
    {
        Console.WriteLine("Configuring windowed");
        _game.Window.AllowUserResizing = true;
        _graphics.IsFullScreen = false;
        _graphics.PreferredBackBufferWidth = width;
        _graphics.PreferredBackBufferHeight = height;
        _graphics.ApplyChanges();
    }
    public string GetSetting(string key, string defaultValue = "")
    {
        if (GlobalSettings.TryGetValue(key, out string value))
        {
            return value;
        }
        return defaultValue;
    }
    public void UpdateSetting(string key, string newValue)
    {
        GlobalSettings[key] = newValue;
        var lines = File.ReadAllLines("Content/settings.ini");
        bool keyFound = false;
        for (int i = 0; i < lines.Length; i++)
        {
            var line = lines[i].Split('#', 2);
            var keyValue = line[0].Trim().Split('=', 2);
            if (keyValue.Length == 2 && keyValue[0].Trim() == key)
            {
                lines[i] = $"{keyValue[0].Trim()}={newValue}" + (line.Length > 1 ? $" # {line[1].Trim()}" : "");
                keyFound = true;
                break;
            }
        }
        if (keyFound)
        {
            File.WriteAllLines("Content/settings.ini", lines);
        }
    }
}

SpriteAtlas.cs

This is a singleton now as well. The goal here is that when we need to load the same Texture2D for multiple objects we do not load a copy for each object.

Now each object will be responsible for cleaning up after itself. I plan to implement an array we will loop through for each scene to unload all the assets. Then if they are the only object using that asset it will be removed from memory.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Xml.Linq;
namespace TargetPractice.Tools;
public class SpriteAtlas
{
    private static SpriteAtlas _instance;
    private Dictionary<string, Texture2D> _sheets = new Dictionary<string, Texture2D>();
    private Dictionary<string, Dictionary<string, Rectangle>> _sprites = new Dictionary<string, Dictionary<string, Rectangle>>();
    private Dictionary<string, int> _spriteRefCount = new Dictionary<string, int>();
    private ContentManager _content;
    private SpriteAtlas(ContentManager content)
    {
        _content = content;
    }
    public static SpriteAtlas Instance
    {
        get
        {
            if (_instance == null)
            {
                throw new Exception("SpriteAtlas not initialized");
            }
            return _instance;
        }
    }
    public static void Initialize(ContentManager content)
    {
        if (_instance == null)
        {
            _instance = new SpriteAtlas(content);
        }
        else
        {
            throw new InvalidOperationException("SpriteAtlas already initialized");
        }
    }
    public void RegisterSpriteSheet(string spriteSheet)
    {
        if (_sheets.ContainsKey(spriteSheet))
        {
            _spriteRefCount[spriteSheet]++;
            return;
        };
        _spriteRefCount[spriteSheet] = 1;
        Texture2D sheet = _content.Load<Texture2D>($"spritesheets/{spriteSheet}");
        _sheets[spriteSheet] = sheet;
        RegisterSpriteSheetSprites(spriteSheet, $"Content/xml/{spriteSheet}.xml");
    }
    public void DeregisterSpriteSheet(string spriteSheet)
    {
        if (_spriteRefCount.ContainsKey(spriteSheet))
        {
            _spriteRefCount[spriteSheet]--;
            if (_spriteRefCount[spriteSheet] == 0)
            {
                _spriteRefCount.Remove(spriteSheet);
                _sheets.Remove(spriteSheet);
                _sprites.Remove(spriteSheet);
            }
        }
    }
    private void RegisterSpriteSheetSprites(string spriteSheet, string xmlPath)
    {
        XDocument doc = XDocument.Load(xmlPath);
        Dictionary<string, Rectangle> spriteMap = new Dictionary<string, Rectangle>();
        foreach (var spriteElement in doc.Descendants("SubTexture"))
        {
            string name = spriteElement.Attribute("name").Value;
            int x = int.Parse(spriteElement.Attribute("x").Value);
            int y = int.Parse(spriteElement.Attribute("y").Value);
            int width = int.Parse(spriteElement.Attribute("width").Value);
            int height = int.Parse(spriteElement.Attribute("height").Value);
            spriteMap[name] = new Rectangle(x, y, width, height);
        }
        _sprites[spriteSheet] = spriteMap;
    }
    public Texture2D GetSpriteSheet(string spriteSheet)
    {
        if (_sheets.ContainsKey(spriteSheet))
        {
            return _sheets[spriteSheet];
        }
        return null;
    }
    public Rectangle? GetSpriteRectangle(string spriteSheet, string spriteName)
    {
        if (_sprites.ContainsKey(spriteSheet) && _sprites[spriteSheet].ContainsKey(spriteName))
        {
            return _sprites[spriteSheet][spriteName];
        }
        return null;
    }
}

ScaleManager.cs

This does not use the singleton pattern but it is a static object. I am not sure if this is the best way to do this and is something I am unsure of. However, for now it's the best option I have found.

We run UpdateResolution every update frame. Then when we need to call a draw function we just call the GlobalScale property

using System;
using Microsoft.Xna.Framework;
public static class ScaleManager
{
    private static Vector2 _baseResolution = new Vector2(800, 480); // Adjust this to your base resolution
    private static float _globalScale;
    public static void UpdateResolution(int currentWidth, int currentHeight)
    {
        // Assuming uniform scaling for simplicity
        float scaleX = currentWidth / _baseResolution.X;
        float scaleY = currentHeight / _baseResolution.Y;
        // Use the smaller scale factor to ensure content fits on screen
        _globalScale = Math.Min(scaleX, scaleY);
    }
    public static float GlobalScale => _globalScale;
}

That's All Folks...

That is all for today. I have been reviewing things and plan to start streaming my game development on March 31st at around 9PM Eastern Time.

Until then I plan to keep up my daily updates. One that date hits I may finish this project up if its not done but likely I will be moving to bi-weekly dev blog updates on a new project.

Leave a comment

Log in with itch.io to leave a comment.