diff --git a/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs b/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs index 2a19b5e9..787b663c 100644 --- a/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs +++ b/source/MonoGame.Extended/Graphics/Texture2DAtlas.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Runtime.InteropServices; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using MonoGame.Extended.Animations; @@ -445,38 +446,70 @@ public class Texture2DAtlas : IEnumerable /// /// The spacing, in pixels, between regions. Defaults to 0. /// A containing the created regions. + /// + /// Region names are automatically generated using the pattern {name}_{index}, where + /// is the atlas name and index is the sequential region number starting from 0. For example, if the atlas + /// is named "spritesheet", the regions will be named "spritesheet_0", "spritesheet_1", "spritesheet_2", etc. + /// Regions are created in row-major order (left-to-right, top-to-bottom). + /// /// Thrown if is null. /// Thrown if is disposed. public static Texture2DAtlas Create(string name, Texture2D texture, int regionWidth, int regionHeight, int maxRegionCount = int.MaxValue, int margin = 0, int spacing = 0) { - var textureAtlas = new Texture2DAtlas(name, texture); - var count = 0; - var width = texture.Width - margin; - var height = texture.Height - margin; - var xIncrement = regionWidth + spacing; - var yIncrement = regionHeight + spacing; + ReadOnlySpan regions = CalculateRegions(name, texture.Width, texture.Height, regionWidth, regionHeight, maxRegionCount, margin, spacing); + Texture2DAtlas textureAtlas = new(name, texture); - var columns = (width - margin + spacing) / xIncrement; - var rows = (height - margin + spacing) / yIncrement; - var totalRegions = columns * rows; - - for (var i = 0; i < totalRegions; i++) + for (int i = 0; i < regions.Length; i++) { - var x = margin + (i % columns) * xIncrement; - var y = margin + (i / columns) * yIncrement; - - if (x >= width || y >= height) - break; - - textureAtlas.CreateRegion(x, y, regionWidth, regionHeight); - count++; - - if (count >= maxRegionCount) - return textureAtlas; + CalculatedRegion region = regions[i]; + textureAtlas.CreateRegion(region.bounds, region.Name); } return textureAtlas; } + internal readonly record struct CalculatedRegion(Rectangle bounds, string Name); + + internal static ReadOnlySpan CalculateRegions(string atlasName, int textureWidth, int textureHeight, int regionWidth, int regionHeight, int maxRegionCount, int margin, int spacing) + { + int width = textureWidth - margin; + int height = textureHeight - margin; + int xIncrement = regionWidth + spacing; + int yIncrement = regionHeight + spacing; + + int columns = (width - margin + spacing) / xIncrement; + int rows = (height - margin + spacing) / yIncrement; + int totalRegions = columns * rows; + + // We know what the final size of the collection will be so calculate it + // and use it to prevent reallocations as items are added to the list + int capacity = Math.Min(totalRegions, maxRegionCount); + + List regions = new List(capacity); + + for (int i = 0; i < totalRegions; i++) + { + int x = margin + (i % columns) * xIncrement; + int y = margin + (i / columns) * yIncrement; + + if (x >= width || y >= height) + { + break; + } + + Rectangle bounds = new Rectangle(x, y, regionWidth, regionHeight); + string name = $"{atlasName}_{i}"; + CalculatedRegion region = new(bounds, name); + regions.Add(region); + + if (regions.Count >= maxRegionCount) + { + break; + } + + } + + return CollectionsMarshal.AsSpan(regions); + } } diff --git a/tests/MonoGame.Extended.Tests/Graphics/Texture2DAtlasTests.cs b/tests/MonoGame.Extended.Tests/Graphics/Texture2DAtlasTests.cs new file mode 100644 index 00000000..acefb181 --- /dev/null +++ b/tests/MonoGame.Extended.Tests/Graphics/Texture2DAtlasTests.cs @@ -0,0 +1,23 @@ +using System; +using MonoGame.Extended.Graphics; + +namespace MonoGame.Extended.Tests.Graphics; + +public sealed class Texture2DAtlasTests +{ + // Reference: https://github.com/MonoGame-Extended/Monogame-Extended/issues/1013 + // Region names being generated during TextureAtlas.Create were not unique + // which was leading to an exception being thrown after the first region was + // added + [Fact] + public void CalculateRegions_ShouldGenerateUniqueRegionNames() + { + ReadOnlySpan regions = Texture2DAtlas.CalculateRegions("spritesheet", 64, 64, 32, 32, int.MaxValue, 0, 0); + + Assert.Equal(4, regions.Length); + Assert.Equal("spritesheet_0", regions[0].Name); + Assert.Equal("spritesheet_1", regions[1].Name); + Assert.Equal("spritesheet_2", regions[2].Name); + Assert.Equal("spritesheet_3", regions[3].Name); + } +}