俄罗斯方块

思考并回答以下问题:

  • 洪水填充(Flood fill)算法是什么?

游戏规则

  • 1.有一块用于摆放方块的平面虚拟场地,其标准大小:行宽为10,列高为20,以一个小方块为一个单位。
  • 2.有一组由4个方块组成的规则图形(中文通称为方块),共有7种,分别以I、J、L、S、T、Z、O这7个字母的形状来命名,如图1所示。

俄罗斯方块的7种形状

  • 3.玩家可以进行如下操作。

    • 以90度为单位旋转方块;
    • 以格子为单位左右移动方块;
    • 让方块加速下落。
      方块移到区域最下方或者着地到其他方块上无法移动时,就会固定在该处,而新的方块会出现在区域上方开始落下。
  • 4.当区域中的某一行格子全部由方块填满,则该行方块会消失并成为玩家的得分。删除的行越多,得分越高。当方块堆到区域最上方而无法消除时,则该游戏结束。

  • 5.一般来说,游戏会提示下一个要落下的方块。
  • 6.几种常见的游戏模式如下。
    • 经典马拉松:在该模式下游戏无计时,一直到方块组堆到最上方并且无法消除时,游戏结束。
    • 竞速模式:消除指定的行数,用时最短者获胜。
    • 定时模式:在一定的时间内,得分最高者获胜。
    • 重力模式:传统版本的俄罗斯方块将堆栈的块向下移动一段距离,正好等于它们之下清除的行的高度。与重力定律相反,块可以悬空在间隙之上,如图2所示。实现使用洪水填充的不同算法将游戏区域分割成连接的区域将使每个区域并行落下,直到它接触到游戏场底部的区域。这开启了额外的“连锁反应”策略,涉及块级联以填补额外的线路,这可能被认为是更有价值的清除,如图3所示。

图2 传统模式

图3 重力模式下的连锁反应

在本章中,我们实现的是俄罗斯方块的经典马拉松模式。

  • 7.降落方式如下。
    • 硬降:方块立即下落到最下方并锁定。
    • 软降:方块加速下落。
  • 8.旋转踢墙:指一个方块即使旋转后重合了左右墙壁或现有方块也有能旋转的能力。在NES(红白机)版本中,如果一个Z竖立着靠着左边的墙壁,玩家就不能旋转这个方块,给人的感觉就像是锁定了一样,在这种情况下,玩家必须在旋转之前把方块组向右移动一格,但就会失去宝贵的时间。

游戏实现思路

随机生成方块

用一个数组存放7种方块组,用生成随机数的方式选择生成方块组形状。

地图的生成

将场景看作是一个10 ×20的二维数组,每一个小方块占1个单位。每个格子在数组中的索引号为这个格子的坐标。

判断方块是否都在边界内

左右移动方块时,获取方块组中每个子方块的坐标以及边界的坐标,进行比较以判断方块组是否在边界内。若在边界内,更新方块组的坐标数据。

1
2
3
4
5
6
7
8
9
for(每个子方块)
{
  if(方块X坐标 > 左边界的X坐标 && 方块X坐标 < 右边界的X坐标)
  {
    return false; // 超出边界
  }

return true; // 在边界内
}

方块组旋转时超出边界的情况,如图4所示。

图4 旋转超出边界

解决办法如下。

  • 不能旋转。应该在旋转之后,循环遍历方块组的每一个方块,判断是否都在边界内,若有子方块不在边界内,再反方向旋转回去,这种方式在视觉上看起来就好像方块组被冻结,无法旋转。
  • 能够旋转。在旋转后,遍历方块组的所有子方块,如果有方块的位置不在边界内,则将每个子方块的坐标向边界内移动,这种情况被称为旋转踢墙。

判断是否碰到其他方块

循环遍历网格数组,检测方块组要到达的地方是否为空,若为空,则继续移动,更新方块位置的坐标数据,若不为空,则停止移动。

检查是否满行

在地图中,位于同一行的方块的y坐标相同,遍历数组中y坐标相同的数据是否为空。若都不为空,则表示y坐标为y的一行被填满。

1
2
3
4
5
6
for(int x = 0; x < 宽度; x++)
{
if((x,y)位置上没有方块)
     return false;
  return true;
}

删除填满的行

删除该行数组的元素,即将数组中该行的数据全部置为空。再遍历上面所有行,将数组中的所有元素的y坐标减1,达到下落的效果。

1
2
3
4
5
6
7
8
for(int y = 0; y < height;y++)
{
if(高度为Y的一行被填满)
  {
删除y行的所有元素;
遍历y行上面所有的行,将上面所有行的Y坐标-1;
  }
}

提示下一个方块组

判断是否是第一次生成方块,若是,则产生两个随机数,一个是当前的方块组的编号,一个是下一个方块组的编号;若不是,则将当前的编号置为下一个方块组的编号,再产生一个随机数表示下一个方块组的编号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool isFirst = true;
int currentNum = 0;  //当前方块组的编号
int nextNum = 0;    //下一个方块组的编号
if(isFirst)
{
currentNum = 方块组数组长度内的一个随机数;
nextNum = 方块组数组长度内的一个随机数;
isFirst = false;
}
else
{
currentNum = nextNum;
nextNum = 方块组数组长度内的一个随机数;
}
产生编号为currentNum的方块组;
绘制编号为nextNum的方块组;

结束判定

获取产生的方块组的坐标,判断方块组中的子方块的坐标是否超过上边界,如果超出上边界,则游戏结束,如果没有超出上边界,则游戏继续。

游戏流程图

如图5所示。

游戏程序实现

点击下载纹理文件

Spawner.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Spawner : MonoBehaviour
{
public GameObject[] Blocks;//储存方块组的数组
public Sprite[] sprites;//储存方块组图片的数组,用于界面提示下一个产生的方块组
public static bool isFirst = true;//是否第一次产生方块
public static int current = 0; //当前方块的序号
public static int next = 0; //下一个产生的方块序号

// Use this for initialization
void Start ()
{
SpawnerNext();
}

/// <summary>
/// 产生方块组
/// </summary>
public void SpawnerNext()
{

if (isFirst)
{
isFirst = false;
current = Random.Range(0, Blocks.Length);
next = Random.Range(0, Blocks.Length);
}
else
{
current = next;
next = Random.Range(0, Blocks.Length);
}

//随机产生方块
Instantiate(Blocks[current], transform.position, Quaternion.identity);
//在界面中显示出图片
GameObject.Find("Image").GetComponent<Image>().sprite = sprites[next];
}
}

Group.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Group : MonoBehaviour
{

public float lastFall = 0;

// Use this for initialization
void Start ()
{
if (!isValidGridPos())
{
Debug.Log("Game Over");
Destroy(gameObject);
}
}

/// <summary>
/// 检测用户输入,移动方块
/// </summary>

// Update is called once per frame
void Update ()
{
//向左
if(Input.GetKeyDown(KeyCode.LeftArrow))
{
transform.position += new Vector3(-1, 0, 0);

if (isValidGridPos())
{
updateGrid();

}
else
transform.position += new Vector3(1, 0, 0);
}

//向右
if (Input.GetKeyDown(KeyCode.RightArrow))
{
transform.position += new Vector3(1, 0, 0);

if (isValidGridPos())
{
updateGrid();

}
else
transform.position += new Vector3(-1, 0, 0);
}

//旋转
if (Input.GetKeyDown(KeyCode.UpArrow))
{
transform.Rotate(0, 0, -90);

if (isValidGridPos())
{
updateGrid();

}
else
transform.Rotate(0,0,90);
}

//加速下落
if (Input.GetKeyDown(KeyCode.DownArrow)||Time.time-lastFall>1)
{
transform.position += new Vector3(0, -1, 0);

if (isValidGridPos())
{
updateGrid();

}
else
{
transform.position += new Vector3(0, 1, 0);
Done_Grid.DeleteFullRows();

//FindObjectsOfType<Spawner>().spawnNext();
FindObjectOfType<Done_Spawner>().SpawnerNext();

enabled = false;

}

lastFall = Time.time;
}
}

/// <summary>
/// 位置是否合理
/// </summary>
/// <returns></returns>
bool isValidGridPos()
{
foreach(Transform child in transform)
{
Vector2 v = Done_Grid.RoundVec2(child.position);

if(!Done_Grid.InsideBorder(v))
{
return false;
}
if(Done_Grid.grid[(int)v.x,(int)v.y]!=null&&Done_Grid.grid[(int)v.x,(int)v.y].parent!=transform)
{
return false;
}
}
return true;
}

/// <summary>
/// 更新网格状态
/// </summary>
void updateGrid()
{
for(int y = 0; y < Done_Grid.height; y++)
{
for(int x = 0; x < Done_Grid.width; x++)
{
if(Done_Grid.grid[x,y]!=null)
{
if(Done_Grid.grid[x,y].parent==transform)
{
Done_Grid.grid[x, y] = null;
}
}
}
}

foreach(Transform child in transform)
{
Vector2 v = Done_Grid.RoundVec2(child.position);
Done_Grid.grid[(int)v.x, (int)v.y] = child;
}
}
}

Grid.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 储存一些函数
/// </summary>

public class Done_Grid : MonoBehaviour
{
public static int width = 10;//游戏场景的宽度
public static int height = 20;//游戏场景的高度
public static int score = 0;//分数

public static Transform[,] grid = new Transform[width, height];

/// <summary>
/// 对坐标进行取整
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
public static Vector2 RoundVec2(Vector2 v)
{
return new Vector2(Mathf.Round(v.x), Mathf.Round(v.y));
}

/// <summary>
/// 放快组是否在边界内
/// </summary>
/// <param name="pos"></param>
/// <returns></returns>
public static bool InsideBorder(Vector2 pos)
{
return ((int)pos.x >= 0 && (int)pos.x < width && (int)pos.y >= 0);
}

/// <summary>
/// 删除行
/// </summary>
/// <param name="y">行号</param>
public static void DeleteRow(int y)
{
for(int x = 0; x < width; x++)
{
Destroy(grid[x, y].gameObject);
grid[x, y] = null;
}
}

/// <summary>
/// 将删除行的上面一行下降
/// </summary>
/// <param name="y">行号</param>

public static void DecreaseRow(int y)
{
for(int x = 0; x < width; x++)
{
if(grid[x,y]!=null)
{
grid[x, y - 1] = grid[x, y];
grid[x, y] = null; //将上一行的方块一下去

grid[x, y - 1].position += new Vector3(0, -1, 0);//坐标下落
}
}
}

/// <summary>
/// 将上面所有行往下移
/// </summary>
/// <param name="y">行号</param>
public static void DecreaseRowAbove(int y)
{
for(int i = y;i<height;i++)
{
DecreaseRow(i);
}
}

/// <summary>
/// 判断一行是否被填满
/// </summary>
/// <param name="y">行号</param>
/// <returns></returns>
public static bool IsRowFull(int y)
{
for(int x = 0; x < width; x++)
{
if(grid[x,y]==null)
{

return false;
}
}
return true;
}

/// <summary>
/// 删除所有填满的行
/// 1、先判断一行是否填满,若填满,就删除
/// 2、删除上面填满的行。
/// 3、分数+1
/// </summary>
public static void DeleteFullRows()
{
for(int y = 0; y < height; y++)
{
if(IsRowFull(y))
{
DeleteRow(y);
score++;
SetScore(score);
DecreaseRowAbove(y + 1);
y--;
}
}
}

/// <summary>
/// 设置分数
/// </summary>
/// <param name="s">分数</param>
public static void SetScore(int s)
{
GameObject.Find("Score").GetComponent<Text>().text = ""+s;
}
}
0%