About me

My name is Fabian Pas, I'm a 21 year old Software Engineer to-be, currently completing my graduation internship. I'm interested in .NET development, webdevelopment (I am a Laravel enthousiast), a VJ and a learning theatre technician.

Custom Tiled MonoGame pipeline

Based on the article by Dylan Wilson, I wrote a custom pipeline importer for the Tiled map editor maps as Json. You can read Wilson's article on how to set up the custom importer in the Content Pipeline. To read up on the pipeline tool, check the MonoGame documentation.

All code is publicly available in the Github repository.

using System.Diagnostics;  
using System.IO;  
using Microsoft.Xna.Framework.Content.Pipeline;  
using Newtonsoft.Json;

namespace TiledMapPipeline  
{
    [ContentImporter(".json", DefaultProcessor = "TiledMapProcessor",
    DisplayName = "Tiled Map Importer")]
    public class TiledMapImporter : ContentImporter<TiledMap>
    {
        public override TiledMap Import(string filename, ContentImporterContext context)
        {
            context.Logger.LogMessage("Importing JSON map: {0}", filename);
            using (var file = File.OpenText(filename))
            {
                var serializer = new JsonSerializer();
                var serializedMap = (TiledMap) serializer.Deserialize(file, typeof (TiledMap));

                return serializedMap;
            }
        }
    }
}

Above class will read our imported JSON file and deserialize it to our TiledMap object using Json.NET.

using System.Collections.Generic;  
using Newtonsoft.Json;

namespace TiledMapPipeline  
{
    public class TiledMap
    {
        [JsonProperty("width")]
        public int Width;

        [JsonProperty("height")]
        public int Height;

        [JsonProperty("layers")]
        public List<TiledLayer> Layers;

        [JsonProperty("tilesets")]
        public List<TiledTileset> Tilesets;
    }
}

Our TiledMap class that the imported json file is deserialized to. It's fairly basic right now but easy to extend. I'm currently only reading the properties that I require for rendering my tilemap.

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using Newtonsoft.Json;

namespace TiledMapPipeline  
{
    public class TiledLayer
    {
        [JsonProperty("name")]
        public string Name;

        [JsonProperty("data")]
        public List<int> Data;
    }
}
using Newtonsoft.Json;

namespace TiledMapPipeline  
{
    public class TiledTileset
    {
        [JsonProperty("image")]
        public string Image;

        [JsonProperty("imagewidth")]
        public int Width;

        [JsonProperty("imageheight")]
        public int Height;
    }
}

Above classes are parts of the root TiledMap class. Both are lists of objects in the Json file.

using Microsoft.Xna.Framework.Content.Pipeline;  
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;  
using Microsoft.Xna.Framework.Graphics;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;

namespace TiledMapPipeline  
{
    [ContentProcessor(DisplayName = "Tiled Map Processor")]
    public class TiledMapProcessor : ContentProcessor<TiledMap, TiledMapProcessorResult>
    {
        public override TiledMapProcessorResult Process(TiledMap map, ContentProcessorContext context)
        {
            return new TiledMapProcessorResult(map, context.Logger);
        }
    }
}

This is the ContentProcessor. It allows you to do an extra cleanup to whatever you are writing to the binary format. I'm currently not utilizing and return the result immediately.

using Microsoft.Xna.Framework.Content.Pipeline;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;

namespace TiledMapPipeline  
{
    public class TiledMapProcessorResult
    {
        public TiledMap Map;
        public ContentBuildLogger Logger;

        public TiledMapProcessorResult(TiledMap map, ContentBuildLogger logger)
        {
            Map = map;
            Logger = logger;
        }
    }
}

This is the result which is returned to the ContentWriter.

using Microsoft.Xna.Framework.Content.Pipeline;  
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;

namespace TiledMapPipeline  
{
    [ContentTypeWriter]
    public class TiledMapWriter : ContentTypeWriter<TiledMapProcessorResult>
    {
        protected override void Write(ContentWriter output, TiledMapProcessorResult value)
        {
            output.Write(value.Map.Width);
            output.Write(value.Map.Height);

            output.Write(value.Map.Layers.Count);

            foreach(var layer in value.Map.Layers)
            {
                output.Write(layer.Name);
                output.Write(layer.Data.Count);

                foreach(var tile in layer.Data)
                {
                    output.Write(tile);
                }
            }

            output.Write(value.Map.Tilesets.Count);

            foreach(var tileset in value.Map.Tilesets)
            {
                output.Write(tileset.Width);
                output.Write(tileset.Height);
                output.Write(tileset.Image);
            }
        }

        public override string GetRuntimeType(TargetPlatform targetPlatform)
        {
            return typeof(TiledMap).AssemblyQualifiedName;
        }

        public override string GetRuntimeReader(TargetPlatform targetPlatform)
        {
            return "Lidige.Maps.MapReader, Lidige";
        }
    }
}

This is the writer that writes the deserialized Json file, now objects, to the binary .xnb format. You can return the runtime type dynamically with reflection. I did get errors using the same reflection for the reader namespace, that's why it's statically typed. Those getters are barely documented by MSDN or MonoGame and due to that a bit hard to interpret.

using Microsoft.Xna.Framework.Content;  
using Microsoft.Xna.Framework.Graphics;  
using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;

namespace Lidige.Maps  
{
    public class MapReader : ContentTypeReader<Map>
    {
        protected override Map Read(ContentReader input, Map existingInstance)
        {
            var width = input.ReadInt32();
            var height = input.ReadInt32();

            var map = new Map(width, height);

            var layerCount = input.ReadInt32();
            for(int i = 0; i < layerCount; i++)
            {
                var layerName = input.ReadString();
                var tileCount = input.ReadInt32();

                var layerTiles = new int[width][];

                for (int k = 0; k < height; k++)
                    layerTiles[k] = new int[height];

                for(int j = 0; j < tileCount; j++)
                {
                    var tileId = input.ReadInt32();

                    var x = j % width;
                    var y = j / width;
                    layerTiles[x][y] = tileId;
                }

                var layer = new Layer(layerName, layerTiles);
                map.Layers.Add(layer);
            }

            var tilesetCount = input.ReadInt32();
            for(int k = 0; k < tilesetCount; k++)
            {
                var tilesetWidth = input.ReadInt32();
                var tilesetHeight = input.ReadInt32();
                var tilesetImage = input.ReadString();

                var texture = input.ContentManager.Load<Texture2D>("Tilesets/" + tilesetImage.Replace(".png", ""));
                var tileset = new Tileset(tilesetWidth, tilesetHeight, texture);

                map.Tilesets.Add(tileset);
            }

            return map;
        }
    }
}

The above class is the ContentTypeReader. It is used in the actual game project and thus not in the same namespace as the ContentImporter. I am not including all returning objects as they are publicly available on the repository.

This is how I wrote my custom content import for the MonoGame pipeline tool. Most online articles use the Tiled .tmx format, but I prefer the readible Json format to make quick and easy changes to my maps.

Questions? @fabianPas