Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

2D Map Editor

4.89/5 (21 votes)
8 Oct 2009CPOL9 min read 1   6.8K  
Create and edit 2D maps using tiles
MapEditor

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

C#
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

C#
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

C#
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

C#
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 Tilesets 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

C#
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

C#
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:

C#
writeFileHeader(); 	//Writes out a 4 byte code to identify the filetype, 
			//and stores the version number
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); //Used to find image data when tileset is loaded
}

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); //You can ignore this when reading in the data for a game
  writeString(layer name);

  //Write out all of the tile data
  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); //Skip this tile when loading
      }
      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); //Used to indicate the end of the data

History

  • 18th May, 2008 - Article posted
  • 7th October, 2009 - Major update to the Editor

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)