Remove obsolete project files for MrGameEng.AI, MrGameEng.Assets, MrGameEng.Atlases, and MrGameEng.Collisions modules. Introduce new AssetManager and related classes for asset loading and management, including support for texture atlases and mod definitions. Enhance mod loading capabilities with DefDatabase and LanguageManager for JSON-based definitions and localization. Implement a shelf packing algorithm for efficient texture atlas creation.

This commit is contained in:
Leonid Pershin
2026-06-12 07:47:04 +03:00
parent 1f87fb0b74
commit c30e2ce764
70 changed files with 0 additions and 233 deletions
@@ -0,0 +1,35 @@
using MrGameEng.Core;
using MrGameEng.Graphics;
namespace MrGameEng.Tilemaps;
/// <summary>Wires the tilemaps module into a <see cref="Scene"/>.</summary>
public static class SceneTilemapExtensions
{
/// <summary>
/// Adds <see cref="TilemapRenderSystem"/> to the scene's draw phase, right before the
/// renderer flush. Call from <c>OnLoad</c> after <c>UseRenderer2D()</c>.
/// </summary>
public static void UseTilemaps(this Scene scene)
{
var renderer =
scene.Context.Services.GetOrDefault<Renderer2D>()
?? throw new InvalidOperationException(
"UseTilemaps requires UseRenderer2D to be called first."
);
var systems = scene.DrawSystems.ChildSystems;
for (var i = 0; i < systems.Count; i++)
{
if (systems[i] is RenderFlushSystem)
{
scene.DrawSystems.Insert(i, new TilemapRenderSystem(renderer));
return;
}
}
throw new InvalidOperationException(
"RenderFlushSystem not found (is UseRenderer2D wired on this scene?)."
);
}
}
@@ -0,0 +1,61 @@
namespace MrGameEng.Tilemaps;
/// <summary>
/// Dense rectangular grid of tile ids (see <see cref="TileSet"/>; 0 = empty).
/// Plain data with bounds-checked access — fill it from worldgen code, mutate at runtime.
/// </summary>
public sealed class TileGrid
{
private readonly ushort[] _cells;
/// <summary>Grid width in cells.</summary>
public int Width { get; }
/// <summary>Grid height in cells.</summary>
public int Height { get; }
/// <summary>Creates a grid filled with the empty tile.</summary>
public TileGrid(int width, int height)
{
ArgumentOutOfRangeException.ThrowIfLessThan(width, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(height, 1);
Width = width;
Height = height;
_cells = new ushort[width * height];
}
/// <summary>Tile id at the given cell.</summary>
public ushort this[int x, int y]
{
get
{
CheckBounds(x, y);
return _cells[y * Width + x];
}
set
{
CheckBounds(x, y);
_cells[y * Width + x] = value;
}
}
/// <summary>True when the cell lies inside the grid.</summary>
public bool Contains(int x, int y) => x >= 0 && x < Width && y >= 0 && y < Height;
/// <summary>Sets every cell to <paramref name="id"/>.</summary>
public void Fill(ushort id) => Array.Fill(_cells, id);
/// <summary>Unchecked read used by the render system after range clamping.</summary>
internal ushort UnsafeGet(int x, int y) => _cells[y * Width + x];
private void CheckBounds(int x, int y)
{
if (!Contains(x, y))
{
throw new ArgumentOutOfRangeException(
nameof(x),
$"Cell ({x},{y}) is outside the {Width}x{Height} grid."
);
}
}
}
@@ -0,0 +1,42 @@
using Microsoft.Xna.Framework;
using MrGameEng.Graphics;
namespace MrGameEng.Tilemaps;
/// <summary>One tile kind: a texture region plus a tint multiplier.</summary>
/// <param name="Region">Texture region the tile is drawn with.</param>
/// <param name="Color">Tint, multiplied with the texture. White = unmodified.</param>
public readonly record struct TileDef(Texture2DRegion Region, Color Color)
{
/// <summary>Creates an untinted tile.</summary>
public TileDef(Texture2DRegion region)
: this(region, Color.White) { }
}
/// <summary>
/// Maps tile ids to <see cref="TileDef"/>s. Built in code: every <see cref="Add"/> returns
/// the id to store in a <see cref="TileGrid"/>. Id 0 is reserved for "empty" (nothing drawn).
/// </summary>
public sealed class TileSet
{
private readonly List<TileDef> _tiles = [default];
/// <summary>Number of defined tiles including the reserved empty tile 0.</summary>
public int Count => _tiles.Count;
/// <summary>The tile definition for <paramref name="id"/>. Id 0 has no region.</summary>
public TileDef this[int id] => _tiles[id];
/// <summary>Defines a tile and returns its id (1, 2, …).</summary>
public ushort Add(Texture2DRegion region, Color? color = null)
{
ArgumentNullException.ThrowIfNull(region);
if (_tiles.Count > ushort.MaxValue)
{
throw new InvalidOperationException("A TileSet holds at most 65535 tiles.");
}
_tiles.Add(new TileDef(region, color ?? Color.White));
return (ushort)(_tiles.Count - 1);
}
}
@@ -0,0 +1,47 @@
using Friflo.Engine.ECS;
using Microsoft.Xna.Framework;
using MrGameEng.Graphics;
namespace MrGameEng.Tilemaps;
/// <summary>
/// Tilemap component: a <see cref="TileGrid"/> of ids drawn with a <see cref="TileSet"/>.
/// Cell (0,0) sits at <see cref="Origin"/>, cells are <see cref="TileSize"/> world units square.
/// Only the cells visible through the camera are submitted each frame, so grids can be large.
/// Create via the constructor — the struct default has no grid and draws nothing.
/// </summary>
public struct Tilemap : IComponent
{
/// <summary>Tile definitions; ids in the grid index into it.</summary>
public TileSet? TileSet;
/// <summary>The cells. Mutating ids takes effect next frame.</summary>
public TileGrid? Grid;
/// <summary>World position of the top-left corner of cell (0,0).</summary>
public Vector2 Origin;
/// <summary>Cell size in world units.</summary>
public float TileSize;
/// <summary>Render layer of the whole map.</summary>
public LayerId Layer;
/// <summary>Draw order within the layer (smaller = drawn first / behind).</summary>
public float Depth;
/// <summary>Tint multiplied into every tile on top of its <see cref="TileDef.Color"/>.</summary>
public Color Color;
/// <summary>Creates a tilemap at world origin.</summary>
public Tilemap(TileGrid grid, TileSet tileSet, float tileSize, LayerId layer = default)
{
Grid = grid;
TileSet = tileSet;
TileSize = tileSize;
Layer = layer;
Origin = Vector2.Zero;
Depth = 0f;
Color = Color.White;
}
}
@@ -0,0 +1,31 @@
using Microsoft.Xna.Framework;
using MrGameEng.Graphics;
namespace MrGameEng.Tilemaps;
/// <summary>Pure cell-range math for tilemap rendering. Y axis points down.</summary>
public static class TilemapMath
{
/// <summary>
/// Computes the inclusive cell range of a grid that intersects <paramref name="cullRect"/>.
/// Returns false when the map is entirely outside the rectangle.
/// </summary>
public static bool VisibleCells(
in RectF cullRect,
Vector2 origin,
float tileSize,
int width,
int height,
out int x0,
out int y0,
out int x1,
out int y1
)
{
x0 = Math.Max(0, (int)MathF.Floor((cullRect.Left - origin.X) / tileSize));
y0 = Math.Max(0, (int)MathF.Floor((cullRect.Top - origin.Y) / tileSize));
x1 = Math.Min(width - 1, (int)MathF.Floor((cullRect.Right - origin.X) / tileSize));
y1 = Math.Min(height - 1, (int)MathF.Floor((cullRect.Bottom - origin.Y) / tileSize));
return x0 <= x1 && y0 <= y1;
}
}
@@ -0,0 +1,101 @@
using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework;
using MrGameEng.Graphics;
namespace MrGameEng.Tilemaps;
/// <summary>
/// Submits the camera-visible cells of every <see cref="Tilemap"/> entity to the renderer.
/// Cells outside the camera are never touched, so per-frame cost scales with the screen,
/// not with the grid. Tiles batch with sprites by the usual layer → depth → texture order.
/// </summary>
public sealed class TilemapRenderSystem : QuerySystem<Tilemap>
{
private readonly Renderer2D _renderer;
/// <summary>Creates the system for <paramref name="renderer"/>.</summary>
public TilemapRenderSystem(Renderer2D renderer) => _renderer = renderer;
/// <inheritdoc />
protected override void OnUpdate()
{
var cullRect = _renderer.Camera.CullRect;
foreach (var (maps, _) in Query.Chunks)
{
foreach (ref readonly var map in maps.Span)
{
if (
map.Grid is not { } grid
|| map.TileSet is not { } tileSet
|| map.TileSize <= 0f
)
{
continue;
}
var screenSpace = _renderer.Layers[map.Layer].Space == LayerSpace.Screen;
if (screenSpace)
{
// Screen-space слои не куллятся камерой — рисуем весь грид.
SubmitRange(in map, grid, tileSet, 0, 0, grid.Width - 1, grid.Height - 1);
}
else if (
TilemapMath.VisibleCells(
in cullRect,
map.Origin,
map.TileSize,
grid.Width,
grid.Height,
out var x0,
out var y0,
out var x1,
out var y1
)
)
{
SubmitRange(in map, grid, tileSet, x0, y0, x1, y1);
}
}
}
}
private void SubmitRange(
in Tilemap map,
TileGrid grid,
TileSet tileSet,
int x0,
int y0,
int x1,
int y1
)
{
for (var y = y0; y <= y1; y++)
{
for (var x = x0; x <= x1; x++)
{
var id = grid.UnsafeGet(x, y);
if (id == 0)
{
continue;
}
var def = tileSet[id];
var region = def.Region;
var transform = new Transform2D(
map.Origin + new Vector2(x, y) * map.TileSize,
scale: new Vector2(map.TileSize / region.Width, map.TileSize / region.Height)
);
var sprite = new Sprite(region, map.Layer)
{
Color =
map.Color == Color.White
? def.Color
: new Color(def.Color.ToVector4() * map.Color.ToVector4()),
Depth = map.Depth,
};
_renderer.Submit(in transform, in sprite);
}
}
}
}