【Unity】MiniGame编辑器小游戏(四)数独【Sudoku】
更新日期:2025年6月24日。
项目源码:后续章节发布
索引
- 数独【Sudoku】
- 一、游戏最终效果
- 二、玩法简介
- 三、正式开始
- 1.定义游戏窗口类
- 2.规划游戏窗口、视口区域
- 3.绘制九宫格棋盘
- ①.定义方格节点
- ②.生成数独棋盘
- ③.绘制数独棋盘
- 4.生成数独题目
- 5.在空白方格中填写数字
- 6.检测游戏是否通关
- 7.绘制游戏操作说明
- 8.暂停游戏、退出游戏
数独【Sudoku】
本篇的目标是开发一个数独【Sudoku】
小游戏。
一、游戏最终效果
Unity编辑器小游戏:数独
二、玩法简介
数独
游戏是一种经典的逻辑推理游戏,其规则简单却富有挑战性。
游戏的目标是在一个9×9的方格中填入数字1到9,使得每一行、每一列以及每一宫内(3×3的小方格),每个数字都只出现一次
。游戏开始时,部分格子中已经填入了数字作为提示,玩家需要根据这些提示,推理出其他空格中的数字。
三、正式开始
1.定义游戏窗口类
首先,定义数独的游戏窗口类MiniGame_Sudoku
,其继承至MiniGameWindow【小游戏窗口基类】
:
/// <summary>/// 数独/// </summary>public class MiniGame_Sudoku : MiniGameWindow{}
2.规划游戏窗口、视口区域
通过覆写虚属性
实现规划游戏视口区域大小:
/// <summary>/// 游戏名称/// </summary>public override string Name => "数独 [Sudoku]";/// <summary>/// 游戏窗体大小/// </summary>public override Vector2 WindowSize => new Vector2(480, 310);/// <summary>/// 游戏视口区域/// </summary>public override Rect ViewportRect => new Rect(5, 25, 280, 280);
注意:游戏窗体大小必须 > 游戏视口区域。
然后通过代码打开此游戏窗口:
[MenuItem("MiniGame/数独 [Sudoku]", priority = 5)]private static void Open_MiniGame_Sudoku(){MiniGameWindow.OpenWindow<MiniGame_Sudoku>();}
便可以看到游戏的窗口、视口区域如下(左侧深色凹陷区域为视口
区域):
3.绘制九宫格棋盘
数独游戏的棋盘
为9x9个方格组成,每一行、每一列均为9个格子,所以我们先来绘制如下这样的棋盘:
①.定义方格节点
首先,定义方格节点Node
,其代表棋盘中的一个方格:
/// <summary>/// 节点/// </summary>public class Node{public int Value = 0;public bool IsBlank = false;}
②.生成数独棋盘
生成数独棋盘:
private int[] _numbers;private Node[,] _sudoku;private Node _current;/// <summary>/// 开始游戏/// </summary>private void StartGame(){//一个缓存数组,用于随机取1-9不重复数字_numbers = new int[9];for (int i = 0; i < _numbers.Length; i++){_numbers[i] = i + 1;}for (int i = 0; i < _numbers.Length; i++){int j = Random.Range(i, _numbers.Length);int temp = _numbers[i];_numbers[i] = _numbers[j];_numbers[j] = temp;}//创建数独棋盘_sudoku = new Node[9, 9];_current = null;for (int row = 0; row < 9; row++){for (int col = 0; col < 9; col++){_sudoku[row, col] = new Node();}}//生成数独棋盘if (GenerateSudoku()){//生成数独题目GenerateSudokuQuestion();}else{_sudoku = null;Debug.LogError("Sudoku: Failed to generate Sudoku board, please try again.");}}
这里的核心即是生成数独棋盘
的算法,目前采用了较为暴力的回溯算法
:
/// <summary>/// 生成数独棋盘/// </summary>private bool GenerateSudoku(){for (int row = 0; row < 9; row++){for (int col = 0; col < 9; col++){//发现未填入数字的方格if (_sudoku[row, col].Value == 0){//随机取1-9数字填入for (int num = 0; num < _numbers.Length; num++){//先尝试填入数字,判断填入后是否合规if (IsSafe(row, col, _numbers[num])){//如果合规,则确认填入该数字_sudoku[row, col].Value = _numbers[num];//填入该数字后,棋盘产生变化,再次递归进行整个棋盘的空方格填字if (GenerateSudoku()){return true;}else{//GenerateSudoku 返回了false,需要回溯到上一步,则本方格数字清空_sudoku[row, col].Value = 0;}}}//如果填入1-9任何数字均不合规,则证明上一方格填写的数字有误,回溯到上一步return false;}}}return true;}/// <summary>/// 在指定行、列填入指定数字后,是否合规(每一行、每一列、每一宫均为1-9不重复数字)/// </summary>private bool IsSafe(int row, int col, int number){//列中存在重复数字,不合规for (int x = 0; x < 9; x++){if (_sudoku[row, x].Value == number){return false;}}//行中存在重复数字,不合规for (int x = 0; x < 9; x++){if (_sudoku[x, col].Value == number){return false;}}//九宫中存在重复数字,不合规int startRow = row - row % 3;int startCol = col - col % 3;for (int i = 0; i < 3; i++){for (int j = 0; j < 3; j++){if (_sudoku[i + startRow, j + startCol].Value == number){return false;}}}return true;}
虽然较为暴力,但此算法也是解决数独问题比较常用的算法。
③.绘制数独棋盘
然后在OnGameViewportGUI
方法中绘制数独棋盘:
protected override void OnGameViewportGUI(){base.OnGameViewportGUI();DrawPanel();}/// <summary>/// 绘制画布/// </summary>private void DrawPanel(){for (int h = 0; h < 9; h++){for (int w = 0; w < 9; w++){DrawNode(w, h);}}}/// <summary>/// 绘制节点/// </summary>private void DrawNode(int x, int y){//每一个方格根据所在位置进行细微偏移,使得九个宫之间存在细微的间隔_nodeRect.Set(x * BLOCKSIZE + x / 3 * 5, y * BLOCKSIZE + y / 3 * 5, BLOCKSIZE, BLOCKSIZE);//如果为空白需填充的节点,则绘制一个按钮,点击可选中并输入数字if (_sudoku[x, y].IsBlank){GUI.color = _current == _sudoku[x, y] ? Color.cyan : Color.white;GUI.contentColor = Color.yellow;string str = _sudoku[x, y].Value == 0 ? "" : _sudoku[x, y].Value.ToString();if (GUI.Button(_nodeRect, str, _blankGS)){if (_current == _sudoku[x, y]){_current = null;}else{_current = _sudoku[x, y];}}GUI.color = Color.white;GUI.contentColor = Color.white;}//如果为已显示数字的节点,则直接绘制数字else{GUI.Box(_nodeRect, _sudoku[x, y].Value.ToString(), _noBlankGS);}}
此时就能绘制出数独棋盘了:
4.生成数独题目
在生成数独棋盘时,我们已然生成了一个完整合规
的数独九宫格,此时只需要随机将其中部分方块的数字抹除(让玩家来填入),便生成了一道数独的题目。
由此,我们依然设计几种难度等级
的关卡(抹除的方格数量越多,意味着需要玩家填写的方格越多,则难度越高):
名称 | 随机抹除的方格数量 |
---|---|
初级 | 2-4 |
中级 | 4-6 |
高级 | 6-8 |
private readonly string[] LEVELS = new string[] { "初级", "中级", "高级" };
在生成数独题目时,根据游戏难度进行随机抹除方格:
/// <summary>/// 生成数独题目/// </summary>private void GenerateSudokuQuestion(){Vector2Int blankNumber = new Vector2Int();if (_level == 0){blankNumber.x = 2;blankNumber.y = 4;}else if (_level == 1){blankNumber.x = 4;blankNumber.y = 6;}else if (_level == 2){blankNumber.x = 6;blankNumber.y = 8;}//以每个小的九宫为单位进行抹除List<Node> nodes = new List<Node>();for (int row = 0; row < 3; row++){for (int col = 0; col < 3; col++){//提取该九宫中的所有方格nodes.Clear();nodes.Add(_sudoku[row * 3, col * 3]);nodes.Add(_sudoku[row * 3, col * 3 + 1]);nodes.Add(_sudoku[row * 3, col * 3 + 2]);nodes.Add(_sudoku[row * 3 + 1, col * 3]);nodes.Add(_sudoku[row * 3 + 1, col * 3 + 1]);nodes.Add(_sudoku[row * 3 + 1, col * 3 + 2]);nodes.Add(_sudoku[row * 3 + 2, col * 3]);nodes.Add(_sudoku[row * 3 + 2, col * 3 + 1]);nodes.Add(_sudoku[row * 3 + 2, col * 3 + 2]);//生成随机抹除数int number = Random.Range(blankNumber.x, blankNumber.y);for (int n = 0; n < number; n++){//随机取出一个方格进行抹除Node node = RandomDequeueNode(nodes);node.Value = 0;node.IsBlank = true;}}}}/// <summary>/// 随机取出一个节点/// </summary>private Node RandomDequeueNode(List<Node> nodes){int index = Random.Range(0, nodes.Count);Node node = nodes[index];nodes.RemoveAt(index);return node;}
生成数独题目完成后(部分方块的数字被抹除),现在的游戏棋盘界面就是如下这样的:
注意:这里有一个选择关卡难度的过程省略了,该过程很简单便不浪费篇幅赘述了,后续在源码中即可一目了然。
5.在空白方格中填写数字
在OnGamePlayingEvent
方法中完成填写数字的逻辑:
protected override void OnGamePlayingEvent(Event e, Vector2 mousePosition){base.OnGamePlayingEvent(e, mousePosition);//空白的方格会绘制为按钮,点击按钮即可选中该方格if (_current == null)return;//选中方格后,可输入指定的数字,Delete或退格键清空输入if (e.type == EventType.KeyDown){switch (e.keyCode){case KeyCode.Alpha1:case KeyCode.Keypad1:_current.Value = 1;Repaint();break;case KeyCode.Alpha2:case KeyCode.Keypad2:_current.Value = 2;Repaint();break;case KeyCode.Alpha3:case KeyCode.Keypad3:_current.Value = 3;Repaint();break;case KeyCode.Alpha4:case KeyCode.Keypad4:_current.Value = 4;Repaint();break;case KeyCode.Alpha5:case KeyCode.Keypad5:_current.Value = 5;Repaint();break;case KeyCode.Alpha6:case KeyCode.Keypad6:_current.Value = 6;Repaint();break;case KeyCode.Alpha7:case KeyCode.Keypad7:_current.Value = 7;Repaint();break;case KeyCode.Alpha8:case KeyCode.Keypad8:_current.Value = 8;Repaint();break;case KeyCode.Alpha9:case KeyCode.Keypad9:_current.Value = 9;Repaint();break;case KeyCode.Backspace:case KeyCode.Delete:_current.Value = 0;Repaint();break;}}}
6.检测游戏是否通关
也即是检测数独题目是否已解:要求每一行
、每一列
、每一宫
中的九个方格中填入1-9不重复
数字。
/// <summary>/// 检测数独题目是否完成/// </summary>private bool CheckSudokuQuestion(){//检测是否有空白for (int row = 0; row < 9; row++){for (int col = 0; col < 9; col++){if (_sudoku[row, col].Value == 0){return false;}}}HashSet<int> nodes = new HashSet<int>();//检测每一列是否合规for (int row = 0; row < 9; row++){nodes.Clear();for (int col = 0; col < 9; col++){nodes.Add(_sudoku[row, col].Value);}if (!IsNonRepetitive(nodes))return false;}//检测每一行是否合规for (int col = 0; col < 9; col++){nodes.Clear();for (int row = 0; row < 9; row++){nodes.Add(_sudoku[row, col].Value);}if (!IsNonRepetitive(nodes))return false;}//检测每一宫是否合规for (int row = 0; row < 3; row++){for (int col = 0; col < 3; col++){nodes.Clear();nodes.Add(_sudoku[row * 3, col * 3].Value);nodes.Add(_sudoku[row * 3, col * 3 + 1].Value);nodes.Add(_sudoku[row * 3, col * 3 + 2].Value);nodes.Add(_sudoku[row * 3 + 1, col * 3].Value);nodes.Add(_sudoku[row * 3 + 1, col * 3 + 1].Value);nodes.Add(_sudoku[row * 3 + 1, col * 3 + 2].Value);nodes.Add(_sudoku[row * 3 + 2, col * 3].Value);nodes.Add(_sudoku[row * 3 + 2, col * 3 + 1].Value);nodes.Add(_sudoku[row * 3 + 2, col * 3 + 2].Value);if (!IsNonRepetitive(nodes))return false;}}return true;}/// <summary>/// 是否为不重复的且包含1-9数字的集合/// </summary>private bool IsNonRepetitive(HashSet<int> nodes){for (int num = 1; num <= 9; num++){if (!nodes.Contains(num))return false;}return true;}
7.绘制游戏操作说明
最后,操作说明等其他UI统一绘制在OnOtherGUI
方法中:
protected override void OnOtherGUI(){base.OnOtherGUI();Rect rect = new Rect(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 25, 80, 20);GUI.backgroundColor = Color.green;if (GUI.Button(rect, "Done")){//点击Done按钮,检测游戏是否通关if (CheckSudokuQuestion()){IsGameSuccessed = true;}else{ShowNotification(new GUIContent("You are failed, please try again."));}}rect.x += 85;GUI.backgroundColor = Color.yellow;if (GUI.Button(rect, "Restart")){OnRestart();}GUI.backgroundColor = Color.white;}
这里绘制出来的效果如下:
至此,一个简单的数独小游戏就完成了,试玩效果如下:数独【Sudoku】。
8.暂停游戏、退出游戏
同俄罗斯方块。