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

【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;
http://www.xdnf.cn/news/14170.html

相关文章:

  • [学习] 牛顿迭代法:从数学原理到实战
  • Nginx、CDN、 DNS的关系解析
  • ​​信息系统项目管理师-信息系统工程 知识点总结与例题分析​​
  • 单项链表的操作及其实现
  • 重定向与缓冲区:C语言IO的奥秘(模拟封装glibc)
  • 工业PID算法在温控器的应用与参数说明
  • 《单调队列》题集
  • list is not in GROUPBY clause and contains nonaggregated column ‘*.*‘
  • Windows10电脑开始菜单快速查找应用程序
  • I/O模式之epoll,本文会讲到epoll的相关接口以及底层,还会涉及水平和边缘工作模式,以及通过epoll相关接口实现一个水平工作模式服务端
  • 【DRL】强化学习中的概念和术语
  • 用数学融智学人力资源模型的核心架构:建立可量化的理论框架
  • openMP的简单介绍以及c++执行实例
  • JS递归了解
  • k3s入门教程(三)部署控制面板
  • 第六章 进阶19 琦琦的追求
  • AI数字人:几分钟克隆,短视频制作新革命
  • leetcode-hot-100 (链表)
  • C语言结构体与联合体详解
  • Windows批处理脚本(.bat脚本、.bat语法)关闭回显@echo off、延迟变量扩展setlocal enabledelayedexpansion
  • 【教程】Windows安全中心扫描设置排除文件
  • Ubuntu Server 24.04|22.04|20.04|18.04 安装GUI DESKTOP xfce4
  • 文本表示的发展概述
  • SpringAI使用总结
  • [蓝桥杯 2023 国 B] AB 路线 (BFS)
  • 事务传播行为详解
  • 学习日记-day29-6.13
  • SpringBoot+vue前后端分离系统开发(期末)
  • 让高端装备“先跑起来”:虚拟仿真验证平台重塑研制流程
  • HarmonyOS5 运动健康app(二):健康跑步(附代码)