身为数独发烧友的我在接触到了unity之后,产生的第一个念头就是用unity制作一个数独。但是,在跟着网上一大堆教程搭了半天还失败之后,我决定改用纯C#代码的形式搭建我的数独游戏。前置步骤为在unity中新建项目并打开,创建C# script并将其拖动至MainCamera中;接下来就是对C# script的正式编写。
//游玩视频放在文末,其中涉及到放置数字、撤回、重新生成题目等活动
一,生成9*9棋盘
为了在屏幕上显示9*9的数独表格,需要一个同样为9*9尺寸的二维数组,与OnGUI函数中循环读取-显示相配合。
首先,我们需要在整个public class中定义好二维数组。格式如下:
private int[,] sudokuBoard = new int[9, 9];
int count;
其次,同样在public class中,我们需要定义OnGUI函数,这个函数会在整个项目运行时的每帧执行一次,负责(数独表格的)实时显示。
//在其中提到的函数都会在后续进行补充
//num_str这个一维数组仅用于按钮上文本的显示,即数独每个小格子的内容
void OnGUI()
{
GUI.Box(new Rect(50, 25, 750, 750), "");//整个棋盘
if (GUI.Button(new Rect(60, 270, 100, 30), "Restart")) Init();//如果点了restrat按钮就会重新初始化
if (!GameOver())
{//在格子填满前,循环判断每个格子应该显示什么内容
string[] num_str = {"0","1","2","3","4","5","6","7","8","9" };
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
if (sudokuBoard[i, j] == 0 &&
GUI.Button(new Rect(170+j*50+j/3*10,50+i*50+i/3*10,50,50), ""))
{
PutChess(i, j);//点击空白区域时调用PutChess函数
}
else if(sudokuBoard[i, j] !=0)//遍历到了非空白区域,显示数字
{
//显示的内容
GUI.Button(new Rect(170+j*50+j/3*10,50+i*50+i/3*10,50,50), num_str[sudokuBoard[i,j] ]);
//如果在撤回状态下(number==0)被点击:
if ((number == 0) && (last_i==i)&&(last_j==j))
{
sudokuBoard[i, j] = 0;
if(count>0)
count--;
number = 1;
}
}
}
}
for (int i = 0; i < 10; i++)//显示界面下方待填入的数字(0-9)
{
if (GUI.Button(new Rect(155 + i * 50, 550, 50, 50), num_str[i]))
{
FixNumber(i);
}
}
}
}
为了更好地让大家理解,我先放出最终的运行图:
在我的OnGUI函数中,前一个嵌套循环对应着:每一帧都遍历数独表格上所有格子,分别进行显示所有格子和修改空格两件事;后一个循环对应着:若点击下方按钮,则调用FixNumber函数,准备修改数独表格上某些格子的内容。下方十个按钮中,0代表撤回前一步。
二,函数 Init()
顾名思义,Init()这个函数的主要功能就是初始化整个数独表格。在这里我放出两版Init()函数,分别是初始化空数独 / 初始化含题目数独的Init()。
没有任何数字的空数独表格Init():
for(int i=0;i<9;i++)
{
for(int j=0;j<9;j++)
{
sudokuBoard[i, j] = 0;
}
}
count = 0;
//理论上来说,初始化自带题目的Init()有更好的做法(每次都随机生成新题目),但我在编写时发现我的做法会导致unity运行卡死,暂时不知道怎么解决。因此,我先手动放入了一个数独的完整解,再将能够推理得到的格子挖空,最终得到一份数独题目:
void Init()//此处完成各个参数和数独题目的初始化
{
count = 0;
int[,] copytable = new int[9, 9]{
{1,2,3,4,5,6,7,8,9 },
{8,6,9,7,3,1,5,4,2 },
{7,4,5,9,2,8,6,1,3 },
{5,9,8,1,7,2,3,6,4 },
{6,3,4,8,9,5,1,2,7 },
{2,7,1,6,4,3,9,5,8 },
{9,5,7,2,6,4,8,3,1 },
{4,8,6,3,1,7,2,9,5 },
{3,1,2,5,8,9,4,7,6 }
};
//挖空,使得数独题目有唯一解,且最简。实际上就是用解数独策略反推哪些格子可以挖掉
//如果能从行+列+宫三种约束条件中推导出一格的内容,则这一格可以挖掉
//注:仍然可以有更多策略,如用4个同种数字排除一个数字等。
int row;
int column;
int my_count = 0;
for (int time = 600; time > 0; time--)//这么写是当时找卡死的原因,可以改成while循环
{
row = Random.Range(0, 9);
column = Random.Range(0, 9);//随机生成行列的数字
for (int g = 0; g < 9; g++)//恢复 geweishu,用于反推该格子能否挖掉
{
geweishu[g] = g + 1;
}
sum = 0;
for (int p = 0; p < 9; p++)
{
if ((copytable[column, p] > 0) && (p != row))//同行有数字,且不是本人
{
geweishu[copytable[column, p] - 1] = 0;//这个数字不可能被填入这个格子
}
if ((copytable[p, row] > 0) && (p != column))//同列有数字,不是本人
{
geweishu[copytable[p, row] - 1] = 0;
}
if (copytable[column / 3 * 3 + p % 3, row / 3 * 3 + p / 3] > 0)//同九宫格
{
if ((column / 3 * 3 + p%3 != column) || (row / 3 * 3 + p/3 != row))
{
geweishu[copytable[column /3*3 + p%3, row /3*3 + p/3] - 1] = 0;
}
}
}
//利用邻近格子完全筛除了所有不可能的数字,再看是否可能填别的数字)
for (int q = 0; q < 9; q++)
{
sum += geweishu[q];
}
if (copytable[column, row] == sum)//本行中只能填原来这个数字,不能填别的数字
{
//这个格子能推理出,直接擦掉
copytable[column, row] = 0;
my_count++;
}
}
for (int s = 0; s < 9; s++)
{
for (int d = 0; d < 9; d++)
{
sudokuBoard[s, d] = copytable[s, d];//将副本覆盖到棋盘上
}
}
count = 81-my_count;//count即为当前数独表格中已有的数字个数
}
三,函数PutChess
函数PutChess的功能是将数字放入数独表格。为此,我们需要有10个按键(1-9,以及撤回前一步),以及完成“点击即填入数字”的设计。
PutChess函数的设计如下:
//需要在public class的开头定义last_i和last_j
void PutChess(int i, int j)
{
sudokuBoard[i, j] = number;//标记
//记录上一次放置数字的位置,用于撤回
last_i = i;
last_j = j;
//记录本局放了多少数字,看游戏是否结束
count++;
}
10个按键的设计如下:
//要将这段代码放在OnGUI的if(!GameOver){ }中
//FixNumber函数会在后续补充定义,功能为记录“即将放入空格的数字”。
for (int i = 0; i < 10; i++)//显示界面下方待填入的数字(0-9)
{
if (GUI.Button(new Rect(155 + i * 50, 550, 50, 50), num_str[i]))
{
FixNumber(i);
}
}
四,GameOver函数
功能为判定游戏是否结束、以什么方式结束(胜/负)
//仅当数独表格填满、count为81时才有可能胜利,也有可能失败,但都返回true意味着游戏结束
//未填满时都返回false意味着未结束
bool GameOver()//判断游戏结束(不够严谨)
{
if(count>=81)//填满棋盘时才判定
{
for (int i = 0; i < 9; i++)//判断每行每列
{
int sum1 = 0;//统计每行
int sum2 = 0;//统计每列
int sum3 = 0;//统计每个九宫格
for (int j = 0; j < 9; j++)
{
sum1 += sudokuBoard[i, j];
sum2 += sudokuBoard[j, i];
//此处是统计第i个九宫格里的所有九个数字
sum3 += sudokuBoard[(i * 3) % 9 + j % 3, j / 3 + (i / 3) * 3];
}
if ((sum1 == 45) || (sum2 == 45) || (sum3==45))
{
continue;
}
//发现和不为45的,提示数独失败
GUI.Box(new Rect(520, 100, 400, 400), "\n\n\n\n\nI'm Sorry\n You has lost.");
return true;//结束了,但是输了
}
//能离开循环说明填对了
GUI.Box(new Rect(520, 100, 400, 400), "\n\n\n\n\nCongratulations!\n You has won.");
return true;//结束且胜利
}
return false;//没填满棋盘就是没结束
}
五,FixNumber函数
顾名思义,其实就是修改number这个变量,用于每次鼠标点击时点击填入数字
void FixNumber(int i)
{
number = i;
}
六,Start函数
Start函数是unity初始化游戏的唯一入口。在此函数中,我们只需将Init函数放入,实现初始化
void Start()
{
Init();
}
改进方向
至此,一个“预设好答案并随机挖空生成题目”的数独游戏就完成啦!
b站视频链接:
只是一个还没长开的unity数独游戏罢了_哔哩哔哩bilibili
Unity数独视频
当然,还可以从以下几个方面进行改进:
1.用更多方式进行挖空,提升游戏难度;
2.随机生成答案,而不仅仅是对固定的答案进行随机挖空;
3.让撤回的范围从“只能撤回前一步”变为“撤回更多步”,或者“擦除固定一格的数据”,提升玩家的游玩体验;
4.美化界面;
5.加入点击音效/点击动画等操作的反馈;
6.加入规则介绍。