当前位置: 首页 > news >正文

【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
Jgamma, left gun, inverse L, reverse
Lright gun
Sinverse skew, right snake
Zskew, 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代表了四连方块处于方块背景板中的具体位置,而Block1OffsetBlock4Offset这四个变量,分别代表了四连方块中的四个小方块基于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;

相关文章:

  • Python 自动化测试/脚本
  • 使用 vscode 开发 uni-app 项目时如何解决 manifest.json 文件注释报错的问题
  • Java-46 深入浅出 Tomcat 核心架构 Catalina 容器全解析 启动流程 线程机制
  • Linux集市采购指南[特殊字符]:yum和apt的“抢货”大战!
  • 【Linux教程】Linux 生存指南:掌握常用命令,避开致命误操作
  • 如何安全高效地维护CMS智能插件?
  • 计算机网络-自顶向下—第三章运输层重点复习笔记
  • 系统架构设计师 2
  • 【DVWA系列】——JavaScript——Medium详细教程
  • 人工智能学习22-Pandas
  • el-table跨页多选和序号连续
  • nodejs和npm升级
  • Lambda 表达式的语法与使用:更简洁、更灵活的函数式编程!
  • awesome-llm-apps 项目带你探索语言模型的无限可能
  • tshark命令行语法详解
  • 华为云Flexus+DeepSeek征文 | 模型即服务(MaaS)安全攻防:企业级数据隔离方案
  • ARDM:一款国产跨平台的Redis管理工具
  • frida-android-mod-menu 使用教程
  • 怎么理解自动驾驶技术中的agent
  • Python 爬虫入门 Day 3 - 实现爬虫多页抓取与翻页逻辑
  • 办公家具网站模版/中国足彩网竞彩推荐
  • 盐城做网站需要多少钱/如何做网站建设
  • 幼儿园网站建设方案结语/山东济南最新消息
  • 简单网站设计模板/惠州seo整站优化
  • 网站如何做seo规划/最经典的营销案例
  • 如何攻击织梦做的网站/seo关键词seo排名公司