更新日期:2025年6月14日。
项目源码:后续章节发布
索引
系列简介
本系列博客准备整点不一样的活,有没有想过开发在Unity编辑器中运行的游戏(打开一个EditorWindow
直接玩)?
当然,这样的游戏体量会有亿点点小,毕竟EditorWindow
的资源有限,所以主要为休闲、逻辑、解密类游戏,但好处是打开一个窗口即玩,一键即可关闭窗口,秉承了Unity开箱即用的原则。
且每个小游戏的体量足够小,仅仅为一个脚本
,不含其他任何资源。
只不过,这需要对Unity编辑器开发
具备基础的了解,然后,我们就可以正式开始了。
俄罗斯方块【Tetris】
本篇的目标是开发一个俄罗斯方块【Tetris】
小游戏。
一、游戏最终效果
Unity编辑器小游戏:俄罗斯方块
二、玩法简介
俄罗斯方块是一款经典的益智游戏,其玩法简单却富有挑战性。
玩家需要通过旋转和移动从屏幕顶部不断落下的四连方块
,将它们放置在游戏板的底部。目标是填满一行或多行,当一行被完全填满
时,该行会消除
,玩家获得分数。游戏会随着时间推移逐渐加快难度,玩家需要尽可能多地消除行数,避免方块堆积到游戏板顶部,否则游戏结束。
三、正式开始
1.定义游戏窗口类
首先,定义俄罗斯方块的游戏窗口类MiniGame_Tetris
,其继承至MiniGameWindow【小游戏窗口基类】
:
csharp
/// <summary>
/// 俄罗斯方块
/// </summary>
public class MiniGame_Tetris : MiniGameWindow
{
}
注意:
MiniGameWindow
包含在小游戏基础模块中,其在EditorWindow
中模拟实现了一套小游戏的基础开发组件,譬如:1.游戏视口渲染(Viewport);
2.游戏逻辑更新(Update);
3.MiniGameObject游戏对象:类似于运行时的
GameObject
;4.动画组件(Animation):用于在
EditorWindow
中播放动画;5.简易物理系统:重力、碰撞检测、射线检测等。
后续放出源码后将会深入讲解此基础模块。
2.规划游戏窗口、视口区域
通过覆写虚属性
实现规划游戏视口区域大小:
csharp
/// <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);
注意:游戏窗体大小必须 > 游戏视口区域。
然后通过代码打开此游戏窗口:
csharp
[MenuItem("MiniGame/俄罗斯方块 [Tetris]", priority = 1)]
private static void Open_MiniGame_Tetris()
{
MiniGameWindow.OpenWindow<MiniGame_Tetris>();
}
便可以看到游戏的窗口、视口区域如下(左侧深色凹陷区域为视口
区域):

3.绘制方块背景板
俄罗斯方块游戏实现起来还是比较简单的,因为他的整个游戏背景仅是一系列方块组成,所以我们先来绘制如下这样的方块背景板:

定义背景宽度(横向方块数量)
、背景高度(纵向方块数量)
、方块大小
:
csharp
private const int WIDTH = 10;
private const int HEIGHT = 20;
private const int BLOCKSIZE = 20;
用一个bool型二维数组
存储所有背景方块:
csharp
//所有背景方块
private bool[,] _panel = new bool[WIDTH, HEIGHT];
注意:为什么是bool型?
因为bool型正好可以表示一个方块的状态:true为该位置存在方块,false为该位置不存在方块。
然后在OnGameViewportGUI
方法中绘制背景板:
csharp
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
代表四连方块类型:
csharp
/// <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
代表四连方块:
csharp
/// <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
,即表明了该位置存在方块,从而该位置便会渲染为青色。
④.生成四连方块
游戏一开始,便会生成一个四连方块:
csharp
//定义一个缓存对象,避免每次重复新建对象
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
即为游戏窗口打开后的初始化方法,在其中完成游戏的初始化操作。
⑤.更新四连方块
每次重新生成
、旋转
等,都需要更新四连方块(也即是更新其中四个小方块的位置偏移):
csharp
/// <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
类型:
csharp
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);
}
}
其他类型也同理,这里就不赘述了。
⑥.绘制四连方块
接下来便是绘制四连方块:
csharp
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
方法中编写控制逻辑:
csharp
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.四连方块碰壁检测
四连方块尝试左右移动、旋转时,都需要检测是否超出边界或碰壁:
csharp
/// <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
方法中编写四连方块的下落
逻辑:
csharp
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
即为游戏逻辑更新方法,在其中编写游戏逻辑更新相关的代码。
接下来是四连方块堆叠
的逻辑:
csharp
/// <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.消除方块
在四连方块堆叠
到底部的同时,就需要检测一次是否可消除方块,我们只需要一行一行的检测即可,因为消除方块的前提就是堆满一行
:
csharp
/// <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
方法中:
csharp
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
键,也可覆写该虚属性重定义退出键:
csharp
/// <summary>
/// 退出键
/// </summary>
public virtual KeyCode QuitKey { get; } = KeyCode.Escape;