diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/.config/dotnet-tools.json b/src/DotTiled.Examples/DotTiled.Example.MonoGame/.config/dotnet-tools.json
new file mode 100644
index 0000000..afd4e2c
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/.config/dotnet-tools.json
@@ -0,0 +1,36 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-mgcb": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb"
+ ]
+ },
+ "dotnet-mgcb-editor": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor"
+ ]
+ },
+ "dotnet-mgcb-editor-linux": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-linux"
+ ]
+ },
+ "dotnet-mgcb-editor-windows": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-windows"
+ ]
+ },
+ "dotnet-mgcb-editor-mac": {
+ "version": "3.8.3",
+ "commands": [
+ "mgcb-editor-mac"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Camera2D.cs b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Camera2D.cs
new file mode 100644
index 0000000..58f724b
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Camera2D.cs
@@ -0,0 +1,52 @@
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace DotTiled.Example.MonoGame;
+
+public class Camera2D
+{
+ public float Zoom { get; set; }
+ public Vector2 Position { get; set; }
+ public Rectangle Bounds { get; protected set; }
+ public Rectangle VisibleArea { get; protected set; }
+ public Matrix Transform { get; protected set; }
+
+ public Camera2D(Viewport viewport)
+ {
+ Bounds = viewport.Bounds;
+ Zoom = 1f;
+ Position = Vector2.Zero;
+ }
+
+ private void UpdateVisibleArea()
+ {
+ var inverseViewMatrix = Matrix.Invert(Transform);
+
+ var tl = Vector2.Transform(Vector2.Zero, inverseViewMatrix);
+ var tr = Vector2.Transform(new Vector2(Bounds.X, 0), inverseViewMatrix);
+ var bl = Vector2.Transform(new Vector2(0, Bounds.Y), inverseViewMatrix);
+ var br = Vector2.Transform(new Vector2(Bounds.Width, Bounds.Height), inverseViewMatrix);
+
+ var min = new Vector2(
+ MathHelper.Min(tl.X, MathHelper.Min(tr.X, MathHelper.Min(bl.X, br.X))),
+ MathHelper.Min(tl.Y, MathHelper.Min(tr.Y, MathHelper.Min(bl.Y, br.Y))));
+ var max = new Vector2(
+ MathHelper.Max(tl.X, MathHelper.Max(tr.X, MathHelper.Max(bl.X, br.X))),
+ MathHelper.Max(tl.Y, MathHelper.Max(tr.Y, MathHelper.Max(bl.Y, br.Y))));
+ VisibleArea = new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y));
+ }
+
+ private void UpdateMatrix()
+ {
+ Transform = Matrix.CreateTranslation(new Vector3(-Position.X, -Position.Y, 0)) *
+ Matrix.CreateScale(Zoom) *
+ Matrix.CreateTranslation(new Vector3(Bounds.Width * 0.5f, Bounds.Height * 0.5f, 0));
+ UpdateVisibleArea();
+ }
+
+ public void UpdateCamera(Viewport bounds)
+ {
+ Bounds = bounds.Bounds;
+ UpdateMatrix();
+ }
+}
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/Content.mgcb b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/Content.mgcb
new file mode 100644
index 0000000..2a28765
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/Content.mgcb
@@ -0,0 +1,33 @@
+
+#----------------------------- Global Properties ----------------------------#
+
+/outputDir:bin/$(Platform)
+/intermediateDir:obj/$(Platform)
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#-------------------------------- References --------------------------------#
+
+
+#---------------------------------- Content ---------------------------------#
+
+#begin kenney_roguelike-rpg-pack/roguelikeSheet_transparent.png
+/importer:TextureImporter
+/processor:TextureProcessor
+/processorParam:ColorKeyColor=255,0,255,255
+/processorParam:ColorKeyEnabled=True
+/processorParam:GenerateMipmaps=False
+/processorParam:PremultiplyAlpha=True
+/processorParam:ResizeToPowerOfTwo=False
+/processorParam:MakeSquare=False
+/processorParam:TextureFormat=Color
+/build:kenney_roguelike-rpg-pack/roguelikeSheet_transparent.png
+
+#begin world-tileset.tsx
+/copy:world-tileset.tsx
+
+#begin world.tmx
+/copy:world.tmx
+
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/kenney_roguelike-rpg-pack/License.txt b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/kenney_roguelike-rpg-pack/License.txt
new file mode 100644
index 0000000..3b4c9db
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/kenney_roguelike-rpg-pack/License.txt
@@ -0,0 +1,23 @@
+
+###############################################################################
+
+
+ Roguelike pack
+ by Kenney Vleugels for Kenney (www.kenney.nl)
+ with help by Lynn Evers (Twitter: @EversLynn)
+
+ ------------------------------
+
+ License (Creative Commons Zero, CC0)
+ http://creativecommons.org/publicdomain/zero/1.0/
+
+ You may use these graphics in personal and commercial projects.
+ Credit (Kenney or www.kenney.nl) would be nice but is not mandatory.
+
+ ------------------------------
+
+ Donate: http://donate.kenney.nl/
+ Request: http://request.kenney.nl/
+
+
+###############################################################################
\ No newline at end of file
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/kenney_roguelike-rpg-pack/roguelikeSheet_transparent.png b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/kenney_roguelike-rpg-pack/roguelikeSheet_transparent.png
new file mode 100644
index 0000000..79b1332
Binary files /dev/null and b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/kenney_roguelike-rpg-pack/roguelikeSheet_transparent.png differ
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/world-tileset.tsx b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/world-tileset.tsx
new file mode 100644
index 0000000..ee710e9
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/world-tileset.tsx
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/world.tmx b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/world.tmx
new file mode 100644
index 0000000..6056474
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Content/world.tmx
@@ -0,0 +1,434 @@
+
+
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/DotTiled.Example.MonoGame.csproj b/src/DotTiled.Examples/DotTiled.Example.MonoGame/DotTiled.Example.MonoGame.csproj
new file mode 100644
index 0000000..07a4569
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/DotTiled.Example.MonoGame.csproj
@@ -0,0 +1,36 @@
+
+
+ Exe
+ net8.0
+ Major
+ false
+ false
+
+
+ app.manifest
+ Icon.ico
+
+
+
+
+
+
+
+ Icon.ico
+
+
+ Icon.bmp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Game1.cs b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Game1.cs
new file mode 100644
index 0000000..3c38bf8
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Game1.cs
@@ -0,0 +1,231 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using DotTiled.Serialization;
+using Microsoft.Xna.Framework;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+
+namespace DotTiled.Example.MonoGame;
+
+///
+/// A beginner-friendly example of using DotTiled with MonoGame.
+/// Loads a Tiled map, renders its layers, and allows basic player movement with collision.
+///
+public class Game1 : Game
+{
+ private readonly GraphicsDeviceManager _graphics;
+ private SpriteBatch _spriteBatch;
+
+ // Camera for following the player
+ private Camera2D _camera;
+
+ // DotTiled map and tileset textures
+ private Map _map;
+ private Dictionary _tilesetTextures;
+
+ // Layers for collisions and points of interest
+ private ObjectLayer _collisionLayer;
+ private ObjectLayer _pointsOfInterestLayer;
+
+ // Player state
+ private Vector2 _playerPosition;
+ private Texture2D _playerTexture;
+
+ // Used for drawing rectangles (player)
+ private Texture2D _whitePixel;
+
+ public Game1()
+ {
+ _graphics = new GraphicsDeviceManager(this)
+ {
+ PreferredBackBufferWidth = 1280,
+ PreferredBackBufferHeight = 720
+ };
+ Content.RootDirectory = "Content";
+ IsMouseVisible = true;
+ }
+
+ ///
+ /// MonoGame initialization.
+ ///
+ protected override void Initialize() => base.Initialize();
+
+ ///
+ /// Load map, textures, and initialize player/camera.
+ ///
+ protected override void LoadContent()
+ {
+ _spriteBatch = new SpriteBatch(GraphicsDevice);
+
+ // Used for drawing rectangles (player)
+ _whitePixel = new Texture2D(GraphicsDevice, 1, 1);
+ _whitePixel.SetData(new[] { Color.White });
+
+ // Load the Tiled map using DotTiled
+ var loader = Loader.Default();
+ _map = loader.LoadMap(Path.Combine(Content.RootDirectory, "world.tmx"));
+
+ // Load all tileset textures referenced by the map
+ _tilesetTextures = LoadTilesetTextures(_map);
+
+ // Extract layers for collisions and points of interest
+ _collisionLayer = _map.Layers.OfType().Single(l => l.Name == "Collisions");
+ _pointsOfInterestLayer = (ObjectLayer)_map.Layers.Single(l => l.Name == "PointsOfInterest");
+
+ // Get the player's spawn point from the PointsOfInterest layer
+ var playerSpawn = _pointsOfInterestLayer.Objects.Single(obj => obj.Name == "PlayerSpawn");
+ _playerPosition = new Vector2(playerSpawn.X, playerSpawn.Y);
+
+ // Set up the camera to follow the player
+ _camera = new Camera2D(GraphicsDevice.Viewport);
+
+ // Optionally, create a simple player texture (blue rectangle)
+ _playerTexture = new Texture2D(GraphicsDevice, 1, 1);
+ _playerTexture.SetData(new[] { Color.Blue });
+ }
+
+ ///
+ /// Loads all tileset textures referenced by the map.
+ ///
+ private Dictionary LoadTilesetTextures(Map map)
+ {
+ // Remove ".png" for MonoGame Content Pipeline compatibility
+ return map.Tilesets.ToDictionary(
+ tileset => tileset.Image.Value.Source.Value,
+ tileset => Content.Load(Path.GetDirectoryName(tileset.Image.Value.Source.Value) + "/" + Path.GetFileNameWithoutExtension(tileset.Image.Value.Source.Value))
+ );
+ }
+
+ ///
+ /// Handles player input and updates game logic.
+ ///
+ protected override void Update(GameTime gameTime)
+ {
+ // Exit on Escape
+ if (Keyboard.GetState().IsKeyDown(Keys.Escape))
+ Exit();
+
+ _camera.UpdateCamera(GraphicsDevice.Viewport);
+
+ // Handle player movement input
+ var move = HandlePlayerInput(gameTime);
+
+ // Define the player's collision rectangle
+ var playerRect = new Rectangle((int)_playerPosition.X, (int)_playerPosition.Y, 12, 12);
+
+ // Collision detection with rectangles in the collision layer
+ foreach (var obj in _collisionLayer.Objects.OfType())
+ {
+ var objRect = new Rectangle((int)obj.X, (int)obj.Y, (int)obj.Width, (int)obj.Height);
+
+ // Horizontal collision
+ var movePlayerHRect = new Rectangle(playerRect.X + (int)move.X, playerRect.Y, playerRect.Width, playerRect.Height);
+ if (move.X != 0 && movePlayerHRect.Intersects(objRect))
+ move.X = 0;
+
+ // Vertical collision
+ var movePlayerVRect = new Rectangle(playerRect.X, playerRect.Y + (int)move.Y, playerRect.Width, playerRect.Height);
+ if (move.Y != 0 && movePlayerVRect.Intersects(objRect))
+ move.Y = 0;
+ }
+
+ // Update player position
+ _playerPosition += move;
+
+ // Smoothly update the camera to follow the player
+ var newCameraTarget = new Vector2(_playerPosition.X, _playerPosition.Y);
+ _camera.Position += (newCameraTarget - _camera.Position) * 15f * (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ base.Update(gameTime);
+ }
+
+ ///
+ /// Handles WASD input for player movement.
+ ///
+ private static Vector2 HandlePlayerInput(GameTime gameTime)
+ {
+ var move = Vector2.Zero;
+ var speed = 150f * (float)gameTime.ElapsedGameTime.TotalSeconds;
+ var state = Keyboard.GetState();
+
+ if (state.IsKeyDown(Keys.W)) move.Y -= speed;
+ if (state.IsKeyDown(Keys.S)) move.Y += speed;
+ if (state.IsKeyDown(Keys.A)) move.X -= speed;
+ if (state.IsKeyDown(Keys.D)) move.X += speed;
+
+ return move;
+ }
+
+ ///
+ /// Draws the map, player, and handles layer ordering.
+ ///
+ protected override void Draw(GameTime gameTime)
+ {
+ GraphicsDevice.Clear(Color.Black);
+
+ _spriteBatch.Begin(transformMatrix: _camera.Transform);
+
+ // Get all visual tile layers from the "Visuals" group
+ var visualLayers = _map.Layers.OfType().Single(l => l.Name == "Visuals").Layers.OfType();
+
+ // Render layers below the player
+ RenderLayers(_map, visualLayers, _tilesetTextures, ["Ground", "Ponds", "Paths", "HouseWalls", "HouseDoors", "FencesBushes"]);
+
+ // Draw the player as a blue rectangle (centered on tile)
+ var playerVisualRect = new Rectangle((int)_playerPosition.X, (int)_playerPosition.Y - 12, 12, 24);
+ _spriteBatch.Draw(_playerTexture, playerVisualRect, Color.White);
+
+ // Render layers above the player
+ RenderLayers(_map, visualLayers, _tilesetTextures, ["HouseRoofs"]);
+
+ _spriteBatch.End();
+
+ base.Draw(gameTime);
+ }
+
+ ///
+ /// Renders specific named layers from the map.
+ ///
+ private void RenderLayers(Map map, IEnumerable visualLayers, Dictionary tilesetTextures, string[] layerNames)
+ {
+ foreach (var layerName in layerNames)
+ {
+ var layer = visualLayers.Single(l => l.Name == layerName);
+ RenderLayer(map, layer, tilesetTextures);
+ }
+ }
+
+ ///
+ /// Renders a single tile layer.
+ ///
+ private void RenderLayer(Map map, TileLayer layer, Dictionary tilesetTextures)
+ {
+ for (var y = 0; y < layer.Height; y++)
+ {
+ for (var x = 0; x < layer.Width; x++)
+ {
+ var tileGID = layer.GetGlobalTileIDAtCoord(x, y);
+ if (tileGID == 0) continue;
+
+ var tileset = map.ResolveTilesetForGlobalTileID(tileGID, out var localTileID);
+ var sourceRect = tileset.GetSourceRectangleForLocalTileID(localTileID);
+
+ var destination = new Vector2(x * tileset.TileWidth, y * tileset.TileHeight);
+ var sourceRectangle = new Rectangle(sourceRect.X + 1, sourceRect.Y + 1, sourceRect.Width - 2, sourceRect.Height - 2);
+
+ _spriteBatch.Draw(
+ tilesetTextures[tileset.Image.Value.Source.Value],
+ destination,
+ sourceRectangle,
+ Color.White,
+ 0f,
+ Vector2.Zero,
+ Vector2.One * (1f / (14f / 16f)), // Shrink by a tiny amount to avoid seams (not a perfect solution)
+ SpriteEffects.None,
+ 0f
+ );
+ }
+ }
+ }
+}
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Icon.bmp b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Icon.bmp
new file mode 100644
index 0000000..2b48165
Binary files /dev/null and b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Icon.bmp differ
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Icon.ico b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Icon.ico
new file mode 100644
index 0000000..7d9dec1
Binary files /dev/null and b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Icon.ico differ
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/Program.cs b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Program.cs
new file mode 100644
index 0000000..399af18
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/Program.cs
@@ -0,0 +1,2 @@
+using var game = new DotTiled.Example.MonoGame.Game1();
+game.Run();
diff --git a/src/DotTiled.Examples/DotTiled.Example.MonoGame/app.manifest b/src/DotTiled.Examples/DotTiled.Example.MonoGame/app.manifest
new file mode 100644
index 0000000..b399c95
--- /dev/null
+++ b/src/DotTiled.Examples/DotTiled.Example.MonoGame/app.manifest
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
+
diff --git a/src/DotTiled.sln b/src/DotTiled.sln
index 5505021..add5504 100644
--- a/src/DotTiled.sln
+++ b/src/DotTiled.sln
@@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DotTiled.Examples", "DotTil
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Example.Raylib", "DotTiled.Examples\DotTiled.Example.Raylib\DotTiled.Example.Raylib.csproj", "{53585FB8-6E94-46F0-87E2-9692874E1714}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Example.MonoGame", "DotTiled.Examples\DotTiled.Example.MonoGame\DotTiled.Example.MonoGame.csproj", "{F074756C-F84C-4F50-AE42-01017CD68AF7}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -52,10 +54,15 @@ Global
{53585FB8-6E94-46F0-87E2-9692874E1714}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53585FB8-6E94-46F0-87E2-9692874E1714}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53585FB8-6E94-46F0-87E2-9692874E1714}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F074756C-F84C-4F50-AE42-01017CD68AF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F074756C-F84C-4F50-AE42-01017CD68AF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F074756C-F84C-4F50-AE42-01017CD68AF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F074756C-F84C-4F50-AE42-01017CD68AF7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67} = {8C54542E-3C2C-486C-9BEF-4C510391AFDA}
{7541A9B3-43A5-45A7-939E-6F542319D990} = {8C54542E-3C2C-486C-9BEF-4C510391AFDA}
{53585FB8-6E94-46F0-87E2-9692874E1714} = {F3D6E648-AF8F-4EC9-A810-8C348DBB9924}
+ {F074756C-F84C-4F50-AE42-01017CD68AF7} = {F3D6E648-AF8F-4EC9-A810-8C348DBB9924}
EndGlobalSection
EndGlobal