(续上篇)
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 highlighted
Color originalColor = pcs.HrdColor; // Replace with your original light color
int darkenFactor = 32; // Adjust this factor to control the darkness
int 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结合Dijkstra的算法。(待续)
MaraSun 2024-03-05 BJFWDQ
PS:《周处除三害》非常好看啊,记录一下。