基础思路
像原版一样把扫雷分为三个难度:初级难度9*9,中级难度16*16,高级难度16*30
做一个游戏需要图片资源,去网上下载了关于 扫雷的有关文件,放在了项目中
扫雷的核心是用一个二维数组存储游戏区。在开始游戏时随机生成雷区,然后根据鼠标点击触发事件,思路是比较简单清晰的。雷的标记可以利用button类的Tag属性区分,0代表自身非雷周围8格有雷,1代表雷,2代表自身非雷但周围有雷。
1、首先是界面的设计
初始界面:有两个按钮,每个按钮中有三个选项
“难度”按钮中的选项应该只有一个被选中
private void ToolStripMenuItem1_CheckedChanged(object sender, EventArgs e) { foreach (ToolStripMenuItem item in 难度ToolStripMenuItem1.DropDownItems) { item.Checked = false; } ToolStripMenuItem checkItem = sender as ToolStripMenuItem; checkItem.Checked = true; }
开始游戏按钮是游戏的入口,需进行窗体大小的调整、提示信息刷新、雷区大小调整、雷区初始化、生成地雷。
//游戏入口 private void 开始游戏ToolStripMenuItem_Click(object sender, EventArgs e) { //先清空显示及btn数组 if (btns != null) { foreach(ButtonWithPos btn in btns) Controls.Remove(btn); Array.Clear(btns, 0, WIDTH * HEIGHT); } //难度选择 if (初级ToolStripMenuItem1.Checked) { level = 1; mineNum = 10; WIDTH = HEIGHT = 9; } else if (中级ToolStripMenuItem1.Checked) { level = 2; mineNum = 40; WIDTH = HEIGHT = 16; } else if (高级ToolStripMenuItem1.Checked) { level = 3; mineNum = 99; WIDTH = 30; HEIGHT = 16; } else Application.Exit(); //对象初始化 mines = new Coord[mineNum]; for (int i = 0; i < mineNum; i++) mines[i] = new Coord(); btns = new ButtonWithPos[HEIGHT, WIDTH]; visited = new bool[HEIGHT, WIDTH]; BoardInit(); //Reveal()函数DFS递归算法所需visited数组初始化 for (int i = 0; i < HEIGHT; ++i) { for (int j = 0; j < WIDTH; ++j) { visited[i, j] = false; } } Generate(); }
BoardInit函数用于窗口大小调整、提示信息刷新以及button数组(即雷区)的初始化:
public void BoardInit() { int x0 = 20, y0 = 120; if (level == 1) { Height = 500; Width = 375; } else if (level == 2) { Height = 730; Width = 620; } else if (level == 3) { Height = 730; Width = 1110; } else Application.Exit(); lblMineSum.Left = x0; lblMineChecked.Left = x0; btnShow.Left = Width / 2 - 20; int d = 35; Console.WriteLine(WIDTH + " " + HEIGHT); for (int i = 0; i < HEIGHT; ++i) { for (int j = 0; j < WIDTH; ++j) { ButtonWithPos button = new ButtonWithPos(); button.Width = d; button.Height = d; button.Top = y0 + i * d; button.Left = x0 + j * d; button.BackgroundImage = imgNormal; button.BackgroundImageLayout = ImageLayout.Stretch; button.X = j; button.Y = i; button.Tag = 0;//Tag = 0,周围无雷;1,地雷;2,周围有雷; button.Visible = true; button.Enabled = true; button.MouseDown += Button_MouseDown; btns[i, j] = button; Controls.Add(button); } } btnShow.BackgroundImage = imgSmile; btnShow.BackgroundImageLayout = ImageLayout.Stretch; lblMineSum.Text = "地雷总数:" + mineNum.ToString(); lblMineChecked.Text = "已标记地雷数:" + mineMarked.ToString(); }
Generate函数用随机数来生成雷区
void Generate() { Random random = new Random(); bool flag; for (int i = 0; i < mineNum; ++i) { int newX = random.Next(0, HEIGHT); int newY = random.Next(0, WIDTH); flag = true; while (true) { flag = true; for (int j = 0; j < i; ++j) { if (mines[j].X == newX && mines[j].Y == newY) { flag = false; break; } } if (flag) { mines[i].X = newX; mines[i].Y = newY; btns[newX, newY].Tag = 1; int left = newX - 1, right = newX + 1; int top = newY - 1, bottom = newY + 1; for (int k = left; k <= right; ++k) { for (int l = top; l <= bottom; ++l) { if (k >= 0 && k < HEIGHT && l >= 0 && l < WIDTH) { if (k == newX && l == newY) continue; if ((int)btns[k, l].Tag != 1) btns[k, l].Tag = 2; } } } break; } else { newX = random.Next(0, HEIGHT); newY = random.Next(0, WIDTH); } } } }
2、鼠标点击处理
左键是翻开,右键是标记地雷。右键只需要进行显示的更新、标记地雷数的刷新以及检查是否胜利。左键要分翻开的类型。如果踩雷直接输掉,打开周围有雷的格子显示对应周围的雷数
private void Button_MouseDown(object sender, MouseEventArgs e) { ButtonWithPos button = sender as ButtonWithPos; if (e.Button == MouseButtons.Right) { if (button.BackgroundImage != imgMarked) { button.BackgroundImage = imgMarked; button.BackgroundImageLayout = ImageLayout.Stretch; ++mineMarked; } else { button.BackgroundImage = imgNormal; --mineMarked; } lblMineChecked.Text = "已标记地雷数:" + mineMarked.ToString(); bool win = CheckWin(); if (win) Win(); } else if (e.Button == MouseButtons.Left) { int row = button.Y, col = button.X; Console.WriteLine("BUTTON DOWN: " + row + col); if ((int)button.Tag == 1) { Lose(button); } else if ((int)button.Tag == 2) { int mineAroundSum = CalcMineAround(row, col); MineAroundShow(button, mineAroundSum); } else if ((int)button.Tag == 0) { button.BackgroundImage = imgNull; Reveal(row, col); } } }
打开周围无雷的格子要复杂一些。我们实际玩扫雷的时候有时候会一下翻开一片区域,这是因为当翻开了一个周围无雷的格子,程序会继续尝试翻周围的格子,直到翻开一个周围有雷的格子。这可以用DFS算法递归实现
void Reveal(int row, int col) { visited[row, col] = true; int mineAroundSum; int left = row - 1, right = row + 1; int top = col - 1, bottom = col + 1; if ((int)btns[row, col].Tag == 0) btns[row, col].BackgroundImage = imgNull; else if ((int)btns[row, col].Tag == 1) return; else if ((int)btns[row, col].Tag == 2) { mineAroundSum = CalcMineAround(row, col); MineAroundShow(btns[row, col], mineAroundSum); return; } for (int i = left; i <= right; ++i) { for (int j = top; j <= bottom; ++j) { if (i >= 0 && i < HEIGHT && j >= 0 && j < WIDTH) { if (!visited[i, j]) { //Console.WriteLine("REVEAL: " + i + j); Reveal(i, j); } } } } }
一些判断胜利失败以及胜利失败的表示的函数
void Lose(ButtonWithPos button) { btnShow.BackgroundImage = imgLose; btnShow.BackgroundImageLayout = ImageLayout.Stretch; foreach (ButtonWithPos btn in btns) { if ((int)btn.Tag == 1) btn.BackgroundImage = imgMineNotChosen; } button.BackgroundImage = imgMineChosen; MessageBox.Show("正中地雷!", "Oops"); 开始游戏ToolStripMenuItem.Enabled = true; } //检查是否胜利 bool CheckWin() { bool flag = true; foreach (ButtonWithPos btn in btns) { if ((int)btn.Tag == 1) { if (btn.BackgroundImage != imgMarked) { flag = false; break; } } } if (mineMarked != mineNum) flag = false; return flag; } //胜利表示 void Win() { btnShow.BackgroundImage = imgVictory; btnShow.BackgroundImageLayout = ImageLayout.Stretch; MessageBox.Show("排雷成功!干得漂亮", "You Win!"); } }
游戏截图
总结
总体来讲用C#编写一个小游戏的过程不算困难,最主要的设计的思路,这个扫雷小游戏就利用了一个随机数来生成地雷和翻格子的时候用DFS递归算法来实现,具体界面的设计在平时作业“编写一个游戏客户端”的时候已经学习到了,利用了C#窗体应用开发。
这次实验主要是了解了.NET框架
.NET的特点:
多平台:可用于开发 服务器程序,桌面程序,PDA,移动电话程序。
行业标准:支持多种通信协议,XML,HTTP,SOAP,WSDL
C#与C++和C都不同,它更面向对象
C#窗体应用开发运用在这次实验上
理解事件驱动程序设计窗体属性、方法和事件基本控件的属性、方法和事件消息对话框的使用这次实验学到了很多