1.游戏编程,需要一个地图编辑器。站在前人肩上,自己写一个,倒也不难。其效果图如下:
只需从左下的 Tiles 面板中选择一个 Tile,在右边的面板中绘制即可。保存为同名的两个文件 file.bmp, file.xml。打开时选择 file.xml 文件。
2.界面设计不详述,可在效果图上点击右键,将其另存为桌面上作为参照。其完整代码如下:
public partial class MainForm : Form, IMessageFilter { int paletteColumes = 5, paletteSelectedIndex = 0; readonly int mapSize = 100; //for map tiles int mouseX = 0, mouseY = 0, boxCol = 0, boxRow = 0; Tile[] tiles; Bitmap bmpMap, bmpTile; Graphics graphMap, graphTile; BoxInfo boxSelected; // editing selected box in the map public MainForm() { InitializeComponent(); Application.AddMessageFilter(this); tiles = new Tile[mapSize * mapSize]; bmpMap = new Bitmap(picMap.Size.Width, picMap.Size.Height); picMap.Image = bmpMap; graphMap = Graphics.FromImage(bmpMap); bmpTile = new Bitmap(picSelected.Size.Width, picSelected.Size.Height); picSelected.Image = bmpTile; graphTile = Graphics.FromImage(bmpTile); // Init Events picMap.MouseClick += new MouseEventHandler(picMap_MouseClick); picMap.MouseMove += new MouseEventHandler(picMap_MouseMove); picPalette.MouseClick += new MouseEventHandler(picPalette_MouseClick); dlgOpen.Filter = "XML Files(*.xml)|*.xml|All Files(*.*)|*.*"; miNew.Click += new EventHandler(miNew_Click); miOpen.Click += new EventHandler(miOpen_Click); miSave.Click += new EventHandler(miSave_Click); miExit.Click += (a, b) => Close(); radioEditMode.CheckedChanged += new EventHandler(radioEditMode_CheckedChanged); } void radioEditMode_CheckedChanged(object sender, EventArgs e) { if (radioEditMode.Checked == false) { int i = boxSelected.Index; tiles[i].TileId = Convert.ToInt32(txtTileId.Text); tiles[i].Data1 = txtData1.Text; tiles[i].Data2 = txtData2.Text; tiles[i].Data3 = txtData3.Text; tiles[i].Data4 = txtData4.Text; tiles[i].Collision = chkCollision.Checked; tiles[i].Portal = chkPortal.Checked; tiles[i].PortalX = Convert.ToInt32(txtPortalX.Text); tiles[i].PortalY = Convert.ToInt32(txtPortalY.Text); tiles[i].PortalFile = txtPortalFile.Text; paletteSelectedIndex = tiles[i].TileId; SetSelectedTile(); boxCol = boxSelected.Col; boxRow = boxSelected.Row; DrawSelectedTile(); } } void picPalette_MouseClick(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { paletteSelectedIndex = e.X / 33 + e.Y / 33 * paletteColumes; SetSelectedTile(); } } void miNew_Click(object sender, EventArgs e) { for (int i = 0; i < mapSize * mapSize; i++) { tiles[i].TileId = 0; tiles[i].Data1 = string.Empty; tiles[i].Data2 = string.Empty; tiles[i].Data3 = string.Empty; tiles[i].Data4 = string.Empty; tiles[i].Collision = false; tiles[i].Portal = false; tiles[i].PortalFile = string.Empty; tiles[i].PortalX = 0; tiles[i].PortalY = 0; } RedrawMap(); } void miSave_Click(object sender, EventArgs e) { StringBuilder sb = new StringBuilder(); sb.AppendLine("<?xml version=\"1.0\" standalone=\"yes\"?>"); sb.AppendLine("<tiles>"); for (int i = 0; i < mapSize * mapSize; i++) { sb.AppendLine(" <tile>"); sb.AppendLine(" <tileId>" + tiles[i].TileId.ToString() + "</tileId>"); sb.AppendLine(" <data1>" + tiles[i].Data1 + "</data1>"); sb.AppendLine(" <data2>" + tiles[i].Data2 + "</data2>"); sb.AppendLine(" <data3>" + tiles[i].Data3 + "</data3>"); sb.AppendLine(" <data4>" + tiles[i].Data4 + "</data4>"); sb.AppendLine(" <collision>" + (tiles[i].Collision ? "true" : "false") + "</collision>"); sb.AppendLine(" <portal>" + (tiles[i].Portal ? "true" : "false") + "</portal>"); sb.AppendLine(" <portalX>" + tiles[i].PortalX.ToString() + "</portalX>"); sb.AppendLine(" <portalY>" + tiles[i].PortalY.ToString() + "</portalY>"); sb.AppendLine(" <portalFile>" + tiles[i].PortalFile + "</portalFile>"); sb.AppendLine(" </tile>"); } sb.AppendLine("</tiles>"); if (dlgSave.ShowDialog() == DialogResult.OK) { File.WriteAllText(dlgSave.FileName + ".xml", sb.ToString()); bmpMap.Save(dlgSave.FileName + ".bmp", ImageFormat.Bmp); } } void miOpen_Click(object sender, EventArgs e) { string file = string.Empty; if (dlgOpen.ShowDialog() == DialogResult.OK) { file = dlgOpen.FileName; } try { XmlDocument doc = new XmlDocument(); doc.Load(file); XmlNodeList nodes = doc.GetElementsByTagName("tile"); int i = 0; foreach (XmlElement node in nodes) { tiles[i].TileId = Convert.ToInt32(node.GetElementsByTagName("tileId")[0].InnerText); tiles[i].Data1 = node.GetElementsByTagName("data1")[0].InnerText; tiles[i].Data2 = node.GetElementsByTagName("data2")[0].InnerText; tiles[i].Data3 = node.GetElementsByTagName("data3")[0].InnerText; tiles[i].Data4 = node.GetElementsByTagName("data4")[0].InnerText; tiles[i].Collision = Convert.ToBoolean(node.GetElementsByTagName("collision")[0].InnerText); tiles[i].Portal = Convert.ToBoolean(node.GetElementsByTagName("portal")[0].InnerText); tiles[i].PortalFile = node.GetElementsByTagName("portalFile")[0].InnerText; tiles[i].PortalX = Convert.ToInt32(node.GetElementsByTagName("portalX")[0].InnerText); tiles[i].PortalY = Convert.ToInt32(node.GetElementsByTagName("portalY")[0].InnerText); i++; } RedrawMap(); } catch (Exception ex) { MessageBox.Show(ex.Message); } } void picMap_MouseMove(object sender, MouseEventArgs e) { boxCol = e.X / 32; boxRow = e.Y / 32; mouseX = e.X; mouseY = e.Y; lblCursor.Text = "Map Cursor: x,y = " + e.X.ToString() + "," + e.Y.ToString(); if (radioDrawMode.Checked) { Tile tile = tiles[boxRow * mapSize + boxCol]; txtTileId.Text = tile.TileId.ToString(); txtData1.Text = tile.Data1; txtData2.Text = tile.Data2; txtData3.Text = tile.Data3; txtData4.Text = tile.Data4; chkCollision.Checked = tile.Collision; chkPortal.Checked = tile.Portal; txtPortalX.Text = tile.PortalX.ToString(); txtPortalY.Text = tile.PortalY.ToString(); txtPortalFile.Text = tile.PortalFile; picMap_MouseClick(sender, e); } } /// <summary> /// 如是 Draw Mode,鼠标左键绘制 Tile 到 map 上,右键清除; /// 否则,绘制 box 到 map 上。 /// </summary> void picMap_MouseClick(object sender, MouseEventArgs e) { switch (e.Button) { case MouseButtons.Left: if (radioDrawMode.Checked) { DrawSelectedTile(); } else { //show selected tile for editing boxSelected.Col = boxCol; boxSelected.Row = boxRow; boxSelected.Index = boxRow * mapSize + boxCol; Tile tile = tiles[boxSelected.Index]; txtTileId.Text = tile.TileId.ToString(); txtData1.Text = tile.Data1; txtData2.Text = tile.Data2; txtData3.Text = tile.Data3; txtData4.Text = tile.Data4; chkPortal.Checked = tile.Portal; chkCollision.Checked = tile.Collision; txtPortalX.Text = tile.PortalX.ToString(); txtPortalY.Text = tile.PortalY.ToString(); txtPortalFile.Text = tile.PortalFile; DrawSelectedBox(); } break; case MouseButtons.Right: if (radioDrawMode.Checked) { DrawTile(boxCol, boxRow, 0); // erase } break; } } void DrawSelectedTile() { DrawTile(boxCol, boxRow, paletteSelectedIndex); } void DrawSelectedBox() { // Erase selected tile with old. //DrawTile(boxSelected.Col, boxSelected.Row, tiles[boxSelected.OldIndex].TileId); // Remember current tile //boxSelected.OldIndex = boxSelected.Index; int x = boxCol * 32; int y = boxRow * 32; Pen pen = new Pen(Color.DarkMagenta, 2); Rectangle rect = new Rectangle(x + 1, y + 1, 30, 30); graphMap.DrawRectangle(pen, rect); picMap.Image = bmpMap; } //[DllImport("user32.dll")] //public static extern IntPtr WindowFromPoint(Point point); //[DllImport("user32.dll")] //public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); // Handle MouseWheel Message. public bool PreFilterMessage(ref Message m) { if (m.Msg == WinMessages.WM_MOUSEWHEEL) //WM_MOUSEWHEEL = 0x020A { Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16); IntPtr hWnd = User32.WindowFromPoint(pos); if (hWnd != IntPtr.Zero && hWnd != m.HWnd && Control.FromHandle(hWnd) != null) { User32.SendMessage(hWnd, m.Msg, m.WParam, m.LParam); return true; } } return false; } void SetSelectedTile() { int x = (paletteSelectedIndex % paletteColumes) * 33; int y = (paletteSelectedIndex / paletteColumes) * 33; var src = new Rectangle(x, y, 32, 32); var dest = new Rectangle(0, 0, 32, 32); graphTile.DrawImage(picPalette.Image, dest, src, GraphicsUnit.Pixel); picSelected.Image = bmpTile; } /// <summary> /// 在 picMap 中按指定的行、列绘制所选的 tile. /// </summary> void DrawTile(int col, int row, int tileId) { tiles[row * mapSize + col].TileId = tileId; int srcX = (tileId % paletteColumes) * 33; int srcY = (tileId / paletteColumes) * 33; int destX = col * 32; int destY = row * 32; var src = new Rectangle(srcX, srcY, 32, 32); var dest = new Rectangle(destX, destY, 32, 32); graphMap.DrawImage(picPalette.Image, dest, src, GraphicsUnit.Pixel); picMap.Image = bmpMap; } void RedrawMap() { for (int i = 0; i < mapSize * mapSize; i++) { int col = i % mapSize; int row = i / mapSize; int tileId = tiles[i].TileId; DrawTile(col, row, tileId); } } } struct Tile { public int TileId; // equal palette index public string Data1, Data2, Data3, Data4; public bool Portal, Collision; public int PortalX, PortalY; public string PortalFile; } struct BoxInfo { public int Index, Col, Row; // all in the map }
3.Picture 放在 Panel 中,使其有滚动效果。为方便操作,添加了鼠标滚轮的消息处理。即代码中的 IMessageFilter 接口实现。其中,用到两个Win32 API,见注释。实际使用,需将注释及 User32 前缀去掉,WM_* 也需用实际值替换。
4.Tiles 选择面板用到的资源如下:
同样可通过右键保存到桌面,以方便观看使用。