说真的我对俄罗斯方块的了解也只有小时候在家里的电视上玩过几盘的俄罗斯方块,一直都是没玩几分钟就输掉了,或者是一直在等那个长条。
最近在想要做个什么游戏当做范例的时候呢,得到了“要不来做个俄罗斯方块吧”这样的建议,于是现在就来试一试吧。
俄罗斯方块的要求
首先,俄罗斯方块有7种方块,然后我才惊奇地发现原来所有方块都是由四个方格组成的。
在这次的设计中,咱们采用一个10*20的空间,方块会从上方落下,满一行就会消一行,在方块的下落过程中你可以旋转它或者是加速让它下落。
不过在做的时候,得知了一种叫做t旋的神奇操作,所以呢也就想顺势实现一下。
不想实现的是胜负的判定和计分系统,所以就...你懂的,留成作业。
俄罗斯方块发展到现在,规则可能已经很完善,在此基础上可能有很多变种和玩法,所以我也不保证我做出来的和主流的玩法是一致的,嘛,开始吧。
俄罗斯方块的具体实现
我们需要一个地图存放下落的方块,定时将方块下移,已经对玩家的按键进行判断。
常量定义和主函数结构
#include <stdio.h>
#include <Windows.h>
#include <conio.h>
#include <time.h>
#define WIDTH 10//x j
#define HIGTH 25//y i
//[y][x] [HIGTH][WIDTH] [i][j] [20][10]
#define TICK 1000
#define COOLDOWN 200
//0为空 1为已下落的方块 2为正在移动的方块
char map[HIGTH][WIDTH];//显示区域为0,5到10,25
ULONGLONG last;
ULONGLONG now;
int key;
int keyboard_flag;
HANDLE hdl;
虽然说是地图是10×20的,但是我这里声明了10×25的地图,意图在于生成方块时将方块生成在看不到的最上方,让其从上方逐渐出现。在代码编辑的时候可以先显示全部的地图,最后再修改显示函数,不让其显示最上的五行即可。
TICK指的是游戏的下落的时间,这里设为常量100。
COOLDOWN是指玩家如果按下一个键并且操作成功,这个方块会有一段时间不会有动作,主要表现在玩家让方块直接落下后,不希望落下的方块正好就固定在地面上,而是期望有一段时间供玩家左右移动。这里为了我自己方便调试,旋转和左右平移也加了这种冷却,不喜欢的话自行去掉即可。冷却的添加依靠的是keyboard_flag这个变量。
如何实现每隔一段时间就将方块下移呢?可以写个死循环,不断比较时间,如果时间达到就执行操作,并把下一个时间设为下一个TICK毫秒后,具体来说:
int main()
{
//游戏初始化
hdl = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cci = { 1,0 };
SetConsoleCursorInfo(hdl, &cci);
SetConsoleTitle("TETRIS!!!");
srand((unsigned)time(NULL));
memset(map, 0, WIDTH * HIGTH * sizeof(char));
//计时开始
last = GetTickCount64();
keyboard_flag = 0;
//生成第一个方块
generate();
update_screen();
while (1)//主循环
{
now = GetTickCount64();
//说明需要一个冷却
if (keyboard_flag)
{
keyboard_flag = 0;
last = GetTickCount64() + COOLDOWN;
update_screen();
}
//说明需要向下移动了
if (now > last)
{
last += TICK;
if (try_move_down() == FUNCTION_FAIL)//向下移动失败,创建一个新的形状
{
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = BLOCK;
clear_row();
generate();
}
update_screen();
}
//按键检测
while (_kbhit())
{
key = _getch();
switch (key)
{
case 'E':case 'e':
if (try_rotate(1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'Q':case 'q':
if (try_rotate(-1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'S':case 's':
if (try_move_down() == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'W':case 'w':
if (try_fall() == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'D':case 'd':
if (try_move_horizontal(1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'A':case 'a':
if (try_move_horizontal(-1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
}
}
}
return 0;
}
main函数之前的部分是初始化,其中计时开始就确定了目标时间last,一旦now大于last,就向下落,并且让last+=TICK。同理,冷却就是给last+=COOLDOWN。这里TICK和COOLDOWN自行合适设计即可,也可以设计成随着游戏进行变化的变量。
while (_kbhit())用于检测当前是否有按键按下,对应的不同事件由不同的函数进行处理,w键是一直落到下面,q和e是旋转,a和d是移动,s是向下移动一格。里面的一些函数,如生成,显示屏幕,移动,都还没有写,不过咱先写个空函数摆在那。
这里向左向右移动,顺时针逆时针旋转里面都写了1或者-1,代表不同方向的移动和旋转。
这里我定义了几个枚举来表示成功移动与否,对于地图元素和形状类型我也定义了枚举,只用在这里。
typedef enum ELEMENT
{
AIR, BLOCK, MOVING
}ELEMENT;
typedef enum FUNCTION_RESULT
{
FUNCTION_SUSSESS, FUNCTION_FAIL
}FUNCTION_RESULT;
typedef enum SHAPE_TYPE
{
O_SHAPE, J_SHAPE, L_SHAPE, T_SHAPE, I_SHAPE, S_SHAPE, Z_SHAPE
}SHAPE_TYPE;
当前正在移动的方块
如何表示当前正在移动的方块,以及判断某个按键操作是否可行呢?其实只要把坐标记下来就好了。
COORD faller[4];
COORD next_faller[4];
SHAPE_TYPE now_shape;
每次操作都考虑一下next_faller合不合理,合理的话就说明这个移动是可行的。
int is_legal(COORD test[4])//1为合法 0为不合法
{
for (int i = 0; i < 4; i++)
{
if (test[i].X < 0 || test[i].X >= WIDTH)return 0;
if (test[i].Y >= HIGTH)return 0;
}
for (int i = 0; i < 4; i++)
{
if (map[test[i].Y][test[i].X] == BLOCK)return 0;
}
return 1;
}
新方块的生成
首先我们需要知道新方块会生成在哪,这个参照一下最上面的七种俄罗斯方块的图,然后我们把这些初始坐标写出来:
//形状的初始位置,分别表示xy坐标,顺序是ojltisz
short o_shape[8] = { 4,3,5,3,4,4,5,4 };
short j_shape[8] = { 4,4,5,4,6,4,4,3 };
short l_shape[8] = { 5,4,4,4,3,4,5,3 };
short t_shape[8] = { 5,4,4,4,6,4,5,3 };
short i_shape[8] = { 4,4,5,4,3,4,6,4 };
short s_shape[8] = { 4,3,5,3,4,2,5,4 };
short z_shape[8] = { 4,3,5,3,5,2,4,4 };
写成一个二位数组,看起来不好看了,调用起来更好调了。
short shape[7][8] =
{
{ 4,3,5,3,4,4,5,4 },
{ 4,4,5,4,6,4,4,3 },
{ 5,4,4,4,3,4,5,3 },
{ 5,4,4,4,6,4,5,3 },
{ 4,4,5,4,3,4,6,4 },
{ 4,3,5,3,4,2,5,4 },
{ 4,3,5,3,5,2,4,4 },
};
接下来是生成和显示屏幕,由于比较简单,我这里就不写了。我在最后会附全部的源码,去下面找相应的函数吧。
这里屏幕是从i=5开始显示的,一开始调试可以把全部25行都显示出来,看得清楚。这里没有方块用一个点表示,也是为了看得清楚。
向下移动、水平移动、清一行
要点是根据faller,算出next_faller,然后判断是否合理,合理的话就把faller再赋成next_faller。多行的移动事实上只是重复单行的移动。
清一行的话,应当从下向上判断,是否有哪行被填满了,如果这样的话,就拿上面的全部向下覆盖。不过注意有时会同时清多行,这样的话,就需要在一行判断清除之后再对那一行进行一次判断。
旋转
旋转比较复杂,不过还是可以这样理解。
旋转必须有一个旋转中心,对O型来说,旋转没有意义,所以遇到O型直接返回就可以了。
T型的旋转中心,应当是那个丁字口处,不过我们之前说到有T旋需要实现,这里就孤立它,单独写个函数好了。
除此之外咱们可以给每个形状指定一个中心点,例如L和J的拐角处,其他点都是相对于这个点进行旋转的。
以顺时针为例,中心点是不会改变位置的,而其他的三个点,如果原先在中心点的上方,现在就会在右方,也就是原先Y坐标小多少,现在X坐标就会大多少;原先X坐标大多少,现在Y坐标就大多少。而逆时针旋转恰恰方向相反,只需要在距离上乘-1即可。
但是这样做有点弊端,那就是有时方块不会在边上旋转。
注意到这个I型,我们希望它能在按下旋转后变成横的,但是因为以中心点旋转后,部分方块就会出界,所以被认为是非法的。所以我们也需要以其他点为旋转中心进行旋转,但是一定要注意优先级,不能让玩家通过旋转,使得方块越来越高了。
T旋规定了这种从蓝色到红色的旋转是合法的,仔细一看是以蓝色最右的方块为旋转中心进行旋转,接着再向下移动一格。所以旋转后向下移动一格也是可以使用的,而且T旋要求这种旋转的优先级要大于以其他点作为旋转中心的旋转,不然蓝色方块会以最左边的方块进行逆时针旋转,从而竖起来。
综上,我们得到了八种旋转方式,分别是以四个方块作为旋转中心,和旋转后再向下移动一格。这里的优先级需要设计合理。我对俄罗斯方块没有太深入的了解,这里按照自己的想法设计了优先级,如果发现和主流的优先级不同的话,自行修改即可。这里有关T旋的实现有些冗余,总之这块就自行参考吧。
展示一下吧
全部代码
#include <stdio.h>
#include <Windows.h>
#include <conio.h>
#include <time.h>
#define WIDTH 10//x j
#define HIGTH 25//y i
//[y][x] [HIGTH][WIDTH] [i][j] [20][10]
#define TICK 1000
#define COOLDOWN 200
typedef enum ELEMENT
{
AIR, BLOCK, MOVING
}ELEMENT;
typedef enum FUNCTION_RESULT
{
FUNCTION_SUSSESS, FUNCTION_FAIL
}FUNCTION_RESULT;
typedef enum SHAPE_TYPE
{
O_SHAPE, J_SHAPE, L_SHAPE, T_SHAPE, I_SHAPE, S_SHAPE, Z_SHAPE
}SHAPE_TYPE;
char map[HIGTH][WIDTH];//显示区域为0,5到10,25 0为空 1为已下落的方块 2为正在移动的方块
COORD faller[4];
COORD next_faller[4];
ULONGLONG last;
ULONGLONG now;
int key;
SHAPE_TYPE now_shape;
int keyboard_flag;
HANDLE hdl;
//形状的初始位置,分别表示xy坐标,顺序是ojltisz
short o_shape[8] = { 4,3,5,3,4,4,5,4 };
short j_shape[8] = { 4,4,5,4,6,4,4,3 };
short l_shape[8] = { 5,4,4,4,3,4,5,3 };
short t_shape[8] = { 5,4,4,4,6,4,5,3 };
short i_shape[8] = { 4,4,5,4,3,4,6,4 };
short s_shape[8] = { 4,3,5,3,4,2,5,4 };
short z_shape[8] = { 4,3,5,3,5,2,4,4 };
short shape[7][8] =
{
{ 4,3,5,3,4,4,5,4 },
{ 4,4,5,4,6,4,4,3 },
{ 5,4,4,4,3,4,5,3 },
{ 5,4,4,4,6,4,5,3 },
{ 4,4,5,4,3,4,6,4 },
{ 4,3,5,3,4,2,5,4 },
{ 4,3,5,3,5,2,4,4 },
};
int is_legal(COORD test[4])//1为合法 0为不合法
{
for (int i = 0; i < 4; i++)
{
if (test[i].X < 0 || test[i].X >= WIDTH)return 0;
if (test[i].Y >= HIGTH)return 0;
}
for (int i = 0; i < 4; i++)
{
if (map[test[i].Y][test[i].X] == BLOCK)return 0;
}
return 1;
}
void update_screen()
{
//system("CLS");
COORD pos_start= { 0,0 };
SetConsoleCursorPosition(hdl, pos_start);
for (int i = 5; i < HIGTH; i++)
{
printf("<|");
for (int j = 0; j < WIDTH; j++)
{
if (map[i][j] == AIR) printf(". ");
else printf("■");
}
printf("|>\n");
}
printf("<---------------------->");
}
void generate()
{
now_shape = rand() % 7;
memcpy(&faller, shape[now_shape], 4 * sizeof(COORD));
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = MOVING;
}
FUNCTION_RESULT try_move_down()
{
memcpy(next_faller, faller, sizeof(faller));
for (int i = 0; i < 4; i++)
{
next_faller[i].Y++;
if (next_faller[i].Y >= HIGTH)return FUNCTION_FAIL;
if (map[next_faller[i].Y][next_faller[i].X] == BLOCK)return FUNCTION_FAIL;
}
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = AIR;
memcpy(faller, next_faller, sizeof(faller));
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = MOVING;
return FUNCTION_SUSSESS;
}
void clear_row()
{
COORD temp_map[HIGTH][WIDTH];
int flag;
for (int i = HIGTH - 1; i >= 5; i--)
{
flag = 1;
for (int j = 0; j < WIDTH; j++)
{
if (map[i][j] != BLOCK)
{
flag = 0;
break;
}
}
if (flag)
{
memcpy(temp_map, map, i * sizeof(map[0]));
memcpy(map + 1, temp_map, i * sizeof(map[0]));
continue;
}
}
}
//向右输入1,向左输入-1
FUNCTION_RESULT try_move_horizontal(int direction)
{
memcpy(next_faller, faller, sizeof(faller));
for (int i = 0; i < 4; i++)
{
next_faller[i].X+=direction;
if (next_faller[i].X >= WIDTH || next_faller[i].X < 0) return FUNCTION_FAIL;
if (map[next_faller[i].Y][next_faller[i].X] == BLOCK)return FUNCTION_FAIL;
}
keyboard_flag = 1;
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = AIR;
memcpy(faller, next_faller, sizeof(faller));
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = MOVING;
return FUNCTION_SUSSESS;
}
FUNCTION_RESULT try_fall()
{
FUNCTION_RESULT result = FUNCTION_FAIL;
while (try_move_down() == FUNCTION_SUSSESS)result = FUNCTION_SUSSESS;
return result;
}
//以中心点旋转
//以所有点旋转后向下
//以其他点旋转
FUNCTION_RESULT t_spin(int direction)
{
for (int j = 0; j < 4; j++)
{
next_faller[j].X = faller[0].X + direction * (faller[0].Y - faller[j].Y);
next_faller[j].Y = faller[0].Y + direction * (faller[j].X - faller[0].X);
}
if (is_legal(next_faller))
{
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = AIR;
memcpy(faller, next_faller, sizeof(faller));
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = MOVING;
return FUNCTION_SUSSESS;
}
for (int round = 1; round >= 0; round--)
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
next_faller[j].X = faller[i].X + direction * (faller[i].Y - faller[j].Y);
next_faller[j].Y = faller[i].Y + direction * (faller[j].X - faller[i].X) + round;
}
if (is_legal(next_faller))
{
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = AIR;
memcpy(faller, next_faller, sizeof(faller));
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = MOVING;
return FUNCTION_SUSSESS;
}
}
}
return FUNCTION_FAIL;
}
//以中心点旋转
//以其他点旋转
//检查以中心点旋转后能否向下移动一格
//检查以其他点旋转后能否向下移动一格
FUNCTION_RESULT try_rotate(int direction)
{
if (now_shape == O_SHAPE)return FUNCTION_FAIL;
if (now_shape == T_SHAPE)return t_spin(direction);
for (int round = 0; round <= 1; round++)
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
next_faller[j].X = faller[i].X + direction * (faller[i].Y - faller[j].Y);
next_faller[j].Y = faller[i].Y + direction * (faller[j].X - faller[i].X) + round;
}
if (is_legal(next_faller))
{
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = AIR;
memcpy(faller, next_faller, sizeof(faller));
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = MOVING;
return FUNCTION_SUSSESS;
}
}
}
return FUNCTION_FAIL;
}
int main()
{
//清除光标
hdl = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cci = { 1,0 };
SetConsoleCursorInfo(hdl, &cci);
SetConsoleTitle("TETRIS!!!");
//随机数种子
srand((unsigned)time(NULL));
//地图初始化
memset(map, 0, WIDTH * HIGTH * sizeof(char));
//计时开始
last = GetTickCount64();
keyboard_flag = 0;
//生成第一个方块
generate();
update_screen();
while (1)//主循环
{
now = GetTickCount64();
//说明该向下移动了
if (keyboard_flag)
{
keyboard_flag = 0;
last = GetTickCount64() + COOLDOWN;
update_screen();
}
if (now > last)
{
last += TICK;
if (try_move_down() == FUNCTION_FAIL)//向下移动失败,创建一个新的形状
{
for (int i = 0; i < 4; i++) map[faller[i].Y][faller[i].X] = BLOCK;
clear_row();
generate();
}
update_screen();
}
while (_kbhit())
{
key = _getch();
switch (key)
{
case 'E':case 'e':
if (try_rotate(1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'Q':case 'q':
if (try_rotate(-1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'S':case 's':
if (try_move_down() == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'W':case 'w':
if (try_fall() == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'D':case 'd':
if (try_move_horizontal(1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
case 'A':case 'a':
if (try_move_horizontal(-1) == FUNCTION_SUSSESS)keyboard_flag = 1;
break;
}
}
}
return 0;
}