-
-
Couldn't load subscription status.
- Fork 345
Description
Summary
Replace the current Tiled-specific tilemap implementation with an agnostic tilemap API that supports multiple tilemap editor formats (Tiled, LDtk, Ogmo, etc.) through a unified interface. This provides users with greater flexibility in tooling choices while maintaining all existing functionality and improving the overall architecture.
Motivation
Current Problems
The existing MonoGame.Extended tilemap implementation has several significant limitations:
- Format Lock-in: Only supports Tiled TMX format, forcing users to use a single tool
- Over-abstraction: Complex layered architecture that makes maintenance difficult
- Missing Features: Limited support for collision detection and tile metadata
- Poor Extensibility: Adding new features or formats requires deep architectural changes
- Limited Tool Ecosystem: Restricts users to a single tilemap editor when other quality tools exist
Use Cases Supported
- Multi-tool Projects: Teams can choose the best tilemap editor for their workflow
- Tool Migration: Projects can switch between editors without code changes
- Expanded Ecosystem: Access to features from different tilemap editors (LDtk's auto-layers, Ogmo's simplicity)
- Custom Workflows: Users can implement parsers for proprietary or specialized formats
- Better Collision: Unified collision system works across all supported formats
- Future-proofing: Easy addition of new tilemap formats as they emerge
Expected Outcomes
- Simplified, maintainable codebase with clear separation of concerns
- Support for Tiled, LDtk, and Ogmo out of the box
- Extensible parser system for community contributions
- No feature loss from current implementation
- Smooth migration path for existing users
Detailed design
Core Architecture
The design separates parsing from rendering through three main components:
- Core API: Format-agnostic data structures (
Tilemap,TilemapLayer,Tile, etc.) - Parser System:
ITilemapParserinterface with format-specific implementations - Renderer: Unified rendering system that works with any parsed tilemap
Key Components
1. Main Tilemap Container
namespace MonoGame.Extended.Tilemaps
{
public sealed class Tilemap
{
public string Id { get; }
public string Name { get; }
public Size Size { get; }
public Size TileSize { get; }
public Size SizeInPixels { get; }
public Color? BackgroundColor { get; }
public TilemapOrientation Orientation { get; }
public TileRenderOrder RenderOrder { get; }
public IReadOnlyList<Tileset> Tilesets { get; }
public IReadOnlyList<TilemapLayer> Layers { get; }
public PropertyCollection Properties { get; }
// Layer and tileset access methods
public T GetLayer<T>(string name) where T : TilemapLayer;
public Tileset GetTilesetByGlobalId(int globalTileId);
}
}2. Layer Hierarchy
public abstract class TilemapLayer
{
public string Id { get; }
public string Name { get; }
public string Type { get; }
public Vector2 Offset { get; }
public Vector2 ParallaxFactor { get; }
public float Opacity { get; }
public bool IsVisible { get; }
public Color? TintColor { get; }
public PropertyCollection Properties { get; }
}
public sealed class TileLayer : TilemapLayer
{
public Size Size { get; }
public IReadOnlyList<Tile> Tiles { get; }
public Tile GetTile(int x, int y);
public void SetTile(int x, int y, Tile tile);
}
public sealed class ObjectLayer : TilemapLayer
{
public IReadOnlyList<TilemapObject> Objects { get; }
public IEnumerable<TilemapObject> GetObjectsOfType(string type);
public IEnumerable<TilemapObject> GetObjectsInRegion(RectangleF region);
}
public sealed class ImageLayer : TilemapLayer
{
public Texture2D Image { get; }
public Vector2 Position { get; }
}
public sealed class GroupLayer : TilemapLayer
{
public IReadOnlyList<TilemapLayer> Layers { get; }
}3. Object System
public abstract class TilemapObject
{
public int Id { get; }
public string Name { get; }
public string Type { get; }
public Vector2 Position { get; }
public SizeF Size { get; }
public float Rotation { get; }
public bool IsVisible { get; }
public PropertyCollection Properties { get; }
public RectangleF Bounds { get; }
}
// Specific object types
public sealed class RectangleObject : TilemapObject { }
public sealed class EllipseObject : TilemapObject
{
/// <summary>
/// Gets the center point of the ellipse.
/// </summary>
public Vector2 Center => Position + new Vector2(Size.Width / 2, Size.Height / 2);
/// <summary>
/// Gets the horizontal radius of the ellipse.
/// </summary>
public float RadiusX => Size.Width / 2;
/// <summary>
/// Gets the vertical radius of the ellipse.
/// </summary>
public float RadiusY => Size.Height / 2;
}
public sealed class PointObject : TilemapObject
{
// Point objects have Size = SizeF.Empty
// Position represents the point location
// Bounds returns RectangleF.Empty or a 1x1 rectangle for hit testing
}
public sealed class PolygonObject : TilemapObject
{
/// <summary>
/// Gets the vertices of the polygon relative to the object position.
/// </summary>
public IReadOnlyList<Vector2> Vertices { get; }
/// <summary>
/// Gets the world-space vertices of the polygon.
/// </summary>
public IEnumerable<Vector2> WorldVertices => Vertices.Select(v => Position + v);
}
public sealed class PolylineObject : TilemapObject
{
/// <summary>
/// Gets the points of the polyline relative to the object position.
/// </summary>
public IReadOnlyList<Vector2> Points { get; }
/// <summary>
/// Gets the world-space points of the polyline.
/// </summary>
public IEnumerable<Vector2> WorldPoints => Points.Select(p => Position + p);
}
public sealed class TileObject : TilemapObject
{
/// <summary>
/// Gets the global tile identifier.
/// </summary>
public int GlobalTileId { get; }
/// <summary>
/// Gets the flip flags for the tile.
/// </summary>
public TileFlipFlags FlipFlags { get; }
}4. Parser Interface
public interface ITilemapParser
{
IReadOnlyList<string> SupportedExtensions { get; }
bool CanParse(string filePath);
Tilemap ParseFromFile(string filePath, GraphicsDevice graphicsDevice);
Tilemap ParseFromStream(Stream stream, GraphicsDevice graphicsDevice);
}
// Concrete implementations
public class TiledTmxParser : ITilemapParser { }
public class LDtkJsonParser : ITilemapParser { }
public class OgmoJsonParser : ITilemapParser { }5. Unified Renderer
public sealed class TilemapRenderer : IDisposable
{
public TilemapRenderer(GraphicsDevice graphicsDevice);
public void LoadTilemap(Tilemap tilemap);
public void Update(GameTime gameTime);
// Drawing methods
public void Draw(SpriteBatch spriteBatch);
public void Draw(SpriteBatch spriteBatch, Matrix? transform);
public void Draw(SpriteBatch spriteBatch, Camera2D camera);
// Layer-specific drawing
public void DrawLayer(SpriteBatch spriteBatch, string layerName);
public void DrawLayer(SpriteBatch spriteBatch, TilemapLayer layer);
public void DrawLayers(SpriteBatch spriteBatch, params string[] layerNames);
// Optimization features
public void SetViewport(Rectangle viewport);
public void EnableCulling(bool enabled);
public void Dispose();
}6. Builder Pattern
public sealed class TilemapBuilder
{
public TilemapBuilder WithSize(int width, int height);
public TilemapBuilder WithTileSize(int width, int height);
public TilemapBuilder AddTileset(Tileset tileset);
public TilemapBuilder AddLayer(TilemapLayer layer);
public Tilemap Build();
}Format Support Matrix
| Feature | Tiled TMX | LDtk JSON | Ogmo | API Support |
|---|---|---|---|---|
| Orthogonal Maps | ✅ | ✅ | ✅ | ✅ |
| Isometric Maps | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Hexagonal Maps | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Tile Layers | ✅ | ✅ | ✅ | ✅ |
| Object Layers | ✅ | ✅ (Entities) | ✅ | ✅ |
| Image Layers | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Group Layers | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Auto-Layers | ❌ | ✅ | ❌ | ✅ (LDtk only) |
| IntGrid Layers | ❌ | ✅ | ✅ (Grid) | ✅ (as properties) |
| Decal Layers | ❌ | ❌ | ✅ | ✅ (as objects) |
| Tile Animation | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Tile Collision | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Custom Properties | ✅ | ✅ (Fields) | ✅ (Values) | ✅ |
| Parallax Scrolling | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Layer Opacity | ✅ | ✅ | ❌ | ✅ |
| Layer Tinting | ✅ | ❌ | ❌ | ✅ (Tiled only) |
| Infinite Maps | ✅ | ❌ | ❌ | ✅ (Tile∏d only) |
| Multi-World | ❌ | ✅ | ❌ | ✅ (LDtk only) |
Usage Examples
Loading Different Formats
```csharp
// Use the parser directly for your chosen format
var tiledParser = new TiledTmxParser();
var tilemap = tiledParser.ParseFromFile("level.tmx", graphicsDevice);
// Or for LDtk
var ldtkParser = new LDtkJsonParser();
var ldtkMap = ldtkParser.ParseFromFile("world.ldtk", graphicsDevice);
// Or for Ogmo
var ogmoParser = new OgmoJsonParser();
var ogmoMap = ogmoParser.ParseFromFile("stage.json", graphicsDevice);
// All produce the same Tilemap type for unified rendering
var renderer = new TilemapRenderer(graphicsDevice);
// Works with any format
renderer.LoadTilemap(tilemap); Collision Detection
// Works with objects from any format
var collisionLayer = tilemap.GetLayer<ObjectLayer>("Collision");
var playerBounds = new RectangleF(player.Position.X, player.Position.Y, 32, 32);
foreach (var obstacle in collisionLayer.GetObjectsInRegion(playerBounds))
{
if (obstacle is PolygonObject polygon)
{
// Handle polygon collision
if (Collision.PolygonRectangle(polygon.WorldVertices, playerBounds))
HandleCollision(obstacle);
}
else if (obstacle.Bounds.Intersects(playerBounds))
{
// Handle rectangle collision
HandleCollision(obstacle);
}
}Programmatic Creation
// Create tilemaps in code
var tilemap = new TilemapBuilder("custom", "Custom Map")
.WithSize(100, 100)
.WithTileSize(32, 32)
.AddTileset(grassTileset)
.AddLayer(new TileLayerBuilder("ground", "Ground")
.WithSize(100, 100)
.SetTile(0, 0, new Tile(grassTileset.FirstGlobalId, new Point(0, 0)))
.Build())
.Build();Migration Strategy
Phase 1: Implementation
- Implement new agnostic API alongside existing Tiled API
- Mark old API as
[Obsolete]with migration guidance - Provide conversion utilities
Phase 2: Adoption
- Add parsers for LDtk and Ogmo
- Update documentation and samples
- Community feedback and refinements
Phase 3: Cleanup
- Remove obsolete Tiled-specific API
- Finalize API based on usage feedback
Migration Example
// Old way (deprecated)
var oldMap = content.Load<TiledMap>("level");
var oldRenderer = new TiledMapRenderer(graphicsDevice, oldMap);
// New way
var parser = new TiledTmxParser();
var newMap = parser.ParseFromFile("level.tmx", graphicsDevice);
var newRenderer = new TilemapRenderer(graphicsDevice);
newRenderer.LoadTilemap(newMap);
// API is similar but more flexible
var collisionLayer = newMap.GetLayer<ObjectLayer>("Collision");
// vs oldMap.GetLayer<TiledMapObjectLayer>("Collision")Drawbacks
- Breaking Changes: Eventually requires migration from existing Tiled API
- Development Time: Significant upfront investment to implement properly
- Complexity: More complex internally due to supporting multiple formats
- Learning Curve: Users need to understand the new, more generic API
- Parser Maintenance: Need to maintain parsers for multiple evolving formats
- Performance: Small overhead from abstraction layer (though likely negligible)
Alternatives
Alternative 1: Keep Current Tiled-only Implementation
Pros: No breaking changes, minimal development effort
Cons: Continues to limit user choice, doesn't solve architectural problems
Alternative 2: Add Format-Specific APIs
Pros: No breaking changes to existing code
Cons: Massive API surface area, inconsistent user experience, high maintenance burden
Alternative 3: Community Extensions
Pros: No core library changes needed
Cons: Fragmented ecosystem, inconsistent quality, duplicated effort
Alternative 4: Minimal Abstraction Layer
Pros: Simpler implementation, smaller scope
Cons: Doesn't solve the underlying architectural issues, limited extensibility
Impact of Not Doing This
- MonoGame.Extended remains tied to a single tilemap tool
- Users miss out on features from other editors (like LDtk's world editor or Ogmo's streamlined workflow)
- Architectural debt continues to accumulate
- Opportunity cost of not improving the core tilemap experience
- Limited growth potential in the tilemap feature set
Open questions
-
Infinite Map Support: How should infinite/chunked maps be handled in the API? Should chunks be first-class citizens?
-
Animation System: Should tile animations be handled by the tilemap system or delegated to the animation system within MonoGame.Extended already?
-
Multi-World Support: For formats like LDtk that support multiple worlds/levels, should the API expose this as separate Tilemap objects or a hierarchical structure?
-
Performance Optimization: What optimizations should be built into the core API (spatial indexing, culling, etc.) vs. left to users?
-
Property Type System: Should custom property types (like LDtk enums or Tiled classes) be strongly typed or remain as generic property bags?
-
Memory Management: For large maps, should the API support streaming/paging of tile data, or assume all data fits in memory?