使用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/
关于作者:从事于网络游戏服务端开发(JAVA)。喜欢接触和了解新技术。通过不断探索学习,提升自身价值。记录经验分享。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接
如有问题,可以通过 zou90512@126.com 联系我,非常感谢。
笔者网店: http://aoleitaisen.taobao.com. 欢迎围观