目录
写在前面(随便写写)
游戏总体规划
代码部分
Model
View
Components
总结
写在前面(随便写写)
学完unity后,要仿照课程的例子编写一个简单的小游戏,例子是井字棋,总不能照着写个五子棋吧。说起简单的游戏,让我想起之前只有一个没网的WinXP,只能在上面玩扫雷来打发时间,无聊的时候做什么都有趣,想起来倒挺怀念的。于是为了纪念那段时间,就决定做扫雷了!
游戏总体规划
作者水平有限,只能照着老师的例子写最简单的代码,所以也不准备弄太复杂,只要实现核心功能就行。首先得有一大片格子,这里就暂定5*5的网格了,在里面随机埋5个雷。鼠标左键点击格子就是挖雷,没有雷就显示周围的雷的个数(后面会说改良方法),有雷的话就游戏结束。鼠标右键点击标记,正确地标记所有地雷就算游戏成功。这是核心的挖雷,再加上一个记数,记录按照标记来说剩余的雷数(地雷总数-标记个数,可为负数)。再加一个重新开始就是我们的游戏了,下面是游戏界面。
参考资料:即时模式 GUI (IMGUI) - Unity 手册,MVC 模式 | 菜鸟教程 (runoob.com)
代码部分
游戏遵循MVC模式,即 Model-View-Controller(模型-视图-控制器)模式,下面将主要按这三个部分来讲解代码。
Model
模型部分包含游戏对象数据,本游戏中只有五个。其中left是上面提到的可以为负数的剩余数量,remain才是实际的剩余数量。Board是二维数组,用来表示游戏中的网格。
//Model
private int left; //剩余数量
private int remain; //实际剩余数量
private bool win;
private bool gameover;
private int[,] Board = new int[5, 5];
View
视图(View)代表模型包含的数据的可视化。在这里主要就是OnGUI函数,函数中包含了IMGUI的元素,其中的代码在每帧都会执行(有点像Update),并绘制到屏幕上。除了 OnGUI 代码附加到的对象,或者层级视图中与绘制的可视元素相关的其他类型对象之外,没有其他持久性游戏对象。
// View to render entities / models
void OnGUI() {
GUI.Box(new Rect(200, 10, 350, 350), "");
GUI.Box(new Rect(325, 15, 100, 30), "剩余"+left+"个雷");
if (GUI.Button(new Rect(325, 320, 100, 30), "重新开始"))
Init();
//0表示初始状态,没有雷也没有标记;1表示有雷但没有标记;2表示错误的的标记;3表示正确的标记
if (!gameover) {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if(GUI.Button(new Rect(250 + j * 50, 50 + i * 50, 50, 50), "")){
//左键点击排雷
if(Input.GetMouseButtonUp(0)){
// Debug.Log("鼠标左键按下");
FindMine(i,j);
}
//右键点击标记
if(Input.GetMouseButtonUp(1)){
// Debug.Log("鼠标右键按下");
MarkMine(i,j);
}
}
else if(Board[i,j]>1)
GUI.Button(new Rect(250 + j * 50, 50 + i * 50, 50, 50), "!");
else if(Board[i,j]<0){
string mnum = ShowNum(i,j).ToString();
GUI.Button(new Rect(250 + j * 50, 50 + i * 50, 50, 50), mnum);
}
}
}
}
else {
if (win)
GUI.Box(new Rect(260, 50, 200, 200), "\n\n\n\n\nCongratulations!\n\n\nYou Won!");
else
GUI.Box(new Rect(260, 50, 200, 200), "\n\n\n\n\n\nYou Lost!");
}
}
在OnGUI中,先画出背景板和计数板,再画重新开始按钮,当点击”重新开始“后,游戏重新初始化,Init函数在下面的Components中。接下来就是当游戏没有结束是,遍历监听每一个按钮和对应的格子,左键单击排雷,右键单击标记和取消标记。右键标记后就显示感叹号,成功地挖开了就显示周围的地雷数量。当游戏结束后,根据情况显示对应的提示信息。总之OnGUI就是用来画出图像的。
Components
Components,也可以说是Controller(控制器)。它作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。是游戏的控制逻辑实现的地方。
// Components
void Init() {
// 初始化数据
left = 0;
win = false;
gameover = false;
int mine, mnum = 5;
Random rd = new Random();
// 初始化棋盘
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
Board[i, j] = 0;
}
}
// 打乱棋盘
int[] indices = new int[25];
for (int i = 0; i < 25; i++) {
indices[i] = i;
}
for (int i = 0; i < 25; i++) {
int j = rd.Next(i, 25);
int temp = indices[i];
indices[i] = indices[j];
indices[j] = temp;
}
// 随机地埋下最多mnum个雷
for (int i = 0; i < mnum; i++) {
int index = indices[i];
int row = index / 5;
int col = index % 5;
Board[row, col] = 1;
left++;
}
remain = left;
}
//挖雷
void FindMine(int i,int j) {
// 点到雷直接游戏结束
if(Board[i,j] == 1){
gameover = true;
}
else if(Board[i,j]==0){
Board[i,j] = -1; // -1表示已被挖开
//递归调用
if(ShowNum(i,j)==0){
for(int k=i-1; k<i+2; k++){
for(int l=j-1; l<j+2; l++){
if(k<0 || l<0 || k>4 || l>4)
continue;
FindMine(k,l);
}
}
}
}
}
void MarkMine(int i,int j){
//标记地雷
if(Board[i,j]==0||Board[i,j]==1){
if(Board[i,j]==1)
remain--;
Board[i,j]+=2;
left--;
//正确地标记了所有地雷且没有多标记
if(remain == 0 && left == 0){
gameover=true;
win=true;
}
return;
}
//取消标记
if(Board[i,j]>1){
if(Board[i,j]==3)
remain++;
Board[i,j]-=2;
left++;
return;
}
}
//显示周围有多少地雷
int ShowNum(int i,int j){
int count=0;
for(int k=i-1; k<i+2; k++){
for(int l=j-1; l<j+2; l++){
if(k<0 || l<0 || k>4 || l>4)
continue;
if(Board[k,l]==1||Board[k,l]==3)
count++;
}
}
return count;
}
本游戏中Components主要有4个部分。
第一部分是初始化函数Init。数据初始化没什么好说的,主要是棋盘初始化的随机埋雷。这里采用的是打乱算法,先把所有的格子打乱,然后取前mnum个格子作为地雷,这样可以保证地雷分布更加均匀,而不是总集中在棋盘的某一部分。
第二个是挖雷的函数FineMine。只要判断点的格子的值是0还是1即可,是1直接游戏结束,是0的话就把值改为-1,让OnGUI后面更新对应格子的显示。我们还期望游戏有类似”泛洪“的效果,即点开的格子周边没有雷的话就一直向外拓展直到碰到周边有雷的格子。实现这一效果只要加上递归调用即可,递归的终止条件就是某个值不为0。
第三个是标记函数MarkMine。规定只有值为0和1的格子才可以标记,标记后根据前面提到的预设把值加2即可。游戏胜利的条件也可以写在这里,当left和remain同时为0时就是游戏胜利,加上left的判断是为了防止直接标记所有格子来获得胜利的情况。取消标记只能针对已经标记的格子,即值大于1的格子。
第四个是ShowNum函数,用来显示某个格子周围有多少雷。只要遍历周边的格子,跳过非法和越界的格子即可。
总结
至此我们就实现了一个最基础的扫雷小游戏,正确地标记完所有的雷就可以获得游戏胜利了。作为一个最基础的版本,游戏还有很多可以优化的地方,比如1.可以让玩家自行选择难度或自定义网格数量和地雷数量;2.增加更多的鼠标点击事件,增加不确定标记"?";3.给游戏增加BGM和音效;4.现在游戏图像只用BOX实现,可以引入图片资源让游戏画面更加好看……
整体代码如下:
using System;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Input;
using Random=System.Random;
public class newgame : MonoBehaviour
{
//Model
private int left; //剩余数量
private int remain; //实际剩余数量
private bool win;
private bool gameover;
private int[,] Board = new int[5, 5];
// System Handlers
// Start is called before the first frame update
void Start()
{
Init();
}
// View to render entities / models
void OnGUI() {
GUI.Box(new Rect(200, 10, 350, 350), "");
GUI.Box(new Rect(325, 15, 100, 30), "剩余"+left+"个雷");
if (GUI.Button(new Rect(325, 320, 100, 30), "重新开始"))
Init();
// 0表示初始状态,没有雷也没有标记;1表示有雷但没有标记;2表示错误的的标记;3表示正确的标记
if (!gameover) {
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if(GUI.Button(new Rect(250 + j * 50, 50 + i * 50, 50, 50), "")){
//左键点击排雷
if(Input.GetMouseButtonUp(0)){
Debug.Log("鼠标左键按下");
FindMine(i,j);
}
//右键点击标记
if(Input.GetMouseButtonUp(1)){
Debug.Log("鼠标右键按下");
MarkMine(i,j);
}
}
else if(Board[i,j]>1)
GUI.Button(new Rect(250 + j * 50, 50 + i * 50, 50, 50), "!");
else if(Board[i,j]<0){
string mnum = ShowNum(i,j).ToString();
GUI.Button(new Rect(250 + j * 50, 50 + i * 50, 50, 50), mnum);
}
}
}
}
else {
if (win)
GUI.Box(new Rect(260, 50, 200, 200), "\n\n\n\n\nCongratulations!\n\n\nYou Won!");
else
GUI.Box(new Rect(260, 50, 200, 200), "\n\n\n\n\n\nYou Lost!");
}
}
// Components
void Init() {
// 初始化数据
left = 0;
win = false;
gameover = false;
int mine, mnum = 5;
Random rd = new Random();
// 初始化棋盘
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
Board[i, j] = 0;
}
}
// 打乱棋盘
int[] indices = new int[25];
for (int i = 0; i < 25; i++) {
indices[i] = i;
}
for (int i = 0; i < 25; i++) {
int j = rd.Next(i, 25);
int temp = indices[i];
indices[i] = indices[j];
indices[j] = temp;
}
// 随机地埋下最多mnum个雷
for (int i = 0; i < mnum; i++) {
int index = indices[i];
int row = index / 5;
int col = index % 5;
Board[row, col] = 1;
left++;
}
remain = left;
}
//挖雷
void FindMine(int i,int j) {
// 点到雷直接游戏结束
if(Board[i,j] == 1){
gameover = true;
}
else if(Board[i,j]==0){
Board[i,j] = -1; // -1表示已被挖开
//递归调用
if(ShowNum(i,j)==0){
for(int k=i-1; k<i+2; k++){
for(int l=j-1; l<j+2; l++){
if(k<0 || l<0 || k>4 || l>4)
continue;
FindMine(k,l);
}
}
}
}
}
void MarkMine(int i,int j){
//标记地雷
if(Board[i,j]==0||Board[i,j]==1){
if(Board[i,j]==1)
remain--;
Board[i,j]+=2;
left--;
//正确地标记了所有地雷且没有多标记
if(remain == 0 && left == 0){
gameover=true;
win=true;
}
return;
}
//取消标记
if(Board[i,j]>1){
if(Board[i,j]==3)
remain++;
Board[i,j]-=2;
left++;
return;
}
}
//显示周围有多少地雷
int ShowNum(int i,int j){
int count=0;
for(int k=i-1; k<i+2; k++){
for(int l=j-1; l<j+2; l++){
if(k<0 || l<0 || k>4 || l>4)
continue;
if(Board[k,l]==1||Board[k,l]==3)
count++;
}
}
return count;
}
}
演示视频:
用Unity制作的简单扫雷游戏演示