Learning Day 2


Dev Blog Day 2

That Didn't Last Long

So yesterday I said I was done adding tools. Well, that was a lie. As soon as I went to use my first spritesheet, I realized I had no way to handle it.

Enter the SpriteAtlas class. For those of you who may not know an atlas is a book of maps or charts. At the risk of aging myself here, I remember back in my late teens, before GPS was widely available or affordable, how I would use the atlases we had at the gas station where I worked to plan trips. I ended up taking most of the trips I had planned, driving all across the US using those directions.

Well in this case the Atlas is a map of where the sprites live on the spritesheet. I am using assets from Kenney. For those unfamiliar, Kenney is an outstanding asset creator who offers a wide range of assets for both non-commercial and commercial use, free of charge. Kenney does an amazing job and includes an XML reference sheet for his spritesheet, and I am using that to reference the sprites. (A lot of paid spritesheets I have purchased don't put that much effort into their work.)

Let's take a look at the modified `ResourceManager.cs` and `SpriteAtlas.cs`

ResourceManager.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System.Xml.Linq;
namespace TargetPractice.Tools;
public class ResourceManager
{
    private readonly ContentManager _content;
    private readonly GraphicsDevice _graphicsDevice;
    private Dictionary<string, Texture2D> _textures = new Dictionary<string, Texture2D>();
    private Dictionary<string, SpriteAtlas> _spriteAtlases = new Dictionary<string, SpriteAtlas>();
    private Dictionary<string, HashSet<string>> _sceneResources = new Dictionary<string, HashSet<string>>();
    public ResourceManager(ContentManager content, GraphicsDevice graphicsDevice)
    {
        _content = content;
        _graphicsDevice = graphicsDevice;
    }
    public Texture2D LoadTexture(string path)
    {
        if (!_textures.ContainsKey(path))
        {
            var texture = _content.Load<Texture2D>(path);
            _textures[path] = texture;
        }
        return _textures[path];
    }
    public Texture2D CreateOverlayTexture()
    {
        return new Texture2D(_graphicsDevice, 1, 1);
    }
    public void LoadSpriteAtlas(string assetName, string sceneName = null)
    {
        if (!_spriteAtlases.ContainsKey(assetName))
        {
            Texture2D spriteSheet = LoadTexture(assetName);
            XDocument doc = XDocument.Load($"Content/{assetName}.xml");
            SpriteAtlas atlas = new SpriteAtlas(spriteSheet, doc);
            _spriteAtlases[assetName] = atlas;
            if (sceneName != null)
            {
                RegisterResourceForScene(sceneName, assetName, isAtlas: true);
            }
        }
    }
    public void DrawSprite(SpriteBatch spriteBatch, string atlasName, string spriteName, Vector2 position, Color color, float rotation = 0f, Vector2 origin = default(Vector2), float scale = 1f, SpriteEffects effects = SpriteEffects.None, float layerDepth = 0f)
    {
        if (_spriteAtlases.ContainsKey(atlasName))
        {
            SpriteAtlas atlas = _spriteAtlases[atlasName];
            atlas.Draw(spriteBatch, spriteName, position, color, rotation, origin, scale, effects, layerDepth);
        }
    }
    public void RegisterResourceForScene(string sceneName, string resourceName, bool isAtlas = false)
    {
        if (!_sceneResources.ContainsKey(sceneName))
        {
            _sceneResources[sceneName] = new HashSet<string>();
        }
        _sceneResources[sceneName].Add(resourceName + (isAtlas ? "|atlas" : "|texture"));
    }
    public void UnloadSceneResources(string sceneName)
    {
        if (_sceneResources.ContainsKey(sceneName))
        {
            HashSet<string> resources = _sceneResources[sceneName];
            foreach (string resource in resources)
            {
                var parts = resource.Split('|');
                var name = parts[0];
                var type = parts[1];
                if (type == "texture")
                {
                    if (_textures.ContainsKey(name))
                    {
                        _textures[name]?.Dispose();
                        _textures.Remove(name);
                    }
                }
                else if (type == "atlas")
                {
                    _spriteAtlases.Remove(name);
                }
            }
            _sceneResources.Remove(sceneName);
        }
    }
}

There were several ways I could have handled this. Since the game is very small I could just have loaded all assets from the beginning. However, I plan to use this infrastructure for future games.

With that in mind, I chose the way that gave me the most control over what assets are currently loaded. When I load a scene I load in all of the assets I need. When I switch scenes then we dispose of all these loaded assets to free up memory.

In future iterations, we could enhance this approach by incorporating lazy loading, which would significantly improve performance for games with extensive asset libraries.

SpriteAtlas.cs

using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace TargetPractice.Tools;
public class SpriteAtlas
{
    private Texture2D _spriteSheet;
    private Dictionary<string, Rectangle> _spriteCoordinates = new Dictionary<string, Rectangle>();
    public SpriteAtlas(Texture2D spriteSheet, XDocument doc)
    {
        _spriteSheet = spriteSheet;
        ParseXML(doc);
    }
    private void ParseXML(XDocument doc)
    {
        foreach (var spriteElement in doc.Descendants("SubTexture"))
        {
            string name = spriteElement.Attribute("name").Value;
            Console.WriteLine(name);
            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);
            _spriteCoordinates[name] = new Rectangle(x, y, width, height);
        }
    }
    public void Draw(SpriteBatch spriteBatch, string spriteName, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth)
    {
        if (_spriteCoordinates.ContainsKey(spriteName))
        {
            Rectangle sourceRectangle = _spriteCoordinates[spriteName];
            spriteBatch.Draw(_spriteSheet, position, sourceRectangle, color, rotation, origin, scale, effects, layerDepth);
        }
    }
}

Here we just load the spritesheet and map it to a dictionary for quick lookup.

Below are some examples of how this new method is used in the game to draw assets.

Example from Logo.cs

public void Draw(SpriteBatch spriteBatch)
    {
        _resourceManager.DrawSprite(spriteBatch, "images/spritesheet_hud", "crosshair_blue_large.png", new Vector2(100, 100), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.5f);
        spriteBatch.Draw(_logo, _logoDestRect, null, Color.White, 0f, Vector2.Zero, SpriteEffects.None, 0f);
        spriteBatch.Draw(_fadeToBlack, _fullScreenRect, null, new Color(0f, 0f, 0f, _overlayAlpha), 0f, Vector2.Zero, SpriteEffects.None, 1.0f);
    }

One improvement I plan to make soon is to encapsulate the disposal of textures in its own function since we actually use that in several places.

I encourage community feedback on ways any of my code can be improved or better techniques I can use in Game Dev. While I have been a developer for over 20 years I know that I do not know everything. I am always looking to learn and improve further.

As we journey through the continuous evolution of game development, I welcome your insights and shared experiences. Together, we can push the boundaries of what's possible.

Leave a comment

Log in with itch.io to leave a comment.