这个俄罗斯方块是用c++基于windows控制台制作的。
源码地址:https://github.com/Guozhi-explore
话不多说,先上图感受一下:(控制台丑陋的界面不是我的锅emmm)
我们知道俄罗斯方块有俄罗斯方块的规则:
1.生成五种不同形状的方块 :L、Z、T、I和方形。
2.通过键盘控制左移、右移、旋转与直接下落。
3.遇到边界或者静止方块停止运动。
4.一行都有方块时自动消去,分数增加,上层方块下落。
5.如果顶部出现了堆积方块时游戏结束。
6.小框提前放映出待会出现的方块形状。
7.等级升高,速度增大。
现在就来讲一讲如和在这些规则都实现的前提下制作俄罗斯方块的游戏,并尽可能的提高用户的游戏体验。
先来总体的规划一番,我们需要设计的类有:
单元类:Unit; 负责存储每个小单元的位置(x,y)以便于在控制台上打印出来。
工具类:Tool; 借助于windows提供的API,移动到特定的位置,打印图案或消除图案。
界面类:Frame; 打印出游戏运行时的外围框架和游戏信息。
游戏逻辑类: Diamond(名字与游戏不是很相关orz); 最长和最复杂的一个类,所有的游戏逻辑都将在这里实现。
主程序类: main.cpp;
一:
为了阅读的方便,我把以后要经常用到的Unit、Tool类先介绍一二:
我们知道windows控制台默认的大小是80*40,也就是说我们游戏要打印出来的所有内容的位置坐标必须都在0<x<80,y<x<40这个范围内。
那怎么样才能控制打印的位置呢?
我借助windows提供的函数自己在Tool类里面封装了一个函数:
void Tool::gotoxy(int x, int y)
{
COORD coor;
coor.X = x;
coor.Y = y;
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, coor);
}
通过windows里的SetConsoleCursorPosition函数,能将光标带到我们指定的控制台坐标(x,y)。
很显然,使用了gotoxy函数我们就能够在控制台上将光标带到各个位置,绘制出我们想要的任何形状。
那么谁来提供这个坐标呢?
答案就是Unit单元类了。
Unit.h
1 #pragma once
2 #include"Tool.h"
3 #include"Frame.h"
4 class Unit
5 {
6 private: int pos_x, pos_y;
7 char pic='#';
8 public:
9 Unit(int x,int y);
10 ~Unit();
11 void show();
12 int get_x();
13 int get_y();
14 void set_x(int x);
15 void set_y(int y);
16 };
Unit.cpp
1 #include "Unit.h"
2 Unit::Unit(int x,int y)
3 {
4 pos_x = x;
5 pos_y = y;
6 }
7 Unit::~Unit()
8 {
9 }
10 void Unit::show()
11 {
12 Tool T;
13 T.gotoxy(pos_x, pos_y);
14 cout << pic; //俄罗斯方块由‘#’号组成
15 return;
16 }
17 int Unit::get_x()
18 {
19 return pos_x;
20 }
21 int Unit::get_y()
22 {
23 return pos_y;
24 }
25 void Unit::set_x(int x)
26 {
27 pos_x = x;
28 }
29 void Unit::set_y(int y)
30 {
31 pos_y = y;
32 }
说完了这两个基础类,我们就来进一步的介绍Frame类。
二:
Frame类要打印出来的信息如图:
Frame类不仅要把东西打印出来,它还负责着区域的划定,在Frame类里面有一些const参数:
1 const int UP = 5;
2 const int BUTTOM = 20;
3 const int LEFT = 18 ;
4 const int RIGHT = 30;
5 const int RIGHT2 = 46;
6 const int RIGHT3 = 63;
7 const int MIDDLE1 = 12;
8 const int MIDDLE2 = 17;
这些参数要放在public里面,以便在Diamond类中实现游戏区域、小屏幕、得分区三个区域各自的任务时能够访问的到。
三:
Diamond类
这是最关键的一个类,实现出来光是.cpp中就有300多行代码。
在Diamond类的主战场,游戏区域我设置了一个grid二维数组,用来保存游戏区域的方块信息,一个arrays数组用来保持当前运动的四个方块的信息,一个speed数组用来设置速度,LEVEL是最高级别,speed是当前速度(speed是每次Sleep()函数的时长,speed越小方块运动越快),level是当前级别,scoretotal是当前得分。
Diamond.h代码:
1 #pragma once
2 #include"Frame.h"
3 #include<vector>
4 #include"Unit.h"
5 #include<ctime>
6 #include<cstdlib>
7 class Diamond
8 {
9 private:
10 static const int LEVEL = 15;
11 bool grid[17][19];
12 vector<Unit> arrays;
13 int Speed[LEVEL];
14 int speed=700;
15 int level = 0, scoretotal = 0;
16 public:
17 Diamond();
18 ~Diamond();
19 void create();
20 void rotate();
21 void show();
22 void move();
23 void shift();
24 void set_speed(int m_speed);
25 void control();
26 int get_level();
28 int eliminate();
29 bool judge_death();
30 bool delay();
31 };
Diamond构造函数:
1 Diamond::Diamond()
2 {
3 Frame F;
4 grid[F.RIGHT - F.LEFT - 1][F.BUTTOM - F.UP - 1]; //有方块是true,没有是false
5 for (int i = 0; i <= F.RIGHT - F.LEFT - 2; ++i)
6 {
7 for (int j = 0; j <= F.BUTTOM - F.UP - 2; ++j)
8 {
9 grid[i][j] = false;
10 }
11 }
12 for (int i = 0; i < 13; ++i)
13 {
14 Speed[i] = 700 - 30 * i;
15 }
16 for (int i = 13; i < LEVEL; ++i)
17 {
18 Speed[i] = 300; //制作速度等级数组
19 }
20 F.Draw_score(0, 0);
21 }
前面讲到了俄罗斯方块的一些规则,第一个就是生成五种不同的方块,分别是Z\T\L\I\方形。
它们之间虽然形状各异,但好在有一个共同的地方,它们都是由四个基础方块(也就是四个Unit)组成的。
不妨用一个数组arrays来实现这五种方块。
arrays是由Unit组成的数组,通过给数组里unit单元不同的x、y值得到不同的形状(unit【0】位于游戏区域的正上方)。
借助于随机函数我们可以每次出现不同的方块(方块出现的概率有差别时,游戏体验更佳),然后使用旋转函数rotate(之后会实现)得到更为丰富的方块形状。
arrays得到之后,我们就要打印了。(由于unit【0】作为基准点在游戏区域的正上方,不用考虑方块旋转之后碰到两侧框架的问题)
打印有两个地方:1.游戏区域,2.小屏幕。
打印完之后,需要在grid数组表示arrays里面各个单元的元素设置为true(此时已经有方块)。
代码贴出如下:
1 void Diamond::create()
2 {
3 Frame F;
4 Tool T;
5 int key;
6 int base_point;
7 int rotate_time=0;
8 srand(time(NULL));
9 base_point = ( F.RIGHT-F.LEFT) / 2;
10 key = rand() % 5; //随机出形状
11 rotate_time = rand() % 9; //随机旋转原始形状,已达到丰富的目的
12 arrays.clear(); //为了降低难度,每个图形出现的比例应该不一样
13 switch(key)
14 {
15 case 0:
16 arrays.clear();
17 for (int i = 0; i < 4; ++i)
18 {
19 Unit unit(i + F.LEFT+base_point+1, F.UP + 1); //****型
20 arrays.push_back(unit);
21 }
22 for (int i = 0; i < rotate_time; ++i)
23 {
24 rotate();
25 }
26 break;
27 case 1:case 5:case 6:
28 arrays.clear();
29 for (int i = 0; i < 2; ++i)
30 {
31 for (int j = 0; j < 2; ++j)
32 {
33 Unit unit(i + F.LEFT + base_point + 1, F.UP + 1 + j); //正方形
34 arrays.push_back(unit);
35 }
36 }
37 break;
38 case 2:case 7: //枪形
39 arrays.clear();
40 arrays.push_back(Unit(F.LEFT + base_point + 1, F.UP + 1));
41 arrays.push_back(Unit (F.LEFT + base_point+2, F.UP + 1));
42 arrays.push_back(Unit (F.LEFT + base_point + 3, F.UP + 1));
43 arrays.push_back(Unit(F.LEFT + base_point + 3, F.UP + 2));
44 for (int i = 0; i < rotate_time; ++i)
45 {
46 rotate();
47 }
48 break;
49 case 3: //梯形
50 arrays.clear();
51 arrays.push_back(Unit(F.LEFT + base_point + 1, F.UP + 1));
52 arrays.push_back(Unit(F.LEFT + base_point+1, F.UP + 2));
53 arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 2));
54 arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 3));
55 for (int i = 0; i < rotate_time; ++i)
56 {
57 rotate();
58 }
59 break;
60 case 4: case 8:
61 arrays.clear(); //城墙形
62 arrays.push_back(Unit(F.LEFT + base_point + 1, F.UP + 1));
63 arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 1));
64 arrays.push_back(Unit(F.LEFT + base_point + 3, F.UP + 1));
65 arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 2));
66 for (int i = 0; i < rotate_time; ++i)
67 {
68 rotate();
69 }
70 break;
71 }
72 //应该先将小屏幕清空。
73 for (int i = 0; i < F.RIGHT2 - F.RIGHT - 1; ++i)
74 {
75 for (int j = 0; j < F.MIDDLE1 - F.UP - 1; ++j)
76 {
77 T.gotoxy(F.RIGHT + 1 + i, F.UP + j + 1);
78 cout << " ";
79 }
80 }
81 for (int i = 0; i < 4; ++i)
82 {
83 Unit unit(F.RIGHT + 7 + arrays[i].get_x() - arrays[0].get_x(), F.UP + 4 + arrays[i].get_y() - arrays[0].get_y());
84 unit.show(); //按照实际的俄罗斯方块,小屏幕放映的应该是下一个出现的方块形状,这里先将功能简单化
85 }
86 for (int i = 0; i < 4; ++i)
87 {
88 grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-1-F.UP] = true;
89 }
90 }
特别注意,在小屏幕打印之前应将其清空。
rotate()旋转函数:
在create()函数里用到了rotate()函数,这里来将其实现。
在开始制作旋转函数的时候,我想这个应该是特别难的,因为有5种形状,而且旋转完之后可能会触碰到两侧框架和静止方块。
但是其实只要用上一点数学知识和技巧,便可以将这些问题轻松解决。
因为旋转(我的游戏中是逆时针转90度)其实就是arrays后三个单元对arrays[0]这个基准点来一个相对x、y的互换(看代码就明白了)。
1 void Diamond::rotate()
2 {
3 Frame F;
4 for (int i = 0; i < 4; ++i)
5 {
6 grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-F.UP-1] = false;
7 }
8 for (int i = 1; i <= 3; ++i)
9 {
10 int gap1 = arrays[i].get_x() - arrays[0].get_x();
11 int gap2 = arrays[i].get_y() - arrays[0].get_y();
12 arrays[i].set_x(arrays[0].get_x() - gap2);
13 arrays[i].set_y(arrays[0].get_y() + gap1);
14 }
15 return;
16 }
就是这么简单,只用了四行,就完成了五种形状方块的旋转。
注意到这里先将grid里面arrays每个单元所代表的元素都设置为了false(有方块是true,没有是false),而且在旋转完之后没有设置回true。这是因为我们的rotate()函数不是独立存在的,都只是被其他的函数引用。注意到旋转分为生成时旋转和过程中旋转。生成时我们会统一的设置true,而过程中会在shift函数设置(这样能避开相撞问题)。如果有疑问接着看下去就好了。
说完旋转,接着说下落吧。
move()函数
这个函数很简单,直接贴代码:
1 void Diamond::move()
2 {
3 Frame F;
4 //Sleep(speed); 暂停会在control函数里实现
5 for (int i = 0; i < 4; ++i)
6 {
7 grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-1-F.UP] = false;
8 }
9 for (int i = 0; i < 4; ++i)
10 {
11 arrays[i].set_y(arrays[i].get_y()+1);
12 }
13 for (int i = 0; i < 4; ++i)
14 {
15 grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-F.UP-1] = true;
16 } //虽说move后面是shift函数,但是为了保持函数的独立性还是在每个函数里面都写上false、true这部分
17 return;
18 }
这里的move设置回了true,因为考虑到move是和shift()函数并列的,而rotate()实在shift()函数里面的。
说曹操,曹操到,shift()函数到了。
这个函数中我们要实现一个很重要的功能——用户操控。
A、a——左移,D、d——右移,R、r——旋转,D、d——速降。
_kbhit()是windows提供的函数,作用是检测到键盘输入。用一个while()持续检测后,在switch语句里判断输入目的。
左移,右移,旋转都有一个问题,如果新的位置与两侧框架、静止方块重合,那么这个运动应当作废,还是得回到原来的位置,并且这次运动不能呈现给用户(不然游戏体验太差了emmm)在这个函数中会将这个问题考虑解决掉。
速降功能实现很简单,只需要把用set_speed()函数设置speed为10(这种速度很快)就好了。
代码贴出来就都明白了(hhh)
1 void Diamond::shift()
2 {
3 Frame F;
4 vector<Unit> array_wait;
5 //防止与两侧方块重合
6 bool flag = false;
7 for (int i = 0; i < 4; ++i)
8 {
9 array_wait.push_back(arrays[i]); //记录下arrays的原始位置
10 grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-F.UP-1] = false;
11 }
12 while(_kbhit())
13 {
14 switch (_getch())
15 {
16 case 97:case 65:case VK_LEFT: //左移
17 for (int i = 0; i < 4; ++i)
18 {
19 int m_x = arrays[i].get_x();
20 arrays[i].set_x(m_x - 1);
21 }
22 break;
23 case 100:case 68: case VK_RIGHT: //右移
24 for (int i = 0; i < 4; ++i)
25 {
26 int m_x = arrays[i].get_x();
27 arrays[i].set_x(m_x + 1);
28 }
29 break;
30 case 82:case 114: //输入字符R(rotate)的大写或者小写,旋转
31 rotate();
32 break;
33 case 83:case 115:
34 set_speed(10); //实现速降功能 这个功能做得简单又有用户体验!!!
35 default:
36 break;
37 }
38 }
39 for (int i = 0; i < 4; ++i)
40 {
41 if (grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y() - 1 - F.UP] == true)
42 flag = true;
43 if (arrays[i].get_x() <= F.LEFT || arrays[i].get_x() >= F.RIGHT)
44 flag = true;
45 }
46 if (flag == true) //如果相撞,则回到原始位置
47 {
48 arrays.clear();
49 for (int i = 0; i < 4; ++i)
50 {
51 arrays.push_back(array_wait[i]);
52 }
53 }
54 for (int i = 0; i < 4; ++i)
55 {
56 grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-1-F.UP] = true;
57 }
58 return;
59 }
为了避免与框架或者静止方块相撞,设计了一个arrays_wait数组记录原来位置。
由于arrays各个单元在grid里面的投射点已经全部变成了false,这个时候一旦新的arrays有true,肯定是遇到了静止的方块,那么这次运动应该恢复。
讲了这么多移动,我们还没有把方块移动后的方块和静止方块一块打印在游戏区呢,这就要看show()了。
原理很简单,扫视整个grid数组,true打印#,false打印空格(为了将刚刚为true,现在为false的点恢复)。
1 void Diamond::show()
2 {
3 Frame F;
4 Tool T;
5 for (int i = F.LEFT+1; i < F.RIGHT; ++i)
6 {
7 for (int j = F.UP + 1; j < F.BUTTOM; ++j)
8 {
9 if (grid[i - F.LEFT - 1][j - F.UP - 1] == true)
10 {
11 T.gotoxy(i, j);
12 cout << "#";
13 }
14 else
15 {
16 T.gotoxy(i, j);
17 cout << " ";
18 }
19 }
20 }
21 return;
22 }
好的移动的函数讲的差不多了,可以上一些检测的函数了。
检测函数共三个:
delay()滞留函数——如果arrays各个方块下面有静止方块,那么返回true。
eliminate()消除函数——如果有一行被方块填满了,消除该行,上面的方块下落一格,继续检测,返回整形,用来得到分数。
judge_death()生死函数——如果静止的方块堆积到了最上面一格,那么返回true,此时该轮游戏将结束。
我们来一个一个实现:
delay()基本与刚刚shift里面的方法一致,先设置为false,检测,再设置为true。
1 bool Diamond::delay()
2 {
3 bool flag = false;
4 Frame F;
5 for (int i = 0; i < 4; ++i)
6 {
7 grid[arrays[i].get_x() - F.LEFT - 1][arrays[i].get_y() - F.UP - 1] = false;
8 }
9 for (int i = 0; i < 4; ++i)
10 {
11 if (arrays[i].get_y() == F.BUTTOM - 1)
12 flag=true;
13 if (grid[arrays[i].get_x()-F.LEFT-1][arrays[i].get_y() + 1-F.UP-1] == true)
14 flag = true;
15 }
16 for (int i = 0; i < 4; ++i)
17 {
18 grid[arrays[i].get_x() - F.LEFT - 1][arrays[i].get_y() - F.UP - 1] = true;
19 }
20 return flag;
21 }
eliminate():
从下往上一行行检测。
1 int Diamond::eliminate()
2 {
3 Frame F;
4 int level = 0;
5 int i;
6 for ( i = F.BUTTOM - F.UP - 2; i >= 0; --i)
7 {
8 bool temp = true;
9 for (int j = 0; j < F.RIGHT - F.LEFT - 1; ++j)
10 {
11 if (grid[j][i] == false)
12 temp = false;
13 }
14 if (temp == true)
15 {
16 level++;
17 for (int k = 0; k < F.RIGHT - F.LEFT - 1; ++k)
18 {
19 for (int t = i; t > 0; --t)
20 {
21 grid[k][t] = grid[k][t - 1];
22 }
23 }
24 for (int k = 0; k < F.RIGHT - F.LEFT - 1; ++k)
25 {
26 grid[k][0] = false;
27 }
28 i++; //检测刚刚替补的一行
29 }
30
31 temp = true;
32 }
33 return level;
34 }
judge_death()函数:
很简单,看代码。
1 bool Diamond::judge_death()
2 {
3 Frame F;
4 int temp = 0;
5 for (int i = 0; i < F.RIGHT - F.LEFT - 1; ++i)
6 {
7 if (grid[i][0] == true)
8 return true;
9 }
10 return false;
11 }
control函数。
contorl函数是很关键的函数,在它里面将之前的函数依次调用才能实现俄罗斯方块的功能。这个顺序也不能随意安排,必须根据程序的逻辑和各个函数的完善程度。
例如倘若我将create()函数放在了judge_death()之前,中间又没有delay()函数,那么judge_death()很可能因为arrays里面的运动方块投射在grid里面是true而返回true,那么游戏就结束了,与实际规则完全不同,因此这一个函数要经过多次调试才能写出最佳效果。
同时这个函数还包含了一些总体布局功能,如分数、等级的计算和打印,速度的设置。
代码如下:
1 void Diamond::control()
2 {
3 Frame F;
4 Tool T;
5 while (judge_death()==false)
6 {
7 create();
8 show();
9 int score = 0;
10 while(!delay()) //判断下面是否有方块,从而停止此次运动
11 {
12 shift();
13 move();
14 show();
15 Sleep(speed);
16 }
17 score = eliminate();
18 scoretotal += score;
19 level = scoretotal / 4;
20 if (level == LEVEL)
21 {
22 break;
23 }
24 speed = Speed[level]; //根据等级改变速度,同时关闭速降功能
25 F.Draw_score(scoretotal, level+1);
26 Sleep(speed);
27 }
28 return;
29 }
至此300多行的Diamond类讲的差不多了,如果要看整体代码可以进入文章末尾的Github账号。
四
main.cpp
这里的设置就很简单了,用一个while循环是游戏实现再来一局的功能。
1 #include<iostream>
2 using namespace std;
3 #include"Tool.h"
4 #include"Frame.h"
5 #include"Unit.h"
6 #include"Diamond.h"
7 /* coded by 郭志
8 2018/9/18 */
9 int main()
10 {
11 Tool T;
12 Frame F;
13 char chioce;
14 bool flag=true;
15 F.Draw_border();
16 F.Draw_message("郭志");
17
18 while (flag)
19 {
20 Diamond diamond;
21 diamond.control();
22 if (diamond.get_level() < 50)
23 {
24 T.gotoxy(0, 0);
25 cout << "你这盘达到的等级是" << diamond.get_level() << endl;
26 cout << "是否继续? 是(y)" << endl;
27 cin >> chioce;
28 if (chioce == 'y')
29 {
30 flag = true;
31 }
32 else flag = false;
33 }
34 }
35 return 0;
36 }
好的,俄罗斯方块的实现基本就是这些了,会了算法和游戏逻辑,虽然现在的界面比较丑陋,但是相信以后也能做出一些好看、好玩的俄罗斯方块。
源代码地址 https://github.com/Guozhi-explore