【Unity】MiniGame编辑器小游戏(一)俄罗斯方块【Tetris】
更新日期:2025年6月14日。
项目源码:后续章节发布
索引
- 系列简介
- 俄罗斯方块【Tetris】
- 一、游戏最终效果
- 二、玩法简介
- 三、正式开始
- 1.定义游戏窗口类
- 2.规划游戏窗口、视口区域
- 3.绘制方块背景板
- 4.四连方块
- ①.四连方块简介
- ②.定义四连方块类型
- ③.定义四连方块
- ④.生成四连方块
- ⑤.更新四连方块
- ⑥.绘制四连方块
- 5.控制四连方块(移动、旋转)
- 6.四连方块碰壁检测
- 7.堆叠方块
- 8.消除方块
- 9.绘制游戏操作说明
- 10.暂停游戏
- 11.退出游戏
系列简介
本系列博客准备整点不一样的活,有没有想过开发在Unity编辑器中运行的游戏(打开一个EditorWindow
直接玩)?
当然,这样的游戏体量会有亿点点小,毕竟EditorWindow
的资源有限,所以主要为休闲、逻辑、解密类游戏,但好处是打开一个窗口即玩,一键即可关闭窗口,秉承了Unity开箱即用的原则。
且每个小游戏的体量足够小,仅仅为一个脚本
,不含其他任何资源。
只不过,这需要对Unity编辑器开发
具备基础的了解,然后,我们就可以正式开始了。
俄罗斯方块【Tetris】
本篇的目标是开发一个俄罗斯方块【Tetris】
小游戏。
一、游戏最终效果
Unity编辑器小游戏:俄罗斯方块
二、玩法简介
俄罗斯方块是一款经典的益智游戏,其玩法简单却富有挑战性。
玩家需要通过旋转和移动从屏幕顶部不断落下的四连方块
,将它们放置在游戏板的底部。目标是填满一行或多行,当一行被完全填满
时,该行会消除
,玩家获得分数。游戏会随着时间推移逐渐加快难度,玩家需要尽可能多地消除行数,避免方块堆积到游戏板顶部,否则游戏结束。
三、正式开始
1.定义游戏窗口类
首先,定义俄罗斯方块的游戏窗口类MiniGame_Tetris
,其继承至MiniGameWindow【小游戏窗口基类】
:
/// <summary>/// 俄罗斯方块/// </summary>public class MiniGame_Tetris : MiniGameWindow{}
注意:
MiniGameWindow
包含在小游戏基础模块中,其在EditorWindow
中模拟实现了一套小游戏的基础开发组件,譬如:
1.游戏视口渲染(Viewport);
2.游戏逻辑更新(Update);
3.MiniGameObject游戏对象:类似于运行时的GameObject
;
4.动画组件(Animation):用于在EditorWindow
中播放动画;
5.简易物理系统:重力、碰撞检测、射线检测等。
后续放出源码后将会深入讲解此基础模块。
2.规划游戏窗口、视口区域
通过覆写虚属性
实现规划游戏视口区域大小:
/// <summary>/// 游戏名称/// </summary>public override string Name => "俄罗斯方块 [Tetris]";/// <summary>/// 游戏窗体大小/// </summary>public override Vector2 WindowSize => new Vector2(400, 430);/// <summary>/// 游戏视口区域/// </summary>public override Rect ViewportRect => new Rect(5, 25, 200, 400);
注意:游戏窗体大小必须 > 游戏视口区域。
然后通过代码打开此游戏窗口:
[MenuItem("MiniGame/俄罗斯方块 [Tetris]", priority = 1)]private static void Open_MiniGame_Tetris(){MiniGameWindow.OpenWindow<MiniGame_Tetris>();}
便可以看到游戏的窗口、视口区域如下(左侧深色凹陷区域为视口
区域):
3.绘制方块背景板
俄罗斯方块游戏实现起来还是比较简单的,因为他的整个游戏背景仅是一系列方块组成,所以我们先来绘制如下这样的方块背景板:
定义背景宽度(横向方块数量)
、背景高度(纵向方块数量)
、方块大小
:
private const int WIDTH = 10;private const int HEIGHT = 20;private const int BLOCKSIZE = 20;
用一个bool型二维数组
存储所有背景方块:
//所有背景方块private bool[,] _panel = new bool[WIDTH, HEIGHT];
注意:为什么是bool型?
因为bool型正好可以表示一个方块的状态:true为该位置存在方块,false为该位置不存在方块。
然后在OnGameViewportGUI
方法中绘制背景板:
private Rect _blockRect = new Rect();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++){//存在方块显示青色,不存在显示灰色GUI.color = _panel[w, h] ? Color.cyan : Color.gray;DrawBlock(w, h);GUI.color = Color.white;}}}/// <summary>/// 绘制方块/// </summary>private void DrawBlock(int x, int y){_blockRect.Set(x * BLOCKSIZE, y * BLOCKSIZE, BLOCKSIZE, BLOCKSIZE);GUI.Box(_blockRect, "", _blockGS);}
注意:
OnGameViewportGUI
即为游戏视口区域的GUI绘制方法,其中左上角坐标为(0, 0)
,右下角坐标为(ViewportRect.width, ViewportRect.height)
:
4.四连方块
①.四连方块简介
在俄罗斯方块中,我们所控制的从顶部下落的方块叫做四连方块
,四连方块共有7
种类型:
他们的字母命名如下:
方块 | 中文别称 | 英文别称 |
---|---|---|
I | 长条、棍子 | long bar, stick, straight |
T | ||
O | 方形、田 | square, block, sun |
J | gamma, left gun, inverse L, reverse | |
L | right gun | |
S | inverse skew, right snake | |
Z | skew, left snake, reverse S |
②.定义四连方块类型
使用一个枚举TetrominoType
代表四连方块类型:
/// <summary>/// 四连方块类型/// </summary>public enum TetrominoType{/// <summary>/// 口口口口/// </summary>I,/// <summary>/// 口/// 口口口/// </summary>J,/// <summary>/// ㅤㅤ 口/// 口口口/// </summary>L,/// <summary>/// 口口/// 口口/// </summary>O,/// <summary>/// ㅤ口口/// 口口/// </summary>S,/// <summary>/// 口口/// ㅤ 口口/// </summary>Z,/// <summary>/// ㅤ 口/// 口口口/// </summary>T}
③.定义四连方块
定义一个类Tetromino
代表四连方块:
/// <summary>/// 四连方块/// </summary>public class Tetromino{/// <summary>/// 四连方块的位置/// </summary>public Vector2Int Position;/// <summary>/// 四连方块的旋转/// </summary>public int Rotation;/// <summary>/// 四连方块的类型/// </summary>public TetrominoType Type;/// <summary>/// 方块1的位置偏移/// </summary>public Vector2Int Block1Offset;/// <summary>/// 方块2的位置偏移/// </summary>public Vector2Int Block2Offset;/// <summary>/// 方块3的位置偏移/// </summary>public Vector2Int Block3Offset;/// <summary>/// 方块4的位置偏移/// </summary>public Vector2Int Block4Offset;}
这里的Position
代表了四连方块处于方块背景板
中的具体位置,而Block1Offset
至 Block4Offset
这四个变量,分别代表了四连方块中的四个小方块
基于Position
的位置偏移值。
通过Position
+ 偏移值(Offset)
即可算出小方块真实的位置,然后将该位置标记为true
,即表明了该位置存在方块,从而该位置便会渲染为青色。
④.生成四连方块
游戏一开始,便会生成一个四连方块:
//定义一个缓存对象,避免每次重复新建对象private Tetromino _tetrominoCache = new Tetromino();/// <summary>/// 当前的四连方块/// </summary>public Tetromino CurrentTetromino { get; private set; }protected override void OnInit(){base.OnInit();CurrentTetromino = GenerateTetromino();}/// <summary>/// 生成四连方块/// </summary>private Tetromino GenerateTetromino(){//四连方块坐标重置到(4, -1),y = -1使得他超出到屏幕上方不可见_tetrominoCache.Position = new Vector2Int(4, -1);//四连方块旋转归零_tetrominoCache.Rotation = 0;//随机一个方块类型_tetrominoCache.Type = (TetrominoType)Random.Range(0, 7);//更新一下四个小方块的位置_tetrominoCache.UpdateBlocks();return _tetrominoCache;}
注意:
OnInit
即为游戏窗口打开后的初始化方法,在其中完成游戏的初始化操作。
⑤.更新四连方块
每次重新生成
、旋转
等,都需要更新四连方块(也即是更新其中四个小方块的位置偏移):
/// <summary>/// 更新方块/// </summary>public void UpdateBlocks(){switch (Type){case TetrominoType.I:UpdateI();break;case TetrominoType.J:UpdateJ();break;case TetrominoType.L:UpdateL();break;case TetrominoType.O:UpdateO();break;case TetrominoType.S:UpdateS();break;case TetrominoType.Z:UpdateZ();break;case TetrominoType.T:UpdateT();break;}}
根据不同的四连方块类型,需要重新计算四个小方块的位置偏移,比如I
类型:
private void UpdateI(){//未旋转(角度0)、旋转180度时,显示为横着的(——),所以四个小方块横向排列(y坐标偏移为0)if (Rotation == 0 || Rotation == 180){Block1Offset = new Vector2Int(-1, 0);Block2Offset = new Vector2Int(0, 0);Block3Offset = new Vector2Int(1, 0);Block4Offset = new Vector2Int(2, 0);}//旋转90度时、旋转180度时,显示为竖着的(|),所以四个小方块竖向排列(x坐标偏移为0)else if (Rotation == 90 || Rotation == 270){Block1Offset = new Vector2Int(0, -1);Block2Offset = new Vector2Int(0, 0);Block3Offset = new Vector2Int(0, 1);Block4Offset = new Vector2Int(0, 2);}}
其他类型也同理,这里就不赘述了。
⑥.绘制四连方块
接下来便是绘制四连方块:
protected override void OnGameViewportGUI(){base.OnGameViewportGUI();//绘制方块背景板DrawPanel();//绘制四连方块(绘制顺序靠后,会在层级上挡住前面的背景板)DrawTetromino();}/// <summary>/// 绘制四连方块/// </summary>private void DrawTetromino(){if (CurrentTetromino != null){//四连方块绘制为黄色GUI.color = Color.yellow;//分别计算四个小方块的偏移量,然后绘制该小方块DrawBlock(CurrentTetromino.Position.x + CurrentTetromino.Block1Offset.x, CurrentTetromino.Position.y + CurrentTetromino.Block1Offset.y);DrawBlock(CurrentTetromino.Position.x + CurrentTetromino.Block2Offset.x, CurrentTetromino.Position.y + CurrentTetromino.Block2Offset.y);DrawBlock(CurrentTetromino.Position.x + CurrentTetromino.Block3Offset.x, CurrentTetromino.Position.y + CurrentTetromino.Block3Offset.y);DrawBlock(CurrentTetromino.Position.x + CurrentTetromino.Block4Offset.x, CurrentTetromino.Position.y + CurrentTetromino.Block4Offset.y);GUI.color = Color.white;}}
现在运行程序大概就是这样的:
5.控制四连方块(移动、旋转)
我们可以控制四连方块左右移动、旋转、加速下落等。
在OnGamePlayingEvent
方法中编写控制逻辑:
protected override void OnGamePlayingEvent(Event e, Vector2 mousePosition){base.OnGamePlayingEvent(e, mousePosition);if (CurrentTetromino != null){if (e.type == EventType.KeyDown){switch (e.keyCode){case KeyCode.A://A键左移CurrentTetromino.Position.x -= 1;//检测是否超出边界或碰壁if (CheckTetrominoMove(CurrentTetromino)){//如果是,则撤销移动CurrentTetromino.Position.x += 1;}Repaint();break;case KeyCode.D://D键右移CurrentTetromino.Position.x += 1;if (CheckTetrominoMove(CurrentTetromino)){CurrentTetromino.Position.x -= 1;}Repaint();break;case KeyCode.W://W键旋转int last = CurrentTetromino.Rotation;//顺时针旋转90度CurrentTetromino.Rotation += 90;if (CurrentTetromino.Rotation >= 360) CurrentTetromino.Rotation = 0;CurrentTetromino.UpdateBlocks();//同样的,如果超出边界或碰壁,撤销旋转if (CheckTetrominoMove(CurrentTetromino)){CurrentTetromino.Rotation = last;CurrentTetromino.UpdateBlocks();}Repaint();break;case KeyCode.S://S键加速下落,直接增加下落速度即可_downSpeed = 0.1f;break;}}}}
注意:
OnGamePlayingEvent
即为游戏输入事件检测方法,在其中编写与输入事件相关的逻辑。
6.四连方块碰壁检测
四连方块尝试左右移动、旋转时,都需要检测是否超出边界或碰壁:
/// <summary>/// 四连方块尝试左右移动、旋转时,检测是否超出边界或碰壁/// </summary>private bool CheckTetrominoMove(Tetromino tetromino){int x1 = tetromino.Position.x + tetromino.Block1Offset.x;int y1 = tetromino.Position.y + tetromino.Block1Offset.y;if (x1 < 0 || x1 >= WIDTH || y1 < 0 || y1 >= HEIGHT || _panel[x1, y1]){return true;}int x2 = tetromino.Position.x + tetromino.Block2Offset.x;int y2 = tetromino.Position.y + tetromino.Block2Offset.y;if (x2 < 0 || x2 >= WIDTH || y2 < 0 || y2 >= HEIGHT || _panel[x2, y2]){return true;}int x3 = tetromino.Position.x + tetromino.Block3Offset.x;int y3 = tetromino.Position.y + tetromino.Block3Offset.y;if (x3 < 0 || x3>= WIDTH || y3 < 0 || y3 >= HEIGHT || _panel[x3, y3]){return true;}int x4 = tetromino.Position.x + tetromino.Block4Offset.x;int y4 = tetromino.Position.y + tetromino.Block4Offset.y;if (x4 < 0 || x4 >= WIDTH || y4 < 0 || y4 >= HEIGHT || _panel[x4, y4]){return true;}return false;}
7.堆叠方块
四连方块落到底部
后,或碰到其他已落底的方块
,将堆叠
到底部。
在OnGamePlayingUpdate
方法中编写四连方块的下落
逻辑:
private float _downSpeed = 0.005f;private float _timer = 0;protected override void OnGamePlayingUpdate(){base.OnGamePlayingUpdate();if (CurrentTetromino != null){_timer += _downSpeed;if (_timer >= 1){_timer -= 1;//四连方块下落一格CurrentTetromino.Position.y += 1;//检测是否落地CheckTetrominoDown(CurrentTetromino);//重绘窗口(游戏视口内容产生变化时,都需要调一下)Repaint();}}}
注意:
OnGamePlayingUpdate
即为游戏逻辑更新方法,在其中编写游戏逻辑更新相关的代码。
接下来是四连方块堆叠
的逻辑:
/// <summary>/// 四连方块尝试下落时,检测是否落地,如果落地,则存储到背景板,并检测是否可消除/// </summary>private void CheckTetrominoDown(Tetromino tetromino){//检测是否落地if (TetrominoIsDown(tetromino)){//如果已落底,则倒回一格(我们是先下落一格,再判断的是否落底,所以要倒回去)tetromino.Position.y -= 1;//存储到背景板(堆叠到底部)SetTetrominoToPanel(tetromino);//检测是否可消除EliminatePanel();//重新生成四连方块CurrentTetromino = GenerateTetromino();_downSpeed = 0.005f;}}/// <summary>/// 检测四连方块是否落地/// </summary>private bool TetrominoIsDown(Tetromino tetromino){//分别检测四个小方块是否抵达边界,或碰到已落底的方块//任意小方块满足条件,则证明整个四连方块已落底int x1 = tetromino.Position.x + tetromino.Block1Offset.x;int y1 = tetromino.Position.y + tetromino.Block1Offset.y;if (x1 >= 0 && x1 < WIDTH && y1 >= 0 && y1 < HEIGHT && _panel[x1, y1]){return true;}int x2 = tetromino.Position.x + tetromino.Block2Offset.x;int y2 = tetromino.Position.y + tetromino.Block2Offset.y;if (x2 >= 0 && x2 < WIDTH && y2 >= 0 && y2 < HEIGHT && _panel[x2, y2]){return true;}int x3 = tetromino.Position.x + tetromino.Block3Offset.x;int y3 = tetromino.Position.y + tetromino.Block3Offset.y;if (x3 >= 0 && x3 < WIDTH && y3 >= 0 && y3 < HEIGHT && _panel[x3, y3]){return true;}int x4 = tetromino.Position.x + tetromino.Block4Offset.x;int y4 = tetromino.Position.y + tetromino.Block4Offset.y;if (x4 >= 0 && x4 < WIDTH && y4 >= 0 && y4 < HEIGHT && _panel[x4, y4]){return true;}if (y1 >= HEIGHT || y2 >= HEIGHT || y3 >= HEIGHT || y4 >= HEIGHT){return true;}return false;}/// <summary>/// 设置四连方块到画板/// </summary>private void SetTetrominoToPanel(Tetromino tetromino){//在方块背景板中,分别将四个小方块所在的位置设置为true,也即是该位置存在方块int x1 = tetromino.Position.x + tetromino.Block1Offset.x;int y1 = tetromino.Position.y + tetromino.Block1Offset.y;if (x1 >= 0 && x1 < WIDTH && y1 >= 0 && y1 < HEIGHT){_panel[x1, y1] = true;}int x2 = tetromino.Position.x + tetromino.Block2Offset.x;int y2 = tetromino.Position.y + tetromino.Block2Offset.y;if (x2 >= 0 && x2 < WIDTH && y2 >= 0 && y2 < HEIGHT){_panel[x2, y2] = true;}int x3 = tetromino.Position.x + tetromino.Block3Offset.x;int y3 = tetromino.Position.y + tetromino.Block3Offset.y;if (x3 >= 0 && x3 < WIDTH && y3 >= 0 && y3 < HEIGHT){_panel[x3, y3] = true;}int x4 = tetromino.Position.x + tetromino.Block4Offset.x;int y4 = tetromino.Position.y + tetromino.Block4Offset.y;if (x4 >= 0 && x4 < WIDTH && y4 >= 0 && y4 < HEIGHT){_panel[x4, y4] = true;}}
8.消除方块
在四连方块堆叠
到底部的同时,就需要检测一次是否可消除方块,我们只需要一行一行的检测即可,因为消除方块的前提就是堆满一行
:
/// <summary>/// 检测消除/// </summary>private void EliminatePanel(){//单次总消除行数,一次消除行数越多,得分越高int lines = 0;//从最底部向上检测for (int h = HEIGHT - 1; h >= 0; h--){bool isEliminate = true;for (int w = 0; w < WIDTH; w++){//只要一行中发现一个空方格,就不可消除if (!_panel[w, h]){isEliminate = false;break;}}if (isEliminate){//消除此行EliminateLine(h);lines += 1;h++;}}//得分_score += lines * lines;}/// <summary>/// 消除一行/// </summary>private void EliminateLine(int line){//消除一行的逻辑也即是其上方的所有行向下顺移一格for (int h = line; h >= 1; h--){for (int w = 0; w < WIDTH; w++){_panel[w, h] = _panel[w, h - 1];}}}
9.绘制游戏操作说明
游戏的得分,操作说明等其他UI统一绘制在OnOtherGUI
方法中:
protected override void OnOtherGUI(){base.OnOtherGUI();Rect rect = new Rect(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + 5, 50, 20);GUI.Label(rect, "Score:");rect.x += 50;rect.width = 100;EditorGUI.IntField(rect, _score);rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 100, 25, 20);GUI.Button(rect, "W");rect.x += 30;rect.width = 100;GUI.Label(rect, "Rotate");rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 75, 25, 20);GUI.Button(rect, "S");rect.x += 30;rect.width = 100;GUI.Label(rect, "Fast down");rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 50, 25, 20);GUI.Button(rect, "A");rect.x += 30;rect.width = 100;GUI.Label(rect, "Move left");rect.Set(ViewportRect.x + ViewportRect.width + 5, ViewportRect.y + ViewportRect.height - 25, 25, 20);GUI.Button(rect, "D");rect.x += 30;rect.width = 100;GUI.Label(rect, "Move right");}
注意:
OnOtherGUI
为绘制游戏视口区域之外的其他UI的方法,但不做强制限制。
这里绘制出来的效果如下:
至此,一个简单的俄罗斯方块
小游戏就完成了,试玩效果如下:俄罗斯方块【Tetris】。
10.暂停游戏
游戏窗口的左上角有一个三角形播放
按钮,默认情况下,打开窗口后该按钮处于按下状态(游戏播放中),点击该按钮可暂停游戏:
11.退出游戏
默认退出游戏按键为Esc
键,也可覆写该虚属性重定义退出键:
/// <summary>/// 退出键/// </summary>public virtual KeyCode QuitKey { get; } = KeyCode.Escape;