更新日期:2025年6月17日。
项目源码:后续章节发布
索引
马赛克【Mosaic】
本篇的目标是开发一个马赛克【Mosaic】
小游戏。
一、游戏最终效果
Unity编辑器小游戏:马赛克
二、玩法简介
马赛克
是扫雷游戏的变种,是一款推理游戏,其玩法简单却富有挑战性。
游戏界面由方块阵列组成,玩家需要推理每个方块的正确颜色,并标记为该颜色(分为黑色
和白色
),所以游戏通关后的界面看起来像马赛克
一样,因而得名。
有些方块上会显示一个数字,代表了该方块所在的9宫格中黑色
方块的数量(包含该方块自身),玩家需要通过这些信息来推理逐步找出所有黑色方块。
三、正式开始
1.定义游戏窗口类
首先,定义马赛克的游戏窗口类MiniGame_Mosaic
,其继承至MiniGameWindow【小游戏窗口基类】
:
csharp
/// <summary>
/// 马赛克
/// </summary>
public class MiniGame_Mosaic : MiniGameWindow
{
}
2.规划游戏窗口、视口区域
通过覆写虚属性
实现规划游戏视口区域大小:
csharp
/// <summary>
/// 游戏名称
/// </summary>
public override string Name => "马赛克 [Mosaic]";
/// <summary>
/// 游戏窗体大小
/// </summary>
public override Vector2 WindowSize => new Vector2(700, 530);
/// <summary>
/// 游戏视口区域
/// </summary>
public override Rect ViewportRect => new Rect(5, 25, 500, 500);
注意:游戏窗体大小必须 > 游戏视口区域。
然后通过代码打开此游戏窗口:
csharp
[MenuItem("MiniGame/马赛克 [Mosaic]", priority = 3)]
private static void Open_MiniGame_Mosaic()
{
MiniGameWindow.OpenWindow<MiniGame_Mosaic>();
}
便可以看到游戏的窗口、视口区域如下(左侧深色凹陷区域为视口
区域):

3.地图方块阵列
马赛克游戏的背景也是由一系列方块组成的,所以我们先来绘制如下这样的地图方块阵列:

①.定义方块结构体
首先,定义方块结构体Block
,其代表方块阵列中的一个方块:
csharp
/// <summary>
/// 方块
/// </summary>
public struct Block
{
/// <summary>
/// 方块位置
/// </summary>
public Rect Position;
/// <summary>
/// 是否为黑色
/// </summary>
public bool IsBlack;
/// <summary>
/// 是否为白色
/// </summary>
public bool IsWhite;
/// <summary>
/// 周围九宫格内黑色方块数量
/// </summary>
public int BlackCount;
/// <summary>
/// 是否显示数量文字
/// </summary>
public bool IsShowCount;
}
②.生成方块阵列
我们设计如下四种难度等级
的关卡:
名称 | 地图大小 |
---|---|
初级 | 5*5 |
中级 | 10*10 |
高级 | 15*15 |
大师级 | 20*20 |
csharp
private readonly string[] LEVELS = new string[] { "初级(5*5)", "中级(10*10)", "高级(15*15)", "大师级(20*20)" };
所以游戏视口的宽度、高度是根据大师级难度(方块尺寸25 * 方块宽高20 = 500)
的大小来设置的:
csharp
private const int BLOCKSIZE = 25;
根据选择的不同难度,来生成对应的地图方块阵列:
csharp
private Block[,] _mosaic;
/// <summary>
/// 开始游戏
/// </summary>
private void StartGame()
{
if (_level == 0)
{
WIDTH = 5;
HEIGHT = 5;
}
else if (_level == 1)
{
WIDTH = 10;
HEIGHT = 10;
}
else if (_level == 2)
{
WIDTH = 15;
HEIGHT = 15;
}
else if (_level == 3)
{
WIDTH = 20;
HEIGHT = 20;
}
GenerateMosaic();
}
/// <summary>
/// 生成马赛克矩阵
/// </summary>
private void GenerateMosaic()
{
//生成马赛克矩阵
_mosaic = new Block[WIDTH, HEIGHT];
for (int row = 0; row < WIDTH; row++)
{
for (int col = 0; col < HEIGHT; col++)
{
_mosaic[row, col].Position = new Rect(row * BLOCKSIZE, col * BLOCKSIZE, BLOCKSIZE, BLOCKSIZE);
_mosaic[row, col].IsBlack = Utility.IsTriggerProbability(60);
_mosaic[row, col].IsWhite = false;
_mosaic[row, col].IsShowCount = Utility.IsTriggerProbability((_level == 0 || _level == 1) ? 70 : 60);
}
}
}
在生成方块阵列的方法中,每一个方块有60%
概率为黑色:
csharp
_mosaic[row, col].IsBlack = Utility.IsTriggerProbability(60);
在初级
和中级
时,每一个方块有70%
概率显示九宫格内黑色方块数量,高级
和大师级
为60%
,相应提升了难度:
csharp
_mosaic[row, col].IsShowCount = Utility.IsTriggerProbability((_level == 0 || _level == 1) ? 70 : 60);
③.计算九宫格黑色方块数量
在生成方块阵列完成后,下一步就需要计算每一个方块所属九宫格中的黑色方块数量:
csharp
/// <summary>
/// 生成马赛克矩阵
/// </summary>
private void GenerateMosaic()
{
//......
//计算所有方块所在九宫格的黑色方块数量
for (int row = 0; row < WIDTH; row++)
{
for (int col = 0; col < HEIGHT; col++)
{
_mosaic[row, col].BlackCount = CalculateBlackCount(row, col);
}
}
}
/// <summary>
/// 计算方块所在九宫格的黑色方块数量
/// </summary>
private int CalculateBlackCount(int x, int y)
{
int count = 0;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
int newX = x + i;
int newY = y + j;
if (newX >= 0 && newX < WIDTH && newY >= 0 && newY < HEIGHT && _mosaic[newX, newY].IsBlack)
{
count++;
}
}
}
return count;
}
④.排除任意九宫格内不存在任何一个显示文字方块的情况
我们必须确保,任意九宫格中,至少有一个
方块会显示黑色方块数量,否则会显著提升解题难度
,甚至不可解:
csharp
/// <summary>
/// 生成马赛克矩阵
/// </summary>
private void GenerateMosaic()
{
//......
//排除单一九宫格内不存在任何一个显示文字方块的情况
for (int row = 0; row < WIDTH; row++)
{
for (int col = 0; col < HEIGHT; col++)
{
int count = CalculateShowCount(row, col);
if (count <= 0)
{
_mosaic[row, col].IsShowCount = true;
}
}
}
}
/// <summary>
/// 计算方块所在九宫格的显示文字的数量
/// </summary>
private int CalculateShowCount(int x, int y)
{
int count = 0;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
int newX = x + i;
int newY = y + j;
if (newX >= 0 && newX < WIDTH && newY >= 0 && newY < HEIGHT && _mosaic[newX, newY].IsShowCount)
{
count++;
}
}
}
return count;
}
⑤.设置马赛克题目
马赛克游戏也可以看作是一道逻辑解密题
,由于之前我们随机生成了一些黑色方块,现在部分方块上已经标注了其所在九宫格中黑色方块的数量,现在只需要将所有黑色方块去掉,使玩家通过逻辑推理来寻找黑色方块即可:
csharp
/// <summary>
/// 生成马赛克矩阵
/// </summary>
private void GenerateMosaic()
{
//......
//设置马赛克题目
for (int row = 0; row < WIDTH; row++)
{
for (int col = 0; col < HEIGHT; col++)
{
_mosaic[row, col].IsBlack = false;
}
}
}
4.绘制方块阵列
然后在OnGameViewportGUI
方法中绘制方块阵列:
csharp
//未标记的方块风格
private GUIStyle _noBlockGS;
//已标记的方块风格
private GUIStyle _blockGS;
protected override void OnGameViewportGUI()
{
base.OnGameViewportGUI();
DrawPanel();
}
/// <summary>
/// 绘制画布
/// </summary>
private void DrawPanel()
{
for (int h = 0; h < HEIGHT; h++)
{
for (int w = 0; w < WIDTH; w++)
{
DrawBlock(w, h);
}
}
}
/// <summary>
/// 绘制方块
/// </summary>
private void DrawBlock(int x, int y)
{
string count = _mosaic[x, y].IsShowCount ? _mosaic[x, y].BlackCount.ToString() : "";
if (_mosaic[x, y].IsBlack)
{
GUI.backgroundColor = Color.black;
GUI.Box(_mosaic[x, y].Position, count, _blockGS);
GUI.backgroundColor = Color.white;
}
else if (_mosaic[x, y].IsWhite)
{
GUI.Box(_mosaic[x, y].Position, count, _blockGS);
}
else
{
GUI.Box(_mosaic[x, y].Position, count, _noBlockGS);
}
}
此时就能绘制出游戏的地图方块阵列了,比如初级(5*5)的:

注意:这里有一个选择关卡难度的过程省略了,该过程很简单便不浪费篇幅赘述了,后续在源码中即可一目了然。
两种状态的方块绘制出来大致是这样的:

5.标记方块
在OnGamePlayingEvent
方法中完成标记方块的逻辑:
csharp
protected override void OnGamePlayingEvent(Event e, Vector2 mousePosition)
{
base.OnGamePlayingEvent(e, mousePosition);
if (e.type == EventType.MouseDown)
{
if (e.button == 0)
{
for (int h = 0; h < HEIGHT; h++)
{
for (int w = 0; w < WIDTH; w++)
{
if (_mosaic[w, h].Position.Contains(mousePosition))
{
//鼠标左键标记为黑色(再次点击则取消标记黑色)
_mosaic[w, h].IsWhite = false;
_mosaic[w, h].IsBlack = !_mosaic[w, h].IsBlack;
Repaint();
return;
}
}
}
}
else if (e.button == 1)
{
for (int h = 0; h < HEIGHT; h++)
{
for (int w = 0; w < WIDTH; w++)
{
if (_mosaic[w, h].Position.Contains(mousePosition))
{
//鼠标右键标记为白色(再次点击则取消标记白色)
_mosaic[w, h].IsBlack = false;
_mosaic[w, h].IsWhite = !_mosaic[w, h].IsWhite;
Repaint();
return;
}
}
}
}
}
}
6.检测游戏是否通关
检测游戏是否通关的逻辑为:每一个显示了黑色方块数量
的方块,其所在九宫格内必须真实标记相应数量
的黑色方块,其余标记为白色。
跟扫雷不同的是:每个方块的
黑、白
属性并不固定,只要最终满足每个九宫格的黑色方块数量即可。
csharp
/// <summary>
/// 检测马赛克题目是否完成(游戏是否通关)
/// </summary>
private bool CheckMosaicQuestion()
{
for (int row = 0; row < WIDTH; row++)
{
for (int col = 0; col < HEIGHT; col++)
{
//如果此方块显示了黑色方块数量,则检测其所在九宫格中是否存在相应数量的方块
if (_mosaic[row, col].IsShowCount)
{
int count = CalculateBlackCount(row, col);
//任意九宫格中黑色方块数量不对,则游戏未通关
if (count != _mosaic[row, col].BlackCount)
{
return false;
}
}
}
}
return true;
}
7.绘制游戏操作说明
最后,操作说明等其他UI统一绘制在OnOtherGUI
方法中:
csharp
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;
//玩家可主动点击Done按钮,检测游戏是否通关
if (GUI.Button(rect, "Done"))
{
if (CheckMosaicQuestion())
{
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;
rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 75, 80, 20);
GUI.Button(rect, "Mouse Left");
rect.x += 85;
rect.width = 100;
GUI.Label(rect, "Marked as black");
rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 50, 80, 20);
GUI.Button(rect, "Mouse Right");
rect.x += 85;
rect.width = 100;
GUI.Label(rect, "Marked as white");
}
这里绘制出来的效果如下:

8.游戏技巧
介绍一些游戏技巧(并不是全部)。
①.数字0所在九宫格全为白色
数字0
代表所在九宫格中一个黑色方块也没有:

②.数字9所在九宫格全为黑色
同理,数字9
代表所在九宫格中全为黑色方块:

③.靠边6所在九宫格全为黑色
靠边数字6
代表所在九宫格中有6个黑色方块,但其九宫格只有6个方块,所以全为黑色:

④.精准排除法
如下图方块数字4
,已知其下方2个方块
为白色,排除后其九宫格只剩4个方块,所以4个全为黑色:

⑤.模糊排除法
如下图方块数字5
,已知其下方2个方块
为黑色,则上方4个方块
中只能有3个黑色方块。
转向数字8
,表明其所在九宫格中只有1个白色方块,则其上方
和左侧
方块均为黑色(那1个白色方块在与数字5
的交界区域中)。

至此,一个简单的马赛克小游戏就完成了,简单但却耐玩,试玩效果如下:马赛克【Mosaic】。
9.暂停游戏、退出游戏
同俄罗斯方块。