Introduction
This is a simple 2D map editor that shows how you can create and manage 2D tiled maps for games or other purposes. While some of the methods used to store and manage the data in the editor may not be directly applicable to a game, where memory management is very important, it does give an easy to understand and flexible starting point.
Background
A very long time ago, I made a quick n' dirty map editor for a project. I then completely rewrote the editor to post here. We are now on the third iteration of the editor. While it is very similar to the previous version, there are some great advancements.
Classes
The Editor is split into two projects; the MapEditor
WinForm which houses all of the controls and MapEditorLib
which provides most of the functionality. I'll go through the classes in the MapEditorLib
first.
Tile
public class Tile
{
public Tileset Tileset;
public int SourceX;
public int SourceY;
public int XPos;
public int YPos;
public bool IsSolid;
public Tile(Tileset tileSet, int srcX, int srcY, bool solid);
public Tile();
public void Clear();
public Tile Clone();
}
The Tile
class is the most basic class we need to create our map. It holds a reference to a Tileset
which is, in its most simple form, just the image or texture that the tile is using. SourceX
and SourceY
are the pixel coordinates in the tileset that this tile starts at. XPos
and YPos
represent the actual location in the map of this tile. Each tile also has another property which we can use to determine if a tile is solid or not; any number of other properties could be added to the tile class like this, for example you could have a property stating that the tile is water, or should only be drawn every second Tuesday. The choice is yours.
MapLayer
public class MapLayer : IComparable<maplayer>
{
public string Name
public int ID
public bool IsBackground
public float Z_Value
public bool Visible
public Tile[,] Tiles
public MapLayer(string Name, int ID, float ZValue, bool Background)
public void Resize(int width, int height)
public bool TileIsAlive(int X, int Y)
}
The next step up is the MapLayer
which is basically the whole map, or at least one layer of it. The main part of this class is the 2D array of Tile
, which represents the map itself. The second most important part is Z_Value
which is used to sort the different layers of the map so that they can be drawn in the correct order. The Name
and ID
properties are used to identify a layer in the editor. While a layer's name doesn't have to be unique, its ID does.
Each element in the array is only initialized when a tile is actually placed there, which is why we have the TileIsAlive
function, which can tell us if a tile is there and should be dealt with.
Map
public class Map
{
public delegate Image RequestTexture(string Name);
public Map()
public List<maplayer> MapLayers
public int RealWidth
public int RealHeight
public int Width
public int Height
public virtual int TileSize
public List<tileset> TileSets
public void Load(string Path, RequestTexture requestTexture)
public virtual void Load(Stream dataStream, RequestTexture requestTexture)
}
Here we are, the Map
class, this one is fairly important. This one uses a List<MapLayer>
to store the layers of the map, and adds a little more information that we need to fully describe the map. So we have a few more properties for the width and height of the map, Width
and Height
are the size in tiles of the map, RealWidth
and RealHeight
are the size in pixels of the map calculated by using the current tilesize. TileSize
is the size in pixels of each tile.
The Map
class also has functions for loading a map from file or from a stream. The map data that is saved to file only includes the name and ID of the Tileset
, so we need a way to get the image data. Depending if we are in a game or in the Editor, the image data could be in the memory or on disk somewhere. In the case of the Editor, the images are saved in a folder with the map data. We use the delegate RequestTexture
to get the image data from whatever application is using the Map
class. The application should be able to get us the data we need via its name.
EditableMap
public class EditableMap : Map
{
public delegate void MapChangedEventHandler(object sender, MapChangedEventArgs e);
public event MapChangedEventHandler OnMapChanged;
public class MapChangedEventArgs : EventArgs
public EditableMap(int NumLayers, int Width, int Height, int TileSize)
public EditableMap()
public MapLayer WorkingLayer
public void SetSize(int Width, int Height)
public void SetSelectedTiles(Tile[,] Tiles)
public void SetTiles(int PosX, int PosY)
public bool TileIsSolid(int X, int Y)
public void ToggleSolid(int X, int Y)
public void ClearTile(int X, int Y)
public string GetTilesetName(int X, int Y)
public bool LayerNameInUse(string Name)
public bool LayerZInUse(float Z_Value)
public void AddLayer(string Name, float Z_Value, bool Background)
public void SetLayerVisible(bool Visible)
public void SetLayerZValue(float Z_Value)
public void Save(string Path)
public void Save(Stream outputData)
public void SaveTextures(string outputFolder)
public void Reset()
}
This is the class that is used by the WinForms half of the application, it contains a plethora of functions to query and update the map. A very important property in here is WorkingLayer
; this is the layer in the map that is currently selected and the one to which any changes will be applied. Most of the functions here are pretty self explanatory, so I shouldn't need to go through them all. I will however mention SetSelectedTiles
; this function takes a 2D array of Tile
that are the currently selected tiles from the Tileselect
control (which I'll cover later). These are the tiles that will be placed on the map wherever the user clicks. We also have the Save
functions and the extra SaveTextures
function which saves the images stored in each of the Tileset
s to disk. Like I said earlier images are not stored in the map itself because they may already be loaded into memory or stored in an archive for a game, etc.
The OnMapChanged
event is fired whenever the map is updated, the main form registers for this event so that it knows when the map needs to be redrawn. I decided to make an event for this so that all calls to redraw the map would come from one place. It's worth mentioning that there is no code in the MapEditorLib
to render the map. It is currently held in the MapEditor
WinForms project.
Tileset
public class Tileset
{
public Microsoft.Xna.Framework.Graphics.Texture2D Texture
public IntPtr pImage
public int Width
public int Height
public Bitmap Image
public int ID
public string Name
public Tileset(Image image, string Name, int ID)
}
The last class in the MapEditorLib
whose main job is to store an image, as well as a name and a unique id as you can see. What I also assume you have noticed is that it stores an IntPtr
and a Texture2D
; these are not set by default but are actually set by one of the two rendering options I've made available; GDI and XNA. Depending on the renderer chosen, the appropriate values are setup and disposed of by the rendering routine; the Image
property is always set up.
There are only a couple of classes from the WinForms project that I need mention, the map renderers. In the MapEditor
project, there is a control MapPanel
which is an empty user control which overrides the regular paint methods and calls a delegate that can be assigned to different external functions. By doing this, we are able to change the methods used to render the map in one simple line, of course we need to set up the render first, which brings us to the map renderers interface.
IMapRenderer
interface IMapRenderer
{
void SetClearColour(byte R, byte G, byte B);
void SetGridColour(byte R, byte G, byte B);
void SetOverlayColour(byte R, byte G, byte B);
void SetGridVisible(bool value);
void SetSolidsVisible(bool value);
void SetOffset(int x, int y);
void ResetGraphicsDevice(int BufferWidth, int BufferHeight);
void RenderMap(PaintEventArgs e);
}
This is the interface
for any renderers that you want to add. The two key functions here are ResetGraphicsDevice
and RenderMap
. The first is called when the form or the map is resized and any buffers may need to be changed to accommodate the new dimensions. The second is called from the overridden OnPaint
in the MapPanel
control via the delegate that I mentioned earlier.
When initially starting the Editor, I simply used GDI+ but as you may know, this is ridiculously slow. I mean it, it was quicker for me to lock the bits in a bitmap, copy the data out and edit it, then copy it back in and unlock the bits and do a BitBlt
than it was to use GDI+ and that is no exaggeration. I actually wrote a method to do that so I could have per pixel alpha blending done in GDI. I then moved on to using PInvoke and the GDI function BitBlt
to render the tiles, which was much quicker. GDI though cannot handle alpha channels, and even though I wrote a function to get around this, it was still too impractical, so I looked into using DirectX and using the Graphics hardware to help render the map with alpha blending. It turns out you are able to pass any window handle to XNA to create a drawing surface, that includes the handles of Windows controls. So I now have two renderers that implement the IMapRenderer
interface, one that uses GDI and GDI+ and another that uses hardware accelerated 3D rendering through XNA.
I can only recommend using the XnaMapRenderer
as it eliminates flickering, is much quicker, provides you with alpha blending and is much more representative of what the map will be like in an actual game. However, if for some reason you like to suffer and have your eyes bleed at the lack of smooth edges and large magenta blocks, then feel free to make use of the GdiMapRenderer
.
How It All Fits Together
On the main form, there are only a couple of controls, the TiledMap
and the TileSelect
; the latter is a fairly simple control that allows you to load an image and then draws a grid over it. You are then able to pick tiles off of the grid. When you select some tiles in the TileSelect
control, an event is sent to the main form, which then passes this information over to the TiledMap
control. The rest of the user input is handled in the TiledMap
control, which is setup to receive the mouse and size changed events, as well as watching for a few key presses. There is also an options dialog that the main form is in control of. When you use this dialog the main form receives an event and passes the data on to the TiledMap
which then makes the necessary changes. The TiledMap
also makes use of either GdiMapRenderer
or XnaMapRenderer
to render the map, no actual drawing code is present on the control.
You will find that most of the actual work for the map editor is done between the EditableMap
and one of the two renderers.
Using the Code
Once you've made the map and saved it, if you want to use it in some game or other application, simply reference the MapEditorLib
and use the Map
class to load up your map. If you want to make your own map editor using this library, then once again simply reference the DLL and make use of the EditableMap
As in older versions of the Editor, the map data is saved out to file sequentially, there are no extra values or descriptions you need to look at when loading the file, so making a loader for a C++ application should be fairly simple.
For your convenience, here is some pseudo code for how the map is written out, so you can understand how to read the file format:
writeFileHeader();
writeInt(Map width);
writeInt(Map height);
writeInt(Map tilesize);
writeInt(Number of tilesets);
foreach(Tileset ts in list of tilesets)
{
writeInt(tileset ID, outputData);
writeString(tileset Name, outputData);
}
writeInt(Number of map layers);
foreach(MapLayer layer in list of layers)
{
writeInt(layer ID);
writeFloat(layer Z Value);
writeBool(layer is background);
writeBool(layer visible);
writeString(layer name);
for(int x=0; x<map width; x++) {
for(int y=0; y<map height; y++) {
if (if tile at [x,y] is null)
{
writeBool(false);
}
else
{
writeBool(true);
writeInt(tiles[x, y] SourceX);
writeInt(tiles[x, y] SourceY);
writeInt(tiles[x, y] XPos);
writeInt(tiles[x, y] YPos);
writeInt(tiles[x, y] Tileset.ID);
writeBool(tiles[x, y] IsSolid);
}
}
}
}
writeInt(0xFF);
History
- 18th May, 2008 - Article posted
- 7th October, 2009 - Major update to the Editor