(续上篇)
HrdGame 类中的图像输出部分的函数,包括两部分,布局定义和绘制。
布局定义
广义上讲,布局只是棋子众多排列组合中一个快照,或者说是一个状态,因此引入了一个GameState 类,用来刻画一个布局。
逻辑布局定义
逻辑布局是UI的内部数据结构,在其定义中主要通过使用HrdPoint 刻画 棋子的位置和大小,数据记录在一个数组中。该类主要属性定义如下:
public class GameState
{public Piece[,] layoutOfObj = new Piece[6, 7];// for the efficiency of searching the blocks beside the blank area public int[,] layoutOfIdx = new int[6, 7];// use index to represent the piece that can save space for hash and stack. public Piece[] pieces = new Piece[12];public List<OpenPiece> openPieces = new List<OpenPiece>();public Piece selPcs { get; set; }......
}
其中 layoutOfObj , 是一个Piece的二维数组,数组下标就是棋子的位置(左上角),数组元素的值刻画了棋子的属性。理论上,只要记住了元素的位置和类型,它的位置和形状(区域)就可以确定了。为了计算的高效,除了描述位置和类型之外,在棋子的其余位置也进行了描述,记录了棋子的基本属性,包括类型,大小以及位置信息。
为了查询方便,同时引入了一个索引数组,相当于每个棋子的基本属性的实例刻画。这样重复记录的意义在于,查找棋子的位置关系时,比较高效。它和算法本身没有直接联系。这个数组定义如下:
public int[,] layoutOfIdx = new int[6, 7];
棋子定义数组
public Piece[] pieces = new Piece[12];
索引布局数据中的索引即来自上面的棋子定义的索引,为了便于理解,我们不妨把这个索引叫做棋子的ID或者主键,这样一个索引值就表示对应的一个具体的棋子(实例)。
public List<OpenPiece> openPieces = new List<OpenPiece>();
上面的List 用来记录当前布局下可以移动的棋子,其中 OpenPiece 定义如下:
public class OpenPiece
{public Piece piece { get; set; }public String Dir;public Piece MoveToPcs;// record the blank pieces to move, usually one element, the max is 2 }
其中Dir 用来表示棋子的可以移动的方向,MoveToPcs 用来描述移到到具体哪个空白区域(这个区域也是用Piece来刻画)。
逻辑布局函数
函数用于对上面的属性进行初始化,代码如下
public void FillLayoutArr(Piece pcs){var pos = pcs.GetHrdPos();var size = pcs.GetHrdSize();var type = pcs.GetHrdType();gameState.layoutOfObj[pos.X, pos.Y] = pcs;gameState.layoutOfObj[pos.X + size.X - 1, pos.Y + size.Y - 1] = pcs;gameState.layoutOfIdx[pos.X, pos.Y] = pcs.idx;gameState.layoutOfIdx[pos.X + size.X - 1, pos.Y + size.Y - 1] = pcs.idx;if (type == 4){gameState.layoutOfObj[pos.X + size.X - 1, pos.Y] = pcs;gameState.layoutOfObj[pos.X, pos.Y + size.Y - 1] = pcs;gameState.layoutOfIdx[pos.X + size.X - 1, pos.Y] = pcs.idx;gameState.layoutOfIdx[pos.X, pos.Y + size.Y - 1] = pcs.idx;}}
上面代码中,对除了左上角的数组元素之外也进行了处理,这样该棋子覆盖区域内的每个数组元素都可踹表征这个棋子的信息。
物理布局函数
物理布局函数将根据逻辑布局以及外部函数传入的控件尺寸,将逻辑布局转化为像素为单位的客户区位置和大小(像素单位),然后调用GDI函数将棋盘和旗子画在输入的控件上。
控件客户区位置和大小映射
函数如下:
public void SetLocAndSize(Piece pcs){int tW = _ctrl.Width / 4;int tH = _ctrl.Height / 5;int tX = pcs.GetHrdPos().X - 1;int tY = pcs.GetHrdPos().Y - 1;int pcsW = pcs.GetHrdSize().X;int pcsH = pcs.GetHrdSize().Y;var x = tX * tW;var y = tY * tH;var w = pcsW * tW;var h = pcsH * tH;//MessageBox.Show("debug");pcs.PcsLoc = new Point(x, y);pcs.PcsSize = new Size(w, h);}
函数中先获取控件的大小,然后根据获取逻辑布局大小,然后进行适当的比例缩放,设置棋子的实际大小和位置。
棋子绘制
调用上面的函数之后,棋子的实际位置和大小已经确定,在控件将其进行绘制。实际绘制时除了大小位置之外,还有棋子的颜色,名字等属性。函数主要代码如下
public void DrawRegularBox(Graphics g, Piece pcs, bool bDrawTp = false){var tRec = new Rectangle((int)pcs.PcsLoc.X, (int) pcs.PcsLoc.Y, pcs.PcsSize.Width,pcs.PcsSize.Height);Brush brush = new SolidBrush(PieceClr[pcs.GetHrdType()]);if (pcs.Highlighted)//获取高亮颜色,用于鼠标滑过时显示{// Highlight the block when it's marked as highlightedColor originalColor = pcs.HrdColor; // Replace with your original light colorint darkenFactor = 32; // Adjust this factor to control the darknessint red = Math.Max(0, originalColor.R - darkenFactor);int green = Math.Max(0, originalColor.G - darkenFactor);int blue = Math.Max(0, originalColor.B - darkenFactor);Color darkerColor = Color.FromArgb(originalColor.A, red, green, blue);brush = new SolidBrush(darkerColor);//e.Graphics.DrawRectangle(new Pen(Color.Yellow), blockRect);}if (System.IO.File.Exists(pcs.avatarFile))//如果存在图标文件,则绘制棋子的图标{try{var img = Image.FromFile(pcs.avatarFile);g.DrawImage(img, tRec);img.Dispose();}catch (Exception ex){MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);}//g.DrawRectangle(new Pen( PieceClr[pcs.GetHrdType()]), tRec); ;g.DrawRectangle(new Pen(Color.White), tRec);brush = new SolidBrush(Color.White);}else //如果图标文件不在,则绘制简单的矩形框{g.FillRectangle(brush, tRec); ;g.DrawRectangle(new Pen(Color.White), tRec);}// 创建Font对象,你可以改变字体样式、大小等var emSize = 24;Font font = new Font("Arial", emSize);// 创建Brush对象,你可以改变颜色等 brush = new SolidBrush(Color.Black);if (pcs.Highlighted){brush = new SolidBrush(Color.Yellow);}if (pcs == gameState.selPcs){brush = new SolidBrush(_selFgColor);var fontW = GetStringSize(g, pcs.Name, font.FontFamily, emSize).Width;var fontH = GetStringSize(g, pcs.Name, font.FontFamily, emSize).Height;var x = pcs.PcsLoc.X + (int)(pcs.PcsSize.Width - fontW) / 2;var y = pcs.PcsLoc.Y + (int)(pcs.PcsSize.Height - fontH) / 2 - 3;var w = (int)fontW;var h = (int)fontH;Rectangle blockRect = new Rectangle((int)x, (int)y, w, h);//_layout.DrawRegularBox(e.Graphics, block, true);g.DrawRectangle(new Pen(Color.Yellow), blockRect);}PointF NameLoc = new Point(0, 0);var hrdType = pcs.GetHrdType();// draw the name of the block {NameLoc.X = pcs.PcsLoc.X + (int)(pcs.PcsSize.Width - GetStringSize(g, pcs.Name, font.FontFamily, emSize).Width) / 2;NameLoc.Y = pcs.PcsLoc.Y + (int)(pcs.PcsSize.Height - GetStringSize(g, pcs.Name, font.FontFamily, emSize).Height) / 2;}brush = new SolidBrush(Color.White);g.DrawString(pcs.Name, font, brush, NameLoc);if (bDrawTp){font = new Font("Arial", 10);// 使用DrawString方法写字 g.DrawString(pcs.GetHrdType().ToString() + "->" + pcs.DirFactor.ToString() + "," + pcs.MoveDir, font, brush, pcs.PcsLoc);}}
至此,就可以再UI上输出一个当前的布局了。
下面默认的横刀立马布局的绘制情况
说明:图片来自百度 文心一言。
下一节,将讲述本软件的第一个查找算法, 即DFS结合Dijkistra的算法。(待续)
MaraSun 2024-03-05 BJFWDQ
PS:《周处除三害》非常好看啊,记录一下。