C#迷宫Winform小游戏,生成可连通的迷宫地图

上一篇本人已经写了一个控制台小游戏,这次使用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 stack = new 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;
            }
        }

        ///


        /// 查找路径,返回起点到终点是否是可连通的
        ///

        /// true:可以到达终点,false:无法到达终点
        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 grids = stack.Reverse();
            foreach (MazeGrid item in grids)
            {
                Console.WriteLine(item);
            }
        }
    }
}
二、新建Windows窗体应用程序MazeTest,添加对MazeDemo控制台程序的引用。将MazeTest设为启动项目。然后将默认的Form1窗体重命名为FormMaze。窗体设置宽度为1054, 高度为606。操作面板panel1的位置Location

窗体设计如下图:

C#迷宫Winform小游戏,生成可连通的迷宫地图_第1张图片

为窗体的重绘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 tcs = new TaskCompletionSource();
            Task task = tcs.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);
        }        
    }
}

 

四:程序运行如下图

C#迷宫Winform小游戏,生成可连通的迷宫地图_第2张图片


设置10行12列,点击初始化Init

C#迷宫Winform小游戏,生成可连通的迷宫地图_第3张图片

可以点击按钮上下左右,也可以按键盘的方向键上下左右。到边界或障碍墙将会有警告提示音。

 

C#迷宫Winform小游戏,生成可连通的迷宫地图_第4张图片

成功如图:

 C#迷宫Winform小游戏,生成可连通的迷宫地图_第5张图片

你可能感兴趣的:(C#)