TETRIS—基于现代官方规则俄罗斯方块实现案例(c/c++,EasyX,附完整代码)

简介:
作为方块圈的玩家,和刚接触 EasyX 的萌新。我希望大家能够重新认识一下这款经久不衰的游戏。
官方规则是基于俄罗斯方块公司(The Tetris Company,TTC)公司授权的方块游戏规则。如俄罗斯方块效应,噗哟噗哟VS俄罗斯方块等游戏采用的规则。
大部分人对方块的印象大多还停留在上个世纪的 ”消消乐“ 吧?我将一边科普现代方块规则, 一边介绍个人的实现思路,而不是随随便便写一个消消乐。
也希望大家可以去体验一下方块的魅力 !
先来介绍一下现代方块界面吧

噗哟噗哟 VS 俄罗斯方块 2 游戏界面。
介绍 / 规则
标准场地是 10 X 20 个小方块场地,共有 7 个方块,一般也都采用固定 7 种颜色。比如我看到黄色就知道是正方形来了。
来认识一下各个方块,现代方块以 “ T ” 方块为尊(紫色),“ I ” 方块居其次(棍子/长条),红色 Z 绿色 S,一个正方形 O 和 JL。
七个字母对应形状,方块也是有名字的,TIOZSJL 每个方块都由 4 个小方块组成,场地里填满一行就会消除,然后相爱相杀!
HOLD:储存一个方块,可与当前操作的方块交换一下。(是从上面重新下来的不是直接和当前位置交换,每次锁定前只能交换一次)
NEXT:这个好理解看见下一个方块是什么。(官方的好像没有超过 5next 的)
GHOST:影子,可以看见当前方块落下后的样子。(游戏里都是可以选择开启/关闭的)
7bg:七巡,方块的出块规则。随机排列这 7 个不同方块为一包,一包一包出方块。
操作:软降(加速下落)/硬降(当前方块直接下落到最底部锁定)左移动/右移动/软/顺时针旋转/逆时针旋转/ HOLD(暂存按键)。标准七个按键。
方块落到最下面不会锁定,还可以旋转和移动如果 1.5 秒(大约)不操作才会锁定,硬降就是直接落下锁定。
对战伤害系统就先不提了,可以来看看下面的视频链接。(与人斗其乐无穷!)
代码思路解析(个人理解)
场地:想必大家也都理解要用二维数组储存场地数据,xy 坐标和 ij 数组下标方便后面功能判断。
#define Column 10 // 10 列
#define RowNum 22 // 22 行,现代方块场地理论上应该更高
const int Radius = 20; // 方块大小
// 枚举每一个小方块的填充和未填充状态
enum T_STATE {NoBlock, IsBlock};
// 表示方向
enum DIRECTION {Up1, Right2, Down3, left4};
// 表示当前方块种类
enum TKIND {T, I, O, S, Z, L, J};
// 创建单个方块结构体
class A_TETRIS
{
public:
int x, y; // 储存小方块的中心 X Y 坐标
int i, j; // 储存小方块的在地图数组下标 i j
T_STATE T_state; // 每一个小方块的状态,存在和不存在
IMAGE T_im; // 一个小方块的皮肤
}
// 打包的 4 个方块都有一个旋转中心点
struct FOUR_TETRIS
{
A_TETRIS TheFourT[4]; // 打包 4 个为一个
int centerX, centerY; // 储存旋转中心位置
float centeri, centerj; // 储存旋转中心 ij 下标,为什么是 float 呢,特殊方块中心点不规则
DIRECTION Direction = Up1;
TKIND Kind; // 方块种类
}
// 定义地图数据结构体
class TETRIS_MAP
{
public:
A_TETRIS Map_T[RowNum][Column]; // 地图二维数组
}
由每一个小方块的数据构成一个地图数据,然后画出来。3 个枚举也好理解,地图里就用一个是否有方块状态,有就显示图片没有就不显示。
你也可以直接使用简单的填充矩形,后期随变改皮肤。数组构成一个地图,每个方块由 4 个小方块组成。
那么我再定义一个结构体,里面由包含 4 个小方块数据的一维数组组成。
我直接写进去 4 个元素数据,就组成了我要的任意形状方块。
方块一个一个按顺序出来。那么我定义一个类,表示序列。
class SEVEN_BG// 创建七循相关结构体
{
public:
FOUR_TETRIS Four_T; // 用于初始化加载的方块
vector<FOUR_TETRIS> SevenList; // 储存初始化的 7 个方块链表
vector<FOUR_TETRIS> PutSevenT; // 用来存放固定的 7 个方块
vector<FOUR_TETRIS> NextList; // 存放 NEXT 序列
}
序列存放方块数据,我想到的就是链表,#include <vector> //矢量模板也是链表,我用的这个。
这里用了 3 个链表储存单个方块信息,第一个里只有 7 个方块数据,我直接暴力写入 7 个方块数据,存到这链表里
void initialize() // 简单粗暴的直接写入 7bg 初始数据
{ // 加载顺序 T I O S Z L J 超出边界的方块手动计算赋值一下
Four_T.TheFourT[0] = MapDate.Map_T[1][4];
Four_T.TheFourT[1] = MapDate.Map_T[2][3];
Four_T.TheFourT[2] = MapDate.Map_T[2][4];
Four_T.TheFourT[3] = MapDate.Map_T[2][5];
for (int i =0; i<4; i++)
{
Four_T.TheFourT[i].T_state = IsBlock; // 也就第一次要加载这个
loadimage(&Four_T.TheFourT[i].T_im, _T("image / T.png"));
}
Four_T.centerX = Four_T.TheFourT[2].x; // T 的中心点
Four_T.centerY = Four_T.TheFourT[2].y;
Four_T.centeri = Four_T.TheFourT[2].i; // 记录下标
Four_T.centerj = Four_T.TheFourT[2].j;
Four_T.Kind = T;
SevenList.push_back(Four_T); // 将一个 T 方块信息储存到了 7BG 链表
}
手写七·个方块数据到第一个链表里,这里省略。
第二个链表复制一遍第一个链表,然后写一个简单的随机取出链表里的函数
int Rand7BG(int Max)// 生成两个数之间的随机数。就是选择下一个方块
{ // max 不能为 0。思考一下取 0 的余是啥?
int num =int(rand()%Max);
return num;
}
7 个方块嘛,循环 7 次不就全取完了,随机取 7 次,取一次 Max 减一。就得到随机方块顺序,然后存到 第三个序列链表里。
取完了再第二链表再复制一遍第一个链表,再随机取出给第三个序列链表,这就是出块规则了。
当前操作的方块就等于第三个链表里的第一个数据,我定义了一个当前方块类,依然引用 4 个单个方块构成的一个方块结构体。
class NOW_TETRIS// 创建当前方块结构体
{
public:
int EraseRow[4]; // 要消除的行的 i 标
int whatRL = 0; // 右旋转为 1,左旋转为-1
FOUR_TETRIS Now_FourT; // 当前方块信息
FOUR_TETRIS Ghost_FourT; // 存放影子信息
FOUR_TETRIS Rotate_FourT; // 用来计算旋转是否成立
FOUR_TETRIS SeekSpin_FourT; // 正常旋转不行就来计算偏移
}
当前方块信息算一个,影子算一个,也就等于链表里的第一个数据。锁定后删除链表第一个数据然后再复制第一个数据,源源不断的生成。
void OnceDown() // 一次下落执行的数据
{ y + = 2 * Radius;
i + = 1; }
单个方块结构体的移动
void OnceUp() // 一个整体方块的一次向上移动
{
for (int i =0; i<4; i++)
TheFourT[i].OnceUp();
centeri - = 1;
centerY - = 2 * Radius;
}
移动简单,下落还是移动都是这种一格一格的,调用然后放到循环里更新就好了。判断能不能移动和下落稍微麻烦一点点而已。
int InspectDown(A_TETRIS &Now, A_TETRIS &Next) // 检查下一个下降位置是否有冲突
{
if (Now.i > = RowNum - 1 || Next.T_state ! = NoBlock)
return 1;
else return 0;
}
int JudgeWhetherDown() // 判断是否可以下落
{
for (int i = 0; i<4; i++)
{ // 下面这串是检查当前方块下路是否和地图里的有冲突
if (InspectDown(Now_FourT.TheFourT[i], MapDate.Map_T[Now_FourT.TheFourT[i].i + 1][Now_FourT.TheFourT[i].j]))
return 0;
}
return 1;
}
每一个小方块里都有一个枚举表示当前是否有方块,也有数组下标数据,提前判断一下它移动后的位置有没有存在方块,和边界判断,如上。
我这里旋转也是提前将旋转过后的数据存一个然后和地图数据比对。
旋转在现代方块规则里是非常重要的!绕旋转中心点旋转,旋转中心各位可以运行后面源文件里程序观看,如下。

旋转中心每个方块都不一样,这个数据也在一开始链表里写数据的直接写了,在 4 个小方块结构体里也定义了中心点数据。以这个点是以为官方标准,旋转系统称为 SRS 系统。
如何通过旋转中心坐标计算旋转后 4 个小方块的坐标。下面这个可是我打了不少草稿发现规律简化的!
A_TETRIS LoadRspin(A_TETRIS &oneT) // 载入右旋转,输入一个单方块用来计算他旋转后的坐标位置
{
float xd = centerX - oneT.x;
float yd = centerY - oneT.y;
float jd = centerj - oneT.j;
float id = centeri - oneT.i;
oneT.x = centerX + yd;
oneT.j = centerj + id;
oneT.y = centerY - xd;
oneT.i = centeri - jd;
return oneT;
}
A_TETRIS LoadLspin(A_TETRIS &oneT) // 载入左旋转,输入一个单方块用来计算他旋转后的坐标位置
{
float xd = centerX - oneT.x;
float yd = centerY - oneT.y;
float jd = centerj - oneT.j;
float id = centeri - oneT.i;
oneT.x = centerX - yd;
oneT.j = centerj - id;
oneT.y = centerY + xd;
oneT.i = centeri + jd;
return oneT;
}
上面就是如何通过旋转中心坐标计算旋转后 4 个小方块的坐标。这个可是我打了不少草稿发现规律简化的!
得到旋转过后的数据了,要和地图里的数据比较。防止重叠嘛,但是当方块移动到边上后,旋转过后的样子肯定超出边界了。
此外还有特殊旋转,你可能会觉得下面这个有点离谱。但这都是真的,当然也是有条件的,也有别的奇怪旋转。

这里就要用到官方设定偏移表了。(千万不要研究什么旋转规律!我痛苦的回忆!)
以下面 i 块为例子,我也会在压缩包里放上完整偏移表。

i 的偏移最特殊,虽然它只有横竖两个状态,注意看旋转中心位置,它是不一样的。
来看看消除
int JudgeErase(int &row) // 判断当前行是否消除
{
int clear = 0; // 判断是否清除。满足 10 行就清除
for (int i = 0; i< Column; i ++)
{
if(Map_T[row][i].T_state == IsBlock)
clear ++;
}
if (clear == 10)
return row;
else return - 1;
}
void eraseline(int &row) // 获取到要消除的行,执行消行后需要做的
{
for (int j = 0; j<Column; j ++) // 首先删除当前行的方块
Map_T[row][j].T_state = NoBlock;
for (int T = row - 1; T>=0; T--) // 当前要消除的上一行开始遍历
for (int j = 0; j<Column; j ++) // 列
{ // 要消除的肯定得是不是方块状态
if (Map_T[T][j].T_state == IsBlock) // 只有遍历到存在方块的数据才开始执行下落
{ // 降消除后落下的方块的信息修改正确
Map_T[T + 1][j].T_im = Map_T[T][j].T_im;
Map_T[T + 1][j].T_state = IsBlock;
Map_T[T][j].T_state = NoBlock; // 当前方块给下面后删除
}
}
}
我这里是每次落下方块,获取当前落下方块的 4 个小方块的 i 坐标,对应地图里那一行有没有满足 10 个,满足就代表这一行要消除,让当前行消失,状态为 noblock 没有方块。上面的方块往下移动一格,从当前 i 行便利走最上面第 22 行,对应 i 下标为 0。
HOLD 交换功能就是和当前方块交换一下嘛,然后显示在右上角就好了。下落就写个时间触发移动一下,不能移动后过 1 秒不操作就锁定,数据写到地图里。
GOHST 影子数据一开始肯定和当前方块一样位置也一样,直接判断影子方块能不能往下移动一格,这个判断放到 while 里,可以下落就继续下落,直到不能下落才是影子位置的数据。
写一个找到影子位置的函数,只在方块左右移动的时候调用一下,别的操作不要调用。移动还有 das 什么操作要求啥的,等我下次把别的功能做好再来更新吧。目前我写的这个操作起来还不错,我就是玩方块的玩起来手感还行哦。
好就写到这吧,源代码都有注释应该容易理解,比较详细。(有些注释自己写着玩的请见谅)
http://farter.cn/t/ 块圈群主“屁大爷”的屁块!(挖掘模式很有意思很上头)
http://tetr.io 目前圈里很火的在线对战方块
墨白的源码 :https://pan.baidu.com/s/1FmIBp5sobAqZb-h870BWWw?pwd=gi68
提取码:gi68
调试环境 VS200 (没有EasyX库的可以去官网看一下https://easyx.cn/)