2D 地图编辑器






4.89/5 (19投票s)
使用图块创建和编辑 2D 地图

引言
这是一个简单的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
在其最简单的形式下,就是瓦片使用的图像或纹理。SourceX
和SourceY
是瓦片集中该瓦片开始处的像素坐标。XPos
和YPos
表示瓦片在地图中的实际位置。每个瓦片还有一个其他属性,我们可以用它来确定瓦片是否为实心;可以像这样向瓦片类添加任意数量的其他属性,例如,您可以添加一个属性来说明该瓦片是水,或者只应每隔一个星期二绘制一次。选择权在您。
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
,用于对地图的不同图层进行排序,以便它们可以按正确的顺序绘制。Name
和ID
属性用于在编辑器中标识图层。虽然图层的名称不必唯一,但其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>
来存储地图的图层,并添加一些额外的信息来完整描述地图。所以我们有几个关于地图宽度和高度的属性,Width
和Height
是地图的瓦片大小,RealWidth
和RealHeight
是地图的像素大小,通过使用当前瓦片大小计算得出。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
。这里有两个关键函数:ResetGraphicsDevice
和RenderMap
。第一个在窗体或地图大小调整时调用,并且可能需要更改任何缓冲区以适应新尺寸。第二个是从前面提到的委托通过MapPanel
控件中重写的OnPaint
调用的。
在最初启动编辑器时,我只使用了GDI+,但正如您可能知道的,这非常慢。我的意思是,对我来说,锁定位图中的位,复制数据出来进行编辑,然后复制回来并进行BitBlt
比使用GDI+更快,这绝不是夸张。我实际上写了一个方法来实现这个目的,以便我可以让GDI实现逐像素的alpha混合。然后我转向使用PInvoke和GDI函数BitBlt
来渲染瓦片,这要快得多。然而,GDI无法处理alpha通道,即使我写了一个函数来解决这个问题,它仍然太不切实际,所以我考虑使用DirectX并利用图形硬件来帮助渲染具有alpha混合的地图。事实证明,您可以将任何窗口句柄传递给XNA来创建绘图表面,包括Windows控件的句柄。所以现在我有两个实现IMapRenderer
接口的渲染器,一个使用GDI和GDI+,另一个使用XNA通过硬件加速的3D渲染。
我只能推荐使用XnaMapRenderer
,因为它消除了闪烁,速度更快,提供了alpha混合,并且更真实地反映了地图在实际游戏中的样子。但是,如果您出于某种原因喜欢受苦,并且因为边缘不平滑和大的洋红色块而让眼睛流血,那么请随意使用GdiMapRenderer
。
它们是如何协同工作的
在主窗体上,只有几个控件:TiledMap
和TileSelect
;后者是一个相当简单的控件,允许您加载图像,然后在图像上绘制网格。然后您就可以从网格中选择瓦片。当您在TileSelect
控件中选择一些瓦片时,会向主窗体发送一个事件,然后主窗体将此信息传递给TiledMap
控件。其余的用户输入由TiledMap
控件处理,该控件设置为接收鼠标和大小更改事件,并监视一些按键。还有一个选项对话框,主窗体负责控制。当您使用此对话框时,主窗体会收到一个事件,并将数据传递给TiledMap
,然后TiledMap
进行必要的更改。TiledMap
还使用GdiMapRenderer
或XnaMapRenderer
来渲染地图,控件本身没有实际的绘图代码。
您会发现,地图编辑器的大部分实际工作是在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日 - 编辑器重大更新