这个作业属于哪个课程 | 2020年面向对象程序设计 |
---|---|
这个作业要求在哪里 | 面向对象程序设计寒假作业2 |
这个作业的目标 | 做出能运行的我罗斯方块、分享代码要点、收获与心得 |
小组成员 | 031902207-黄新成 |
github代码 | https://github.com/ying-hua/MyGame/tree/master |
一、游戏截图
二、代码要点
我在代码中写了很多注释,在github看会更清楚一点
把一些重要的代码写在这里
方块类
方块的表示
我开始想用一个二维数组来表示方块的形状,数值为1表示有方块,数值为0表示无方块
但这样要定义很多变量名不同的数组,使用时不方便
最后决定用三维数组来表示,第一维表示方块的形状
方块类中只需要一个属性shape就可以知道是什么方块了
下面这张图是方块的编号
//只选择了部分代码做示例
static int stdblock[30][4][4] = { //标准方块
{ {1,1,1,1},{1,1,1,1 },{1,1,1,1},{1,1,1,1} },//0//无
{ {0,0,0,0},{0,0,0,0 },{1,1,1,1},{0,0,0,0} },//1
{ {0,0,1,0},{0,0,1,0 },{0,0,1,0},{0,0,1,0} },//2
{ {0,0,0,0},{0,0,0,0 },{0,0,0,0},{0,0,0,0} },//3//无
{ {0,0,0,0},{0,0,0,0 },{0,0,0,0},{0,0,0,0} },//4//无
};
随机生成方块
随机生成方块函数我用了系统自带的rand()函数
每一种方块对应一种颜色
先从七种方块中随机选择一种,再随机选择这种方块的形态,这样每种方块产生的概率相同
//srand()函数在别的地方
//方块的颜色事先用了宏定义
void Block::roundBlock() {
int random;//随机数
random = rand() % 7;//产生0-6的随机数,随机一种方块
random = random * 4 + 1;
switch (random) {//随机方块形状和颜色
case 1:color = GREEN | INTENSITY; random += rand() % 2; break;//亮绿
case 5:color = RED | INTENSITY;break;//红
case 9:color = RED | BLUE | INTENSITY; random += rand() % 4; break;//品红
case 13:color = RED | GREEN | INTENSITY; random += rand() % 4; break;//黄
case 17:color = BLUE | GREEN | INTENSITY; random += rand() % 4; break;//青
case 21:color = BLUE | INTENSITY; random += rand() % 2; break;//蓝
case 25:color = GREEN; random += rand() % 2; break;//绿
default:break;
}
shape = random;
}
渲染类
class Render {
public:
static void initialPrint1(HANDLE hOut,string name); //初始化单人界面
static void initialPrint2(HANDLE hOut,string name1,string name2);//初始化双人界面
static void gotoXY(HANDLE hOut, int x, int y); //移动光标
static void printBlock(HANDLE hOut, int block[4][4],int x,int y,int color);//打印方块
static void clearBlock(HANDLE hOut, int block[4][4],int x,int y);//消除方块
static void printScore(HANDLE hOut, int score, int x, int y);//更新分数
static void printMap(HANDLE hOut, Player player);//打印玩家地图
static void printWin(HANDLE hOut, string name);//双人模式玩家获胜画面
};
打印方块函数
/*===========================
打印方块
传入需要打印的方块数组、坐标和颜色
在指定位置打印方块
方块超出屏幕上边的部分不会打印
方块的坐标不是在玩家地图上的坐标,而是在屏幕上的坐标
=============================*/
void Render::printBlock(HANDLE hOut, int block[4][4], int x,int y,int color) {
SetConsoleTextAttribute(hOut, color); //设置颜色
for (int i = 0; i < 4; i++) { //开始打印方块
if (y + i < 0) //忽略超出地图上边的部分
continue;
for (int j = 0; j < 4; j++) {
if (block[i][j] == 1) {
Render::gotoXY(hOut, x + 2 * j, y + i); //移动光标打印方块
cout << "■";
}
}
}
}
打印玩家地图
为了区分双人模式两个玩家地图的位置
在玩家类定义了一个mapX属性
光标移动时加上该玩家的mapX就能移动到指定位置
/*===========================
打印玩家地图
打印指定玩家的地图
双人模式中两玩家地图的位置不同,用mapX的值区分
=============================*/
void Render::printMap(HANDLE hOut, Player player) {
SetConsoleTextAttribute(hOut, RED | GREEN | BLUE | INTENSITY); //设置颜色为亮白色
for (int i = 1; i <= 20; i++) { //开始打印地图
for (int j = 1; j <= 10; j++) {
if (player.map[i][j] == 1) { //有方块的地方
gotoXY(hOut, 2 * j + player.mapX, i - 1);
cout << "■";
}
else { //无方块的地方
gotoXY(hOut, 2 * j + player.mapX, i - 1);
cout << " ";
}
}
}
}
玩家类
由于不是每个方块都需要变形,只有玩家正在下落的方块可以变形
为了方便,我把方块移动和变形的函数都放在了玩家类
这样可能导致玩家类的函数太多
class Player {
public:
Player();//构造函数
void setName(string n);//设置玩家姓名
void setScore(int s);//设置分数
void setMapX(int x);//设置地图位置,玩家1为0,玩家2为50
string getName();//获取玩家名字
int getScore();//获取玩家分数
int eliminateRow(HANDLE hOut);//判断并消行,加分,返回消去的行数
void addRow(int x);//增加随机的x行
bool collisionDetection(Block block);//检测方块是否卡墙或超出地图
void goLeft(HANDLE hOut);//下落中的方块左移
void goRight(HANDLE hOut);//下落中的方块右移右移
void transform(HANDLE hOut);//下落中的方块变形
void goDown(HANDLE hOut,Player &opponent);//加速下落,双人模式使用
void goDown(HANDLE hOut);//加速下落,单人模式使用
void buildMap(int x,int y,int shape);//将正在下落的方块固定在地图中
private:
string name;//玩家名字
int score;//玩家分数
int map[25][15];//玩家当前的地图,从(1,1)到(20,10) 数值为1表示有方块,0表示没方块
int mapX;//地图位置
Block nextBlock;//下一个方块
Block nowBlock;//正在下落的方块
friend class Block;//这三个类为友元类
friend class Render;
friend class Game;
};
接下来是一些比较重要的函数
加行函数
增加的一行要有随机性,且不能是满行
于是我想先随机这一行小方块的个数
再随机它们的位置
/*===========================
加行
仅在双人模式使用
某玩家消行时,对手便在底部增加一行
增加的一行不可能是满行,其他情况都有可能
传入的参数x为增加的行数
=============================*/
void Player::addRow(int x) {
int n, pos[15] = { 0 }; //pos为方块的位置,n为方块个数
while(x--){
n = rand() % 10;//随机方块个数 0~9
for (int i = 1; i <= n; i++) { //生成不同的n个位置
pos[i] = rand() % 10 + 1; //pos[i]的取值为1~10
for (int j = 1; j < i; j++) { //判断是否重复,执行后保证所有方块的位置不重复
if (pos[i] == pos[j]) {
i--;
break;
}
}
}
for (int i = 2; i <= 20; i++) { //地图上移
for (int j = 1; j <= 10; j++) {
map[i - 1][j] = map[i][j];
}
}
for (int j = 1; j <= 10; j++) { //底层置零
map[20][j] = 0;
}
for (int i = 1; i <= n; i++) { //放置方块
map[20][pos[i]] = 1;
}
}
}
方块变形
刚开始写方块变形函数的时候遇到了一个问题
那就是有些方块贴在地图边缘时变形会卡出墙外
这时方块就不能变形,非常影响游戏体验
于是我想方块在变形之前就检测一下是否会超出地图范围
如果会超出就自动左移或右移若干格
但是对于每个方块移动的格数不同,就要分很多类,导致函数很长
我只截取了一部分放在这里
/*===========================
方块变形
先进行碰撞检测
若能变形就变形,否则跳过
有些方块靠近地图边缘变形时会超出地图
该函数将会自动将方块左右移动,使方块恰好不会超出地图
左右移动多少格与方块形状有关
因此对不同的方块要进行不同的处理,代码较长
而有些方块可以进行相同处理
因此要对方块特点分类
=============================*/
void Player::transform(HANDLE hOut) {
int x, y, shape, color; //x,y为下落中的方块在地图上的坐标
Block tempBlock;//临时方块
x = nowBlock.getX();//变量获取数据
y = nowBlock.getY();
shape = nowBlock.getShape();
color = nowBlock.getColor();
tempBlock = nowBlock;
if (shape == 5) //立方体形方块不会变形
return;
if (shape == 2) { //竖条形方块
tempBlock.setShape(1); //变形为横条形
if (!collisionDetection(tempBlock)) { //若能够变形
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1); //就把当前形状的方块清除
nowBlock.setShape(1); //正在下落的方块变形
return; //退出函数
}
tempBlock.setY(y - 1); //直接变形方块可能卡出地图外,那就试试向左移动一格能不能变形
if (!collisionDetection(tempBlock)) {
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1);
nowBlock.setShape(1);
nowBlock.setY(y - 1); //自动往左移动一格
return;
}
tempBlock.setY(y + 1); //再试试向右移动一格能不能变形
if (!collisionDetection(tempBlock)) {
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1);
nowBlock.setShape(1);
nowBlock.setY(y + 1);
return;
}
tempBlock.setY(y + 2); //再试试向右移动两格能不能变形
if (!collisionDetection(tempBlock)) {
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1);
nowBlock.setShape(1);
nowBlock.setY(y + 2);
return;
}
}
else if (shape % 2 == 1) { //编号为奇数的方块有共同特点(除了编号为5的立方体形方块)
tempBlock.setShape(shape + 1); //变形为下一个形状
if (!collisionDetection(tempBlock)) { //可以直接变形
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1);
nowBlock.setShape(shape + 1);
return;
}
}
else if (shape % 4 == 0) { //编号为4的倍数的方块
tempBlock.setShape(shape - 3);
if (!collisionDetection(tempBlock)) {
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1);
nowBlock.setShape(shape - 3);
return;
}
tempBlock.setY(y + 1);
if (!collisionDetection(tempBlock)) {
Render::clearBlock(hOut, stdblock[shape], 2 * y + mapX, x - 1);
nowBlock.setShape(shape - 3);
nowBlock.setY(y + 1);
return;
}
}
//之后的代码省略
}
游戏类
按键检测方法
我用了系统自带的_kbhit()函数和_getch()函数
检测到按键就调用相应的函数
if (_kbhit()) { //按键检测
key = _getch(); //获取按键
switch (key) {
case 97: //←键
player1.goLeft(hOut); break;
case 100://→键
player1.goRight(hOut); break;
case 119://↑键
player1.transform(hOut); break;
case 115://↓键
player1.goDown(hOut, p2); break;
case 75://A键
player2.goLeft(hOut); break;
case 77://D键
player2.goRight(hOut); break;
case 72://W键
player2.transform(hOut); break;
case 80://S键
player2.goDown(hOut, p1); break;
case 112://P键
gamePause(hOut); break;
case 27://Esc键
exitGame(hOut);break;
default:break;
}
}
三、依然存在的问题
1.最大的问题是双人模式两个玩家不能同时长按,这样只有一个玩家能正常游戏,比较影响游戏体验
我准备之后再试试其他按键检测函数
2.代码过于冗长,特别是方块变形函数,还不知道该怎么简化
3.遇到一个小bug:一个玩家的方块落到底部,但还没有固定,这时增加了一行,下落的方块就会和地图原来的方块重合。
4.两个玩家的地图同时满了的时候可能会有bug,因为没有平手的设定。(这个概率应该很低吧?)
四、收获与心得
游戏第一次运行的时候特别兴奋,玩自己的游戏有不一样的感觉。
刚开始的时候特别迷茫,不知道怎么开始,特别是渲染的问题。
可是到后来就越来越清晰了,各种想法也是在这个时候出现的。
刚开始的时候不应该想整个游戏怎么写,这样容易把自己吓到。
如果就从一个函数开始,把问题细化,就更有信心解决了。
渐渐的代码上的红色波浪线就消失了。
我并没有花很长时间调试,最后稍微修改一下就差不多了。
因为每写完一个函数我就想办法测试一下,虽然花了一些时间。
但这样能让我更安心,这样后面的工作量就不会很大。