上一篇本人已经写了一个控制台小游戏,这次使用Winform来生成可连通的地图,并测试运行游戏
迷宫小游戏控制台
一、先更改控制台游戏的一点点代码,用于测试迷宫是否连通的【即:从起点可以到达终点】。只用更改 MazeUtil.cs的查找路径方法FindPath()。用于返回是否连通。
整体更改后的代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MazeDemo
{
///
/// 迷宫可以认为是一个N*M的行列式,也可以认为是一个二维数组,每个元素都是一个单元格MazeGrid
///
public class MazeUtil
{
///
/// 使用一个障碍墙的二维数组来初始化迷宫地图
///
/// 数组元素的值为1时,代表障碍墙,否则是可通过的
public MazeUtil(int[,] wallArray)
{
if (wallArray == null || wallArray.Length == 0)
{
throw new Exception("初始化数组不能为空");
}
Width = wallArray.GetLength(0);
Height = wallArray.GetLength(1);
//初始化地图
MazeArray = new MazeGrid[Width, Height];
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
MazeArray[i, j] = new MazeGrid(i, j, wallArray[i, j] == 1);
}
}
CurrentGrid = MazeArray[0, 0];
TargetGrid = MazeArray[Width - 1, Height - 1];
}
///
/// 迷宫的宽度,也就是二维数组的行数【注意:行数对应纵坐标Y】
///
public int Width { get; set; }
///
/// 迷宫的高度,也就是二维数组的列数【注意:列数对应横坐标X】
///
public int Height { get; set; }
///
/// 迷宫行列式,由 Width*Height 个网格组成
///
public MazeGrid[,] MazeArray { get; set; }
///
/// 当前网格,起点默认为左上角,即 MazeGrid[0,0]
///
public MazeGrid CurrentGrid { get; set; }
///
/// 终点:目标网格,默认为右下角,即 MazeGrid[Width-1,Height-1]
///
public MazeGrid TargetGrid { get; set; }
///
/// 栈,用于存储移动的路径:找到一个 未访问的 并且 不是障碍,就Push()入栈
///
public Stack
///
/// 以此遍历当前网格的上、右、下、左四个方向。
/// 如果遇到障碍 或者 已访问过,就尝试其他方向。否则就把 无障碍 并且 未访问的网格作为新的网格
///
///
public void MoveNext(MazeGrid mazeGrid)
{
Direction direction = Direction.All;
//按照上、右、下、左【顺时针】顺序以此遍历,当遍历完左Left后,则全部遍历完成,此时置方向为All
switch (mazeGrid.Direction)
{
case Direction.None://当是None时,默认向上Up遍历
mazeGrid.Direction = Direction.Up;
direction = Direction.Up;
break;
case Direction.Up://当是Up时,接着向右Right遍历
mazeGrid.Direction = Direction.Right;
direction = Direction.Right;
break;
case Direction.Right://当是Right时,接着向下Down遍历
mazeGrid.Direction = Direction.Down;
direction = Direction.Down;
break;
case Direction.Down://当是Down时,接着向左Left遍历
mazeGrid.Direction = Direction.Left;
direction = Direction.Left;
break;
case Direction.Left://当是Left时,说明四个方向全部遍历完了,置为All
mazeGrid.Direction = Direction.All;
direction = Direction.All;
break;
}
//对上、右、下、左四个方向进行处理【None 和 All不做处理】
switch (direction)
{
case Direction.Up:
if (mazeGrid.RowIndex - 1 >= 0)
{
MazeGrid upGrid = MazeArray[mazeGrid.RowIndex - 1, mazeGrid.ColumnIndex];
if (!upGrid.IsWall && !upGrid.IsVisited)
{
//如果不是障碍 并且 没有访问过
CurrentGrid = upGrid;
}
else
{
//尝试其他方向
MoveNext(CurrentGrid);
}
}
break;
case Direction.Right:
if (mazeGrid.ColumnIndex + 1 < Height)
{
MazeGrid rightGrid = MazeArray[mazeGrid.RowIndex, mazeGrid.ColumnIndex + 1];
if (!rightGrid.IsWall && !rightGrid.IsVisited)
{
//如果不是障碍 并且 没有访问过
CurrentGrid = rightGrid;
}
else
{
//尝试其他方向
MoveNext(CurrentGrid);
}
}
break;
case Direction.Down:
if (mazeGrid.RowIndex + 1 < Width)
{
MazeGrid downGrid = MazeArray[mazeGrid.RowIndex + 1, mazeGrid.ColumnIndex];
if (!downGrid.IsWall && !downGrid.IsVisited)
{
//如果不是障碍 并且 没有访问过
CurrentGrid = downGrid;
}
else
{
//尝试其他方向
MoveNext(CurrentGrid);
}
}
break;
case Direction.Left:
if (mazeGrid.ColumnIndex - 1 >= 0)
{
MazeGrid leftGrid = MazeArray[mazeGrid.RowIndex, mazeGrid.ColumnIndex - 1];
if (!leftGrid.IsWall && !leftGrid.IsVisited)
{
//如果不是障碍 并且 没有访问过
CurrentGrid = leftGrid;
}
else
{
//尝试其他方向
MoveNext(CurrentGrid);
}
}
break;
}
}
///
/// 查找路径,返回起点到终点是否是可连通的
///
///
public bool FindPath()
{
//如果当前网格没有移动到目标网格。
bool existPath = true;
//这里如果遇到无法到达目标网格的障碍地图时,需要终止
while (CurrentGrid != TargetGrid)
{
if (CurrentGrid.IsVisited)
{
//如果当前网格全部访问完成,则出栈
if (CurrentGrid.Direction == Direction.All)
{
if (stack.Count > 0)
{
stack.Pop();//移除最后一次添加的
}
if (stack.Count > 0)
{
//获取倒数第二次添加的
CurrentGrid = stack.Peek();
}
else
{
Console.WriteLine("无路可走,请检查迷宫障碍设置...");
existPath = false;
break;
}
}
else
{
//没有遍历完,继续遍历
MoveNext(CurrentGrid);
}
}
else
{
//如果未访问,则设置为已访问,同时添加入栈
CurrentGrid.IsVisited = true;
stack.Push(MazeArray[CurrentGrid.RowIndex, CurrentGrid.ColumnIndex]);
}
}
//将目标网格添加到顶部
if (stack.Count > 0)
{
stack.Push(TargetGrid);
}
return existPath;
}
///
/// 打印路径
///
public void PrintPath()
{
if (stack.Count == 0)
{
Console.WriteLine("无法到达目的网格,请检查迷宫地图设置...");
return;
}
//因第一个插入的元素是入口,栈是先进后出,入口反而成为最后元素。这里进行反转
IEnumerable
foreach (MazeGrid item in grids)
{
Console.WriteLine(item);
}
}
}
}
二、新建Windows窗体应用程序MazeTest,添加对MazeDemo控制台程序的引用。将MazeTest设为启动项目。然后将默认的Form1窗体重命名为FormMaze。窗体设置宽度为1054, 高度为606。操作面板panel1的位置Location
窗体设计如下图:
为窗体的重绘Paint绑定事件方法FormMaze_Paint(),Init按钮绑定事件btnInit_Click(),上、下、左、右绑定事件btnDirection_Click。同时重写键盘事件ProcessCmdKey,对上下左右方向键进行相应处理。
三、FormMaze.cs整体代码如下(忽略设计器自动生成的代码):
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using MazeDemo;
namespace MazeTest
{
public partial class FormMaze : Form
{
///
/// 初始化设置障碍墙
///
static int[,] wallArray = new int[5, 6] { { 0, 0, 1, 0, 0, 0 }, { 0, 1, 0, 1, 0, 0 }, { 0, 1, 0, 0, 0, 1 }, { 0, 1, 0, 1, 0, 0 }, { 0, 0, 0, 1, 0, 0 } };
MazeUtil mazeUtil = new MazeUtil(wallArray);
public FormMaze()
{
InitializeComponent();
//使用双缓冲来减少图形闪烁(当绘制图片时出现闪烁时,使用双缓冲)
this.DoubleBuffered = true;//设置本窗体启用双缓冲
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true); //禁止擦除背景.
SetStyle(ControlStyles.DoubleBuffer, true); //双缓冲
}
///
/// 处理windows消息:禁掉清除背景消息
/// 主要是处理部分控件使用双缓冲也会闪烁的现象
///
///
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x0014) // 禁掉清除背景消息
return;
base.WndProc(ref m);
}
///
/// 重写键盘事件,对上下左右方向键进行相应处理
///
///
///
///
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
switch (keyData)
{
case Keys.Up:
btnDirection_Click(btnUp, null);
break;
case Keys.Right:
btnDirection_Click(btnRight, null);
break;
case Keys.Down:
btnDirection_Click(btnDown, null);
break;
case Keys.Left:
btnDirection_Click(btnLeft, null);
break;
}
return false;//如果要调用KeyDown,这里一定要返回false才行,否则只响应重写方法里的按键.
//这里调用一下父类方向,相当于调用普通的KeyDown事件.如果按空格会弹出两个对话框
//return base.ProcessCmdKey(ref msg, keyData);
//return true;//这里return true 否则控件焦点会跟着方向键改变
}
private void FormMaze_Load(object sender, EventArgs e)
{
this.AutoScroll = true;
rtxbDisplay.ReadOnly = true;
}
///
/// 上下左右方向移动事件【四个方向按钮都绑定该事件】
///
///
///
private void btnDirection_Click(object sender, EventArgs e)
{
Button button = sender as Button;
//MessageBox.Show(mazeUtil.CurrentGrid+",行数:"+ mazeUtil.Width+",列数:"+ mazeUtil.Height);
switch (button.Name)
{
case "btnUp":
if (mazeUtil.CurrentGrid.RowIndex - 1 < 0)
{
//使用系统声音报警
System.Media.SystemSounds.Beep.Play();
//Asterisk:星号,引起注意,重要的声音
//Beep:操作无效的声音
//Exclamation:感叹声 打开某个文件的声音
//Hand:手动处理的声音
//Question
}
else
{
MazeGrid nextGrid = mazeUtil.MazeArray[mazeUtil.CurrentGrid.RowIndex - 1, mazeUtil.CurrentGrid.ColumnIndex];
if (nextGrid.IsWall)
{
//障碍,报警
System.Media.SystemSounds.Asterisk.Play();
}
else
{
mazeUtil.CurrentGrid = nextGrid;
this.Invalidate();//触发paint事件
//update,repaint,invalid
}
}
break;
case "btnRight":
if (mazeUtil.CurrentGrid.ColumnIndex + 1 >= mazeUtil.Height)
{
//使用系统声音报警
System.Media.SystemSounds.Beep.Play();
}
else
{
MazeGrid nextGrid = mazeUtil.MazeArray[mazeUtil.CurrentGrid.RowIndex, mazeUtil.CurrentGrid.ColumnIndex + 1];
if (nextGrid.IsWall)
{
//障碍,报警
System.Media.SystemSounds.Asterisk.Play();
}
else
{
mazeUtil.CurrentGrid = nextGrid;
this.Invalidate();//触发paint事件
}
}
break;
case "btnDown":
if (mazeUtil.CurrentGrid.RowIndex + 1 >= mazeUtil.Width)
{
//使用系统声音报警
System.Media.SystemSounds.Beep.Play();
}
else
{
MazeGrid nextGrid = mazeUtil.MazeArray[mazeUtil.CurrentGrid.RowIndex + 1, mazeUtil.CurrentGrid.ColumnIndex];
if (nextGrid.IsWall)
{
//障碍,报警
System.Media.SystemSounds.Asterisk.Play();
}
else
{
mazeUtil.CurrentGrid = nextGrid;
this.Invalidate();//触发paint事件
}
}
break;
case "btnLeft":
if (mazeUtil.CurrentGrid.ColumnIndex - 1 < 0)
{
//使用系统声音报警
System.Media.SystemSounds.Beep.Play();
}
else
{
MazeGrid nextGrid = mazeUtil.MazeArray[mazeUtil.CurrentGrid.RowIndex, mazeUtil.CurrentGrid.ColumnIndex - 1];
if (nextGrid.IsWall)
{
//障碍,报警
System.Media.SystemSounds.Asterisk.Play();
}
else
{
mazeUtil.CurrentGrid = nextGrid;
this.Invalidate();//触发paint事件
}
}
break;
}
if (mazeUtil.CurrentGrid == mazeUtil.TargetGrid)
{
DisplayContent("已到达迷宫终点,真棒!");
mazeUtil.CurrentGrid = mazeUtil.MazeArray[0, 0];
MessageBox.Show("已到达迷宫终点,真棒!", "成功");
}
}
private void btnInit_Click(object sender, EventArgs e)
{
int rowCount;//行数
int columnCount;//列数
if (!CheckInputCount(txbRowCount, "行数", out rowCount))
{
return;
}
if (!CheckInputCount(txbColumnCount, "列数", out columnCount))
{
return;
}
wallArray = new int[rowCount, columnCount];
DisplayContent("正在生成随机地图,请稍候...");
//异步生成地图
GenerateConnectableMap(rowCount, columnCount);
//重绘迷宫,新的开始
mazeUtil.CurrentGrid = mazeUtil.MazeArray[0, 0];
mazeUtil.stack.Clear();
this.Invalidate();
}
///
/// 生成可连通的地图【从起点可以到达终点】。因行数、列数较大时。寻找出可连通地图的耗时较长。
/// 会造成界面假死,这里增加异步处理耗时任务
///
///
///
public void GenerateConnectableMap(int rowCount, int columnCount)
{
TaskCompletionSource
Task
//这里进行耗时操作
System.Threading.ThreadPool.QueueUserWorkItem(waitCallback =>
{
//这里执行耗时任务:寻找到一个可连通的地图
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
bool existPath = false;
do
{
//注意:起始点、终点一定不是墙。定义 随机0或1的随机数
for (int i = 0; i < rowCount; i++)
{
for (int j = 0; j < columnCount; j++)
{
if ((i == 0 && j == 0) || (i == rowCount - 1 && j == columnCount - 1))
{
//起点、终点一定为0,不考虑随机数。其他点随机
continue;
}
wallArray[i, j] = new Random(Guid.NewGuid().GetHashCode()).Next(0, 2);
}
}
mazeUtil = new MazeUtil(wallArray);
existPath = mazeUtil.FindPath();
//如果起点、终点不是连通的,则重新随机设计地图
} while (!existPath);
stopwatch.Stop();
DisplayContent($"生成随机地图成功,用时【{stopwatch.ElapsedMilliseconds}】ms.新的一局开始");
//一旦对 TaskCompletionSource 调用 SetResult 方法,相关联的 Task 便会结束,返回 Task 的结果值
tcs.SetResult(existPath);
});
task.Wait(10000);
DisplayContent($"耗时任务结果:【{task.Result}】.");
}
///
/// 显示文本框内容
///
///
private void DisplayContent(string message)
{
this.BeginInvoke(new Action(() =>
{
if (rtxbDisplay.TextLength > 10240)
{
rtxbDisplay.Clear();
}
rtxbDisplay.AppendText(message + "\n");
rtxbDisplay.ScrollToCaret();
}));
}
///
/// 检查输入
///
///
///
///
///
private bool CheckInputCount(TextBox txb, string commentStr, out int count)
{
if (!int.TryParse(txb.Text, out count))
{
MessageBox.Show($"[{commentStr}]请输入正整数", "错误");
txb.Focus();
return false;
}
if (count <= 0 || count >= 100)
{
MessageBox.Show($"[{commentStr}]范围是【1~99】,请重新输入", "错误");
txb.Focus();
return false;
}
return true;
}
///
/// 窗体的重绘事件,调用Invalidate()会触发重绘事件
///
///
///
private void FormMaze_Paint(object sender, PaintEventArgs e)
{
int sideLength = 50;//正方形【迷宫的一个网格MazeGrid】的边长。
float fontSize = 13;//打印的起点、终点文字的字体大小
//以边长为50为例:因当前窗体的高度为606,去除窗体顶部和底部的高度【约56】,只能正常显示11行。因Panel控件的横坐标为705,因此只能显示14列。
if (mazeUtil.Width <= 11 && mazeUtil.Height <= 14)
{
sideLength = 50;
fontSize = 13;
}
else if (mazeUtil.Width <= 22 && mazeUtil.Height <= 28)
{
//如果行数在22行之内,列数在28列之内,则将网格的边长设置25
sideLength = 25;
fontSize = 8;
}
else
{
//如果行数、列数过大(行数大于22,列数大于28)。则应该将界面变大,并增加滚动条
sideLength = 25;
fontSize = 8;
if (mazeUtil.Width > 22)
{
this.Height = this.Height + (mazeUtil.Width - 22) * sideLength;
}
if (mazeUtil.Height > 28)
{
this.Width = this.Width + (mazeUtil.Height - 28) * sideLength;
//Panel操作面板要整体向右移动,即X坐标增加
panel1.Location = new Point(panel1.Location.X + (mazeUtil.Height - 28) * sideLength, panel1.Location.Y);
}
}
Graphics graphics = e.Graphics;
for (int i = 0; i < mazeUtil.Width; i++)
{
for (int j = 0; j < mazeUtil.Height; j++)
{
//注意:第一行是Y坐标没变,X坐标在变化。因此i是纵坐标 j是横坐标
Rectangle rect = new Rectangle(sideLength * j, sideLength * i, sideLength, sideLength);
graphics.DrawRectangle(new Pen(Color.Red), rect);
if (mazeUtil.MazeArray[i, j].IsWall)
{
graphics.FillRectangle(new SolidBrush(Color.Black), rect);
}
//如果不是起点,也不是终点,并且是当前移动到节点
else if ((i != 0 || j != 0) && (i != mazeUtil.Width - 1 || j != mazeUtil.Height - 1)
&& mazeUtil.MazeArray[i, j] == mazeUtil.CurrentGrid)
{
graphics.FillRectangle(new SolidBrush(Color.Yellow), rect);
}
}
}
//起点设置为蓝色
Rectangle rectStart = new Rectangle(0, 0, sideLength, sideLength);
graphics.FillRectangle(new SolidBrush(Color.Blue), rectStart);
AddTextAlignCenter(graphics, "起点", new Font("宋体", fontSize), rectStart);
//终点设置为红色
Rectangle rectEnd = new Rectangle(sideLength * (mazeUtil.Height - 1), sideLength * (mazeUtil.Width - 1), sideLength, sideLength);
graphics.FillRectangle(new SolidBrush(Color.Red), rectEnd);
AddTextAlignCenter(graphics, "终点", new Font("宋体", fontSize), rectEnd);
}
///
/// 将显示的文字放在矩形的中间
///
///
///
///
///
private void AddTextAlignCenter(Graphics graphics, string text, Font font, Rectangle rect)
{
SizeF sizeF = graphics.MeasureString(text, font);
float destX = rect.X + (rect.Width - sizeF.Width) / 2;
float destY = rect.Y + (rect.Height - sizeF.Height) / 2;
graphics.DrawString(text, font, Brushes.Black, destX, destY);
}
}
}
四:程序运行如下图
设置10行12列,点击初始化Init
可以点击按钮上下左右,也可以按键盘的方向键上下左右。到边界或障碍墙将会有警告提示音。
成功如图: