使用uGUI系统玩转标准俄罗斯方块

使用uGUI系统玩转标准俄罗斯方块

笔者使用的Unity3D版本是4.6b17。由于一些工作上的一些事情导致制作的进度被严重滞后。笔者实际用于开发俄罗斯方块的时间,大概也就2-3天吧。

开始前的准备

想制作一个标准俄罗斯方块小游戏。先了解俄罗斯方块的游戏规则。它有I,L,J,T,O,S,Z,共7种基础形状。每一种形状可以根据一定规则变化。当一行排满消除这一行并获得相对应的游戏分数。

假如将俄罗斯方块的所有图形都可以理解为是一个九宫格9格方块的变形。俄罗斯方块的7种基础图形都可以根据9宫格削减某些部分而组成。

J型:

  T型:

  L型:

  实际游戏中笔者发现这三种图形可以通过根据红色的点为中心点顺时针旋转90度形成下一个形态的形状。一个周期为4次变形,即旋转360°之后的图形会和当前的图形重叠。

  S型:

  Z型:

I型:(是四格的,这里只显示3格是为了看上去像九宫格,实际游戏中,I型的上面还有一个格子)

  以上三种图形,笔者参考了大量的实际游戏后发现。有别于TJL三种图形。他们只有进行顺时针90度和逆时针90度这样两种变化形式。

  O型:

  O型图形是最特殊的一种方块,所以放在最后。O型是7种图形中唯一没有形态变换的形状。

  分析了7种基础图案之后。笔者最先实现的是7种方块的变化。

  写代码之前先根据我们上面的分析设计一下。

  总结如下:方块是一个Box对象。每个方块都有一个旋转中心点。都有一个状态。TLJ有四种状态、SZI有两种状态、O只有一种状态。

 1 /// <summary>
 2     /// 方块旋转中心
 3     /// </summary>
 4     public Vector2 Center { get; set; }
 5
 6     /// <summary>
 7     /// 方块类型
 8     /// </summary>
 9     public BoxType Type { get; set; }
10
11     /// <summary>
12     /// 方块状态 - 用于区别方块的形态
13     /// </summary>
14     public int State { get; set; }
15
16     /// <summary>
17     /// 方块点描述
18     /// </summary>
19     public Vector2[] Pos = new Vector2[4];

  Box工具类。有一些用于创建。操作方块等很多方法:

 1 /// <summary>
 2     /// 方块坐标工厂
 3     /// </summary>
 4     /// <param name="center">中心点</param>
 5     /// <param name="type">方块类型</param>
 6     /// <param name="state">状态</param>
 7     /// <returns>返回方块实例</returns>
 8     public static Vector2[] BoxPositionFactory(Vector2 center, BoxType type, int state)
 9     {
10         var pos = new Vector2[4];
11         switch (type)
12         {
13             case BoxType.S:
14                 /*    *
15                  *    **
16                  *     *
17                  */
18                 pos[0] = new Vector2(center.x, center.y + 1);
19                 pos[1] = new Vector2(center.x, center.y);
20                 pos[2] = new Vector2(center.x + 1, center.y);
21                 pos[3] = new Vector2(center.x + 1, center.y - 1);
22                 break;
23             case BoxType.Z:
24                 /*
25                  *    **
26                  *   **
27                  */
28                 pos[0] = new Vector2(center.x, center.y);
29                 pos[1] = new Vector2(center.x - 1, center.y);
30                 pos[2] = new Vector2(center.x, center.y - 1);
31                 pos[3] = new Vector2(center.x + 1, center.y - 1);
32                 break;
33             case BoxType.L:
34                 pos[0] = new Vector2(center.x - 1, center.y);
35                 pos[1] = new Vector2(center.x, center.y - 1);
36                 pos[2] = new Vector2(center.x + 1, center.y - 1);
37                 pos[3] = new Vector2(center.x - 1, center.y - 1);
38                 break;
39             case BoxType.J:
40                 pos[0] = new Vector2(center.x + 1, center.y);
41                 pos[1] = new Vector2(center.x, center.y - 1);
42                 pos[2] = new Vector2(center.x + 1, center.y - 1);
43                 pos[3] = new Vector2(center.x - 1, center.y - 1);
44                 break;
45             case BoxType.I:
46                 pos[0] = new Vector2(center.x, center.y + 1);
47                 pos[1] = new Vector2(center.x, center.y + 2);
48                 pos[2] = new Vector2(center.x, center.y);
49                 pos[3] = new Vector2(center.x, center.y - 1);
50                 break;
51             case BoxType.O:
52                 pos[0] = new Vector2(center.x, center.y);
53                 pos[1] = new Vector2(center.x + 1, center.y);
54                 pos[2] = new Vector2(center.x + 1, center.y - 1);
55                 pos[3] = new Vector2(center.x, center.y - 1);
56                 break;
57             case BoxType.T:
58                 pos[0] = new Vector2(center.x, center.y);
59                 pos[1] = new Vector2(center.x - 1, center.y);
60                 pos[2] = new Vector2(center.x + 1, center.y);
61                 pos[3] = new Vector2(center.x, center.y + 1);
62                 break;
63             default:
64                 throw new ArgumentOutOfRangeException();
65         }
66         switch (state)
67         {
68             case 2:
69                 pos = BoxUtil.Rotate(pos, center, Math.PI / 2);
70                 break;
71             case 3:
72                 pos = BoxUtil.Rotate(pos, center, Math.PI);
73                 break;
74             case 4:
75                 pos = BoxUtil.Rotate(pos, center, (Math.PI * 3) / 2);
76                 break;
77         }
78         return pos;
79     }
 1 /// <summary>
 2     /// 根据中心点选择
 3     /// </summary>
 4     /// <param name="center">中心点</param>
 5     /// <param name="point">当前点</param>
 6     /// <param name="angle">角度的弧度</param>
 7     /// <returns></returns>
 8     public static Vector2 BoxRotate(Vector2 point, Vector2 center, double angle)
 9     {
10         /**
11          * 复数法:
12          * http://www.topschool.org/sx/sx/200904/1601.html
13          * http://www.tesoon.com/ask/htm/04/16403.htm
14          */
15         angle = -angle;
16
17         var result = default(Vector2);
18 #if !USE_COMPLEX
19     // 方式一:实现复数乘法计算
20         var x = (float)((point.x - center.x) * Math.Cos(angle) - (point.y - center.y) * Math.Sin(angle));
21         var y = (float)((point.x - center.x) * Math.Sin(angle) + (point.y - center.y) * Math.Cos(angle));
22         result = new Vector2(center.x + x, center.y + y);
23 #elif
24     // 方式二:利用复数类进行计算
25         var complex = new Complex(point.x - center.x, point.y - center.y) * new Complex(Math.Cos(angle), Math.Sin(angle)) + new Complex(center.x, center.y);
26         result = new Vector2((float)complex.Real, (float)complex.Image);
27 #endif
28         return result;
29     }

  ps:A点根据中线点B旋转某个角度,获得点C。笔者这里使用的复数法。由于复数是.net4.5版本之后才支持的。所以笔者这里默认是直接计算。没有.net 4.5框架的朋友,笔者在示例中增加了网上找的Complex类实现。在源码中BoxUtil类中定义了计算方法。有兴趣的可以在文章的末尾看到下载链接。欢迎下载

方块的运动

  在俄罗斯方块游戏中。任意图形都有下降、左移、右移、变化四种基础操作,方块根据难度自动下落的速度逐渐加快(表现为下落的时间间隔会逐渐缩短)。上面的Box对象类增加Down,Left,Right,Change四个方法。

  在实际游戏中还有快速左移、快速右移、快速下降等操作。为了满足这样的需求我们为这些操作增加了时间间隔和标示符。本例中经过一番的调整,最终决定将快速相关操作的时间间隔设置为50毫秒。游戏本身的基础自动下落的时间间隔为500毫秒,根据难度逐步缩短此间隔值,已达到难度加大的设计。

  1 /// <summary>
  2     /// 最后一次左移或者右移
  3     /// </summary>
  4     private long _timeLastLeftOrRight;
  5
  6     /// <summary>
  7     /// 最后一次下降
  8     /// </summary>
  9     private long _timeLastAutoDown;
 10
 11     /// <summary>
 12     /// 是否快速下降
 13     /// </summary>
 14     public bool IsFastDown;
 15
 16     /// <summary>
 17     /// 快速左移
 18     /// </summary>
 19     public bool IsFastLeft;
 20
 21     /// <summary>
 22     /// 快速右移
 23     /// </summary>
 24     public bool IsFastRight;
 25
 26     /// <summary>
 27     /// 左移
 28     /// </summary>
 29     public void Left()
 30     {
 31         var now = DateTime.Now.Ticks;
 32         if (now - _timeLastLeftOrRight <= BoxUtil.INTERVAL_FAST) return;
 33         _timeLastLeftOrRight = now;
 34         var position = new Vector2[Pos.Length];
 35         for (var i = 0; i < Pos.Length; i++)
 36             position[i] = Pos[i] - Vector2.right;
 37         if (IsCanChange(Blocks, position))
 38         {
 39             Center -= Vector2.right;
 40             Pos = position;
 41         }
 42     }
 43
 44     /// <summary>
 45     /// 右移
 46     /// </summary>
 47     public void Right()
 48     {
 49         var now = DateTime.Now.Ticks;
 50         if (now - _timeLastLeftOrRight <= BoxUtil.INTERVAL_FAST) return;
 51         _timeLastLeftOrRight = now;
 52         var position = new Vector2[Pos.Length];
 53         for (var i = 0; i < Pos.Length; i++)
 54             position[i] = Pos[i] + Vector2.right;
 55         if (IsCanChange(Blocks, position))
 56         {
 57             Center += Vector2.right;
 58             Pos = position;
 59         }
 60     }
 61
 62     /// <summary>
 63     /// 下落
 64     /// </summary>
 65     public void Down()
 66     {
 67         var dynamicInterval = 500;
 68         var now = DateTime.Now.Ticks;
 69         if ((now - _timeLastAutoDown <= dynamicInterval*10000) &&
 70             (!IsFastDown || now - _timeLastAutoDown <= BoxUtil.INTERVAL_FAST)) return;
 71         _timeLastAutoDown = now;
 72         var position = new Vector2[Pos.Length];
 73         for (var i = 0; i < Pos.Length; i++)
 74             position[i] = Pos[i] - Vector2.up;
 75         if (IsCanDown(position))
 76         {
 77             Center -= Vector2.up;
 78             Pos = position;
 79         }
 80         else
 81         {
 82             IsGameOver(Pos);
 83         }
 84     }
 85
 86     /// <summary>
 87     /// 方块变换
 88     /// </summary>
 89     public void Change()
 90     {
 91         var now = DateTime.Now.Ticks;
 92         if (now - _timeLastLeftOrRight <= BoxUtil.INTERVAL_FAST) return;
 93         _timeLastLeftOrRight = now;
 94         var targetPos = new Vector2[Pos.Length];
 95         for (var i = 0; i < Pos.Length; i++)
 96             targetPos[i] = Pos[i];
 97         switch (Type)
 98         {
 99             case BoxType.L:
100             case BoxType.J:
101             case BoxType.T:
102                 BoxUtil.Rotate(targetPos, Center);
103                 break;
104             case BoxType.S:
105             case BoxType.Z:
106             case BoxType.I:
107                 var isClockWise = State == 1;
108                 State = isClockWise ? 2 : 1;
109                 BoxUtil.Rotate(targetPos, Center, isClockWise);
110                 break;
111             case BoxType.O:
112                 // Do nothing
113                 break;
114             default:
115                 throw new ArgumentOutOfRangeException();
116         }
117         if (IsCanChange(Blocks, targetPos))
118             Pos = targetPos;
119     }

边界和结束检测

实际游戏中,当方块下落移动到游戏界面的底部,此方块将会被固定在底部的位置。新的方块将会在顶部生成,然后下落。如此往复循环直至新生成的方块一次都不能下落则游戏结束。游戏的左移、右移、下落操作都不能超过游戏的边界。

 1 /// <summary>
 2     /// 检查是否游戏结束
 3     ///
 4     /// <br/>
 5     /// 当不能移动时,原始方块的点超出边界则说明游戏结束
 6     /// </summary>
 7     /// <param name="position">点位置</param>
 8     /// <returns>是否游戏结束</returns>
 9     private bool IsGameOver(Vector2[] position)
10     {
11         foreach (var vector2 in position)
12         {
13             if (vector2.x < 0 || vector2.y < 0 || vector2.x >= BoxUtil.MAX_X || vector2.y >= BoxUtil.MAX_Y)
14             {
15                 if (GameOver != null) GameOver(this, EventArgs.Empty);
16                 return true;
17             }
18         }
19         return false;
20     }
21
22     /// <summary>
23     /// 方块是否可以下落
24     /// </summary>
25     /// <param name="tp">方块的点集合</param>
26     /// <returns>返回方块是否可以下落</returns>
27     private bool IsCanDown(IEnumerable<Vector2> tp)
28     {
29         foreach (var vector2 in tp)
30         {
31             if (vector2.y < 0 || (vector2.y < BoxUtil.MAX_Y && Blocks[(int) vector2.x, (int) vector2.y] == 1))
32             {
33                 if (MoveCompleted != null) MoveCompleted(this, EventArgs.Empty);
34                 return false;
35             }
36         }
37         return true;
38     }
39
40     /// <summary>
41     /// 方块是否可以变形
42     /// </summary>
43     /// <param name="blocks">阻挡信息</param>
44     /// <param name="positions">目标形状</param>
45     /// <returns>方块是否可以变形</returns>
46     private static bool IsCanChange(int[,] blocks, IEnumerable<Vector2> positions)
47     {
48         foreach (var position in positions)
49         {
50             if (position.x < 0 || position.x >= BoxUtil.MAX_X || position.y < 0)
51                 return false;
52             if ((position.y < BoxUtil.MAX_Y && blocks[(int) position.x, (int) position.y] == 1))
53                 return false;
54         }
55         return true;
56     }

随机方块生成算法

俄罗斯方块的方块随机生成算法。笔者最开始使用的是全部状态的全部随机。但是在测试游戏中感觉体验并不是很好。经过一番的资料查找和学习之后。将源码中的随机算法修改为—随机包方式。这种方式大概的内容就是,将每7个图形制作成一个包,游戏就按照一个包一个包的进行下去。这样的好处就是相同的两个图形出现的间隔最大不会超过12个。降低了完全随机生成的偶然性。

修改完之后。顺便把下个方块的预览和积分统计计算实现。

 1 /// <summary>
 2     /// 随机方块包生成
 3     /// </summary>
 4     /// <returns>返回方块包</returns>
 5     public static List<BoxProperty> BagRandomizer()
 6     {
 7         var array = new[] {BoxType.I, BoxType.J, BoxType.L, BoxType.S, BoxType.Z, BoxType.O, BoxType.T};
 8         var result = new List<BoxProperty>(array.Length);
 9         var random = new System.Random();
10         var indexList = new List<int>();
11         while (result.Count < array.Length)
12         {
13             var index = random.Next(0, array.Length);
14             if (indexList.Contains(index))
15                 continue;
16             indexList.Add(index);
17
18             var state = 1;
19             switch (array[index])
20             {
21                 case BoxType.S:
22                 case BoxType.I:
23                 case BoxType.Z:
24                     state = random.NextDouble() > 0.5 ? 1 : 2;
25                     break;
26                 case BoxType.L:
27                 case BoxType.T:
28                 case BoxType.J:
29                     state = random.Next(1, 4);
30                     break;
31                 case BoxType.O:
32                     state = 1;
33                     break;
34                 default:
35                     throw new ArgumentOutOfRangeException();
36             }
37
38             Vector2 center = random.NextDouble() > 0.5 ? new Vector2(5, 20) : new Vector2(6, 20);
39
40             result.Add(new BoxProperty(center, array[index], state));
41         }
42         return result;
43     }

增加一个开始菜单

为游戏增加一个入口菜单。用于控制程序的进行和结束。

 1 /// <summary>
 2     /// 开始游戏
 3     /// </summary>
 4     public void OnStartGame()
 5     {
 6         if (IsGameStart) return;
 7         IsGameStart = true;
 8         IsGameOver = false;
 9
10         var find = Root.Find("PanelMenu");
11         if (find != null)
12             find.gameObject.SetActive(false);
13         var game = Instantiate(PrefabPanelMain) as GameObject;
14         if (game != null)
15         {
16             game.transform.SetParent(Root, false);
17             game.transform.name = "Main";
18         }
19         Root.gameObject.AddComponent<Main>();
20     }
21
22     /// <summary>
23     /// 结束游戏
24     /// </summary>
25     public void OnGameOver()
26     {
27         IsGameStart = false;
28         // 返回Menu界面
29         var game = Root.Find("Main");
30         if (game != null)
31             Destroy(game.gameObject);
32         Destroy(Root.GetComponent<Main>());
33
34         var find = Root.Find("PanelMenu");
35         if (find != null)
36             find.gameObject.SetActive(true);
37     }

总结

  至此,一个简单的单机的完全使用UGUI系统制作的俄罗斯方块小游戏就算完成了。UI系统的大致全部模块都有使用到。也算是对新系统的熟悉和学习吧。O(∩_∩)O哈哈~。

  后续看情况可能会继续完善此例,增加以下新的功能和完善一下界面,优化一下算法等等。

  本例使用到了Reference resolution来简单解决了屏幕自适应

  源码下载地址:http://pan.baidu.com/s/1o6lxWIe

后记

源码中实现了简单的屏幕自适应。但是写得比较粗糙。功能实现的也不是特别的完善。期盼有人能提供好的解决方案。共同交流进步。先谢谢各位不吝赐教。

作者:TinyZ
出处:http://www.cnblogs.com/zou90512/
关于作者:努力学习,天天向上。不断探索学习,提升自身价值。记录经验分享。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接
如有问题,可以通过 [email protected] 联系我,非常感谢。
笔者网店: http://aoleitaisen.taobao.com. 欢迎广大读者围观

时间: 2024-10-11 18:33:15

使用uGUI系统玩转标准俄罗斯方块的相关文章

苹果系统玩转“中文名”的背后 其实是焦虑症发作了

原本双11是让所有商家都为之"虎躯一震"的大促节日,就算不能一夜暴富,起码也能减少库存.增加店铺的销量,让数字变得好看.但对于苹果这一以往在双11备受青睐的"宠儿"来说,这次却引来一场"灾难",iPhone 8系列的价格止不住地雪崩.而对于苹果来说,似乎能做的就是在系统上玩花样. 据了解,在苹果即将推送的iOS.watchOS和macOS三大系统和相关应用中,国内消费者将看到后者相对应的中文名字.这究竟是苹果突然开窍了想脚踏实地去为中国消费者服务

SAP系统玩阴的?

近日和项目上的ABAP开发顾问一起弄一个自开发的报表.其中某个栏位的取值需要从批次主数据里抓取到供应商代码,然后根据供应商代码取到供应商名称等.为此笔者需要备功能说明书.在说明书里笔者需要将具体取值逻辑写清楚.即要取到批次主记录里的'供应商'字段,如下图示, 根据物料号+批次号组合取供应商代码100823. 习惯性的,笔者将鼠标放在供应商字段上点击F1键调出帮助文档, 由该字段的技术信息表明,它的技术名称是MCHA-LIFNR.如上图示. 可以当笔者使用事务代码SE16 +表名 MCHA, 输入

COMSCI系统和JWFD系统PC配置标准

下面的标准为  强制标准 CPU: 使用 intel公司  65-45纳米工艺制程   Q6600,  E2140-E2160,E5200,Q9400,Q9500,Q9550,Q9650 主板:使用 华硕,技嘉,微星  这三家公司出的带南桥和北桥芯片的P35-P45主板 显卡:   ATI公司的3650-4650-5650系列GPU显卡 显示器:美国优派-三只鸟的标志的液晶显示器 硬盘:希捷公司的机械硬盘   1T-单碟盘-5400转 光驱:先锋DVD刻录机(不准使用蓝光,蓝光带时流数据,可以读

ROS系统玩转自主移动机器人(3)-- 开源机器人结构介绍

本机器人机械结构设计相关的所有设计文件下载地址为:传送门  其中包含:三维造型设计文件(所有零件+装配效果)(tips:基于Solidworks 2015 绘制) 非标加工的零件图纸(PDF格式+Dwg格式) 本开源机器人项目首要目的是让感兴趣的朋友花很少的钱就能玩转功能简单的机器人,了解机器人的搭建过程并学习ROS系统(如机器人SLAM),因此结构设计的目标和原则很明确:设计一款轮式机器人,满足功能要求的基础上尽量做到成本低廉,总之花小钱多办事. 例如,最终笔者在非标准金属加工件的的总投入是1

XBox360自制系统玩机心得

我有一台XBox360,去年在奸商那里花了150块刷了自制系统,下面是我的玩机心得: 查看XBox360的系统版本信息

(系统架构)标准Web系统的架构分层

标准Web系统的架构分层 1.架构体系分层图 在上图中我们描述了Web系统架构中的组成部分.并且给出了每一层常用的技术组件/服务实现.需要注意以下几点: 系统架构是灵活的,根据需求的不同,不一定每一层的技术都需要使用.例如:一些简单的CRM系统可能在产品初期并不需要K-V作为缓存:一些系统访问量不大,并且可能只有一台业务服务器存在,所以不需要运用负载均衡层. 业务系统间通信层并没有加入传统的HTTP请求方式.这是因为HTTP请求-响应的延迟比较高,并且有很多次和正式请求无关的通信(这在下面的内容

ROS系统玩转自主移动机器人(5)-- ROS系统建模

注:本篇博文全部源码下载地址为:Git Repo传送门. 1. 下载到本地后解压到当前文件夹然后运行:catkin_make 编译. 2. 源码是在 Ubuntu14.04 + Indigo 环境下编写. 前面博文已经介绍了机器人平台的机械结构设计.嵌入式硬件平台的搭建等内容,从本片开始介绍本开源机器人平台ROS系统的相关程序,主要有: ROS系统建模: Gazebo仿真: ROS系统机器人SLAM框架: SLAM中Gmapping和地图构建: SLAM中AMCL算法: 机器人正逆运动学: 路径

Linux系统编程_4_标准I/O(附:清空缓冲区方法)

标准I/O属于库文件,系统调用和库是有区别的,为了方便,标准库中实现了和所有系统调用同名的函数:参考<APUE> 这里部分不解释过多,网上的资料很多,其实熟悉的人基本都知道,我们不可能记住所有的函数的,特别是参数等等,我们能做的就是尽量熟悉他,用到时查一下就能用就行了. 标准I/O函数,摘自于网络: 当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针.该对象通常是一个结构,它包含了标准I/O库为管理该流所需的所有信息,包括:用于实际I/O的文件描述符.指向用于该缓冲区的指针

Android系统中标准Intent的使用

一 Android系统用于Activity的标准Intent 1 根据联系人ID显示联系人信息 Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); //显示联系人信息 intent.setData(Uri.parse("content://contacts/people/492")); startActivity(intent); 2 根据联系人ID显示拨号面板 Intent intent = new