65.9K
CodeProject 正在变化。 阅读更多。
Home

2D 地图编辑器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (19投票s)

2008年5月18日

CPOL

9分钟阅读

viewsIcon

191815

downloadIcon

6826

使用图块创建和编辑 2D 地图

MapEditor

引言

这是一个简单的2D地图编辑器,展示了如何为游戏或其他目的创建和管理2D瓦片地图。虽然编辑器中用于存储和管理数据的方法可能不直接适用于内存管理至关重要的游戏,但它提供了一个易于理解且灵活的起点。

背景

很久以前,我为一个项目制作了一个快速粗糙的地图编辑器。然后我完全重写了编辑器来发布。现在已经是第三版编辑器了。虽然它与前一版本非常相似,但也有一些重大的改进。

编辑器分为两个项目;MapEditor WinForm项目包含所有控件,MapEditorLib项目提供大部分功能。我将首先介绍MapEditorLib中的类。

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();
}

Tile类是我们创建地图所需的最基础的类。它包含一个对Tileset的引用,Tileset在其最简单的形式下,就是瓦片使用的图像或纹理。SourceXSourceY是瓦片集中该瓦片开始处的像素坐标。XPosYPos表示瓦片在地图中的实际位置。每个瓦片还有一个其他属性,我们可以用它来确定瓦片是否为实心;可以像这样向瓦片类添加任意数量的其他属性,例如,您可以添加一个属性来说明该瓦片是水,或者只应每隔一个星期二绘制一次。选择权在您。

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)
}

再往上一层是MapLayer,它基本上就是整个地图,或者至少是地图的一个图层。这个类的主体是Tile的二维数组,它表示地图本身。第二重要部分是Z_Value,用于对地图的不同图层进行排序,以便它们可以按正确的顺序绘制。NameID属性用于在编辑器中标识图层。虽然图层的名称不必唯一,但其ID必须唯一。

数组中的每个元素仅在实际放置瓦片时才被初始化,这就是为什么我们有TileIsAlive函数,它可以告诉我们瓦片是否存在并且应该被处理。

映射

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)
}

这就是Map类,这个类相当重要。它使用List<MapLayer>来存储地图的图层,并添加一些额外的信息来完整描述地图。所以我们有几个关于地图宽度和高度的属性,WidthHeight是地图的瓦片大小,RealWidthRealHeight是地图的像素大小,通过使用当前瓦片大小计算得出。TileSize是每个瓦片的像素大小。

Map类还具有从文件或流加载地图的函数。保存到文件的地图数据仅包含Tileset的名称和ID,因此我们需要一种方法来获取图像数据。根据我们在游戏中还是在编辑器中,图像数据可能在内存中或磁盘上的某个位置。在编辑器的情况下,图像与地图数据一起保存在一个文件夹中。我们使用委托RequestTexture从使用Map类的任何应用程序获取图像数据。应用程序应该能够通过其名称向我们提供所需的数据。

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()
}

这是应用程序的WinForms部分使用的类,它包含大量用于查询和更新地图的函数。这里一个非常重要的属性是WorkingLayer;这是地图中当前选中的图层,任何更改都将应用于该图层。这里的大部分函数都非常直观,所以我应该不需要一一介绍。但我会提到SetSelectedTiles;这个函数接收一个Tile的二维数组,这些是来自Tileselect控件(我稍后会讲到)的当前选中的瓦片。这些瓦片将被放置在用户点击的任何地方。我们还有Save函数和额外的SaveTextures函数,它将每个Tileset中存储的图像保存到磁盘。就像我之前说的,图像不存储在地图本身中,因为它们可能已经加载到内存中或存储在游戏存档中等。

每当地图更新时,就会触发OnMapChanged事件,主窗体注册此事件,以便知道何时需要重绘地图。我决定为此创建一个事件,以便所有重绘地图的调用都来自一个地方。值得一提的是,MapEditorLib中没有渲染地图的代码。它目前由MapEditor WinForms项目处理。

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)
}

MapEditorLib中的最后一个类,其主要工作是存储图像,以及名称和唯一ID,如您所见。我假设您还注意到它存储了一个IntPtr和一个Texture2D;它们不是默认设置的,而是由我提供的两个渲染选项之一设置的:GDI和XNA。根据选择的渲染器,相应的值由渲染例程设置和处置;Image属性始终已设置。

我只需要提一下WinForms项目中的几个类,就是地图渲染器。在MapEditor项目中,有一个控件MapPanel,它是一个空的user control,它覆盖了常规的paint方法并调用一个可以分配给不同外部函数的委托。通过这样做,我们可以用简单的一行代码更改用于渲染地图的方法,当然,我们需要先设置渲染器,这就引出了地图渲染器的接口。

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);
}

这是您要添加的任何渲染器的interface。这里有两个关键函数:ResetGraphicsDeviceRenderMap。第一个在窗体或地图大小调整时调用,并且可能需要更改任何缓冲区以适应新尺寸。第二个是从前面提到的委托通过MapPanel控件中重写的OnPaint调用的。

在最初启动编辑器时,我只使用了GDI+,但正如您可能知道的,这非常慢。我的意思是,对我来说,锁定位图中的位,复制数据出来进行编辑,然后复制回来并进行BitBlt比使用GDI+更快,这绝不是夸张。我实际上写了一个方法来实现这个目的,以便我可以让GDI实现逐像素的alpha混合。然后我转向使用PInvoke和GDI函数BitBlt来渲染瓦片,这要快得多。然而,GDI无法处理alpha通道,即使我写了一个函数来解决这个问题,它仍然太不切实际,所以我考虑使用DirectX并利用图形硬件来帮助渲染具有alpha混合的地图。事实证明,您可以将任何窗口句柄传递给XNA来创建绘图表面,包括Windows控件的句柄。所以现在我有两个实现IMapRenderer接口的渲染器,一个使用GDI和GDI+,另一个使用XNA通过硬件加速的3D渲染。

我只能推荐使用XnaMapRenderer,因为它消除了闪烁,速度更快,提供了alpha混合,并且更真实地反映了地图在实际游戏中的样子。但是,如果您出于某种原因喜欢受苦,并且因为边缘不平滑和大的洋红色块而让眼睛流血,那么请随意使用GdiMapRenderer

它们是如何协同工作的

在主窗体上,只有几个控件:TiledMapTileSelect;后者是一个相当简单的控件,允许您加载图像,然后在图像上绘制网格。然后您就可以从网格中选择瓦片。当您在TileSelect控件中选择一些瓦片时,会向主窗体发送一个事件,然后主窗体将此信息传递给TiledMap控件。其余的用户输入由TiledMap控件处理,该控件设置为接收鼠标和大小更改事件,并监视一些按键。还有一个选项对话框,主窗体负责控制。当您使用此对话框时,主窗体会收到一个事件,并将数据传递给TiledMap,然后TiledMap进行必要的更改。TiledMap还使用GdiMapRendererXnaMapRenderer来渲染地图,控件本身没有实际的绘图代码。

您会发现,地图编辑器的大部分实际工作是在EditableMap和两个渲染器之间完成的。

Using the Code

一旦您制作并保存了地图,如果您想在某个游戏或其他应用程序中使用它,只需引用MapEditorLib并使用Map类来加载您的地图。如果您想使用此库制作自己的地图编辑器,那么再次只需引用DLL并使用EditableMap

与旧版本编辑器一样,地图数据是按顺序保存到文件的,加载文件时不需要查看任何额外的数值或描述,因此为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

历史

  • 2008年5月18日 - 发布文章
  • 2009年10月7日 - 编辑器重大更新
© . All rights reserved.