作业描述 | 详情 |
---|---|
这个作业属于哪个课程 | 班级链接 |
这个作业要求在哪里 | 作业要求 |
这个作业的目标 | 代码的 git 仓库链接。 运行截图/运行视频。代码要点。 收获与心得。 依然存在的问题。 |
作业正文 | 我罗斯方块最终篇汇报 |
其他参考文献 | |
项目地址 | 项目GitHub地址 |
小组成员 | 031902517-田剑心 031902637-廖晓玲 061900414-廖智炫 |
仓库链接
代码仓库链接:https://github.com/JustinRochester/World-Tetris
运行视频
代码要点、收获、心得
田剑心
在本次的大作业中,我主要负责Birck
类是设计,以及后期对Render
类中添加了渲染方块的方法
代码要点
Brick
-
Brick
作为一个游戏块,由4个小方块构成,通过数组保存坐标信息int BrickPos[9];//坐标集合
-
为了便于坐标的推导以及移动计算,采用给定中心坐标的方法进而推导其他块的坐标
int CenterX, CenterY;//中心点坐标
-
与
Player
类交互,经过协商后只需要将颜色信息以及坐标以九元组形势返回int* getInformation();//返回信息
-
在实现这部分的时候还未学习继承,因此代码采用选择结构来实现多种块的坐标推导
-
旋转一个方块等于在中心坐标不变的情况将其变形成另一种形状,因此直接通过
void brickSet(int x, int y);
方法构造void Brick::rotateBrick() { int x = CenterX; int y = CenterY; int CountRotate; if (IsSym) CountRotate = 3; else CountRotate = 1; for (int i = 1; i <= CountRotate; i++) { BrickType--; if (BrickType >= 8) BrickType += 3; BrickType = ((BrickType >> 2) << 2) | ((BrickType + 1) & 3); if (BrickType > 8) BrickType -= 3; BrickType++; } brickSet(x, y); }
其他方法的具体实现可以在
github
上看源代码
Render
通过Player
传来的Map[][]
信息,再调用windows的SetConsoleCursorPosition()
方法来获取光标的位置,即可避免system("cls")
在一定程度上减少卡屏的效果,主要是双缓冲有点难搞
之后就是循环填充输出图形
具体实现见github
收获和心得
学了快1年的程序设计,一直只是做题目,感觉好像对自己的成长并未有很大的帮助,都不知道自己能干啥
借助这个机会,强迫自己去开发,可以说是收获良多
能够将简单的循环,选择等知识运用,借助搜索引擎,他人的开发经验,开发出一款团队合作的简单小游戏,可以说是肯定了自己的学习成果,虽然不咋地
通过本次的大作业,第一次将c++运用到项目开发,能够开发出自己的小游戏,其实还是挺开心的
在今后的学习中,应该多注重项目的制作,从小项目起手,完成一个项目不仅可以学到很多的知识点,而且还会有点小成就感。。。
廖晓玲
在本次的大作业中,我主要负责Render
类的设计
(但是写的都是比较基础的部分)
代码要点
Render
-
Render
是用来对各个模块的处理的一个模块 -
这是为了给界面以及各个模式用来上色的函数
void Render::SetColor(int color_num)//设置颜色
{
int n;
switch(color_num)
{
case 0: n = 0x08; break;
case 1: n = 0x0C; break;
case 2: n = 0x0D; break;
case 3: n = 0x0E; break;
case 4: n = 0x0A; break;
case 5: n = 0x0F; break;
case 6: n = 0x09; break;
case 7: n = 0x0B; break;
case 8: n = 0x05; break;
case 9: n = 0x03; break;
case 10: n= 0x00; break;
}
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), n);
}
- 选择使用光标对各个数据进行移动
void SetPos(int i, int j) //控制光标位置, 列, 行
{
COORD pos={i,j};
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}
- 然后是单人地图和双人地图
void DrawMap1(); //游戏界面
void DrawMap2();
收获和心得
通过这次的我罗斯方块的项目,我学习到了一些东西。这是我第一次接触到了渲染这个部分,由于我对渲染这个部分理解不够到位,经常出现错误,但是我还是学习到了不少东西,以后会更加努力。我希望以后自己能够有更好的代码能力,为此我也会更加努力的去学习,不断完善自我。我也十分感谢能够加入我所在的小组,我的代码能力不足,对项目的贡献较小,而在这次的开发过程中,组长的领导和组织的能力令我敬佩,让我看到了团队齐心协力的所能迸发出的能量是巨大的。
廖智炫
本次大作业中,本人负责 Player
类、Game
类、FileRecoder
类的开发,以及部分Brick
类和Render
类的优化
代码要点
Player
类
-
该类主要负责的是执行指令、规则判定、后续变换。其中包括执行给定的指令(如果指令合法且有效)、命令
Brick
类进行变换、边界的判定、地图的储存、方块与地图的合并、加行操作以及分数的记录 -
为了记录地图、积分以及玩家的个人信息,开通了以下三个属性来储存
int CountScore; bool MapSqure[32][12]; bool GameOver; std::string Name;
-
同时,为了方便后续的可修改性,地图的边界采用
static const int
来定义。而为了方便后续代码的实现,将当前方块与下一方块设置为该类的成员static const int UP_LIM, DOWN_LIM, LEFT_LIM, RIGHT_LIM; Brick NowBrick, NextBrick;
-
为了进可能减少码量,定义了方法
bool isOverlap()
来判定当前方块是否与地图重叠bool Player::isOverlap(){ /* This method is used to check whether the working brick is overlapping the map. */ const int* Tmp = NowBrick.getInformation(); for (int i = 1; i < 9; i+=2) if (Tmp[i]<UP_LIM || Tmp[i]>DOWN_LIM) return true; for (int i = 2; i < 9; i+=2) if (Tmp[i]<LEFT_LIM || Tmp[i]>RIGHT_LIM) return true; /* The brick is out of the map. We considered that it is a case of overlap. (Considered that there are some walls around the map.) */ for (int i = 1; i < 9; i+=2) if (MapSqure[Tmp[i]][Tmp[i + 1]]) return true; return false; }
-
定义上述方法后,其余操作即可高效的进行:每次输入指令,先直接命令
Brick
类变换(包括上下左右移动、旋转)。若移动过程中重叠则可终止操作。 -
将
Brick
类加入地图后,定义方法int delLine()
来实现消除满行的操作,修改地图信息并且返回消除的行数,以方便下一步操作。实现方法比较简单:考虑到可能一次性消除多行,先在第一次扫描时,消除满的行,并记录消除的行数;其次将下一行为空行的行下移 -
为了实现此消彼长,定义方法
int addLine(int CountLine)
来实现操作:给该玩家的地图从底部开始,加入指定行数。由于设定中,每一行的个数为 (10) 个方块,每个方块在加行后都是有或者没有的状态,共记 (2^{10}=1024) 种状态;再扣除全空和全满两个非法状态,故本人使用随机数rand()%1022+1
的方法来生成所加行的信息。其他的一些操作主要就是上移以及判断是否消行:int Player::addLine(int CountLine) { static int Bas = (1 << RIGHT_LIM - LEFT_LIM + 1) - 2; if (GameOver) return 0; for (int i = UP_LIM; i <= DOWN_LIM - CountLine; i++) for (int j = LEFT_LIM; j <= RIGHT_LIM; j++) MapSqure[i][j] = MapSqure[i + CountLine][j]; /* Move all of the map up. */ for (int i = DOWN_LIM, j = 1; j <= CountLine; i--, j++) { int State = rand() % Bas + 1; for (int j = LEFT_LIM; j <= RIGHT_LIM; j++, State >>= 1) MapSqure[i][j] = (State & 1); } /* Built the first CounLine lines randomly. */ int CountDeleteLine = 0; if (isOverlap()) { while (isOverlap()) NowBrick.Operation(Brick::Up); } if (touchBottom()) { addToMap(); CountDeleteLine = delLine(); renewBrick(); } /* Move the working brick up if it overlaps the map. And check out whether it is touches the bottom. */ if (touchCeiling()) { GameOver = 1; return 0; } return CountDeleteLine; }
-
其余的实现细节可以参考本人传到GitHub上的代码
Game
类
-
这是整个程序中最复杂的类,且是设定中唯一一个从键盘读入指令的类
-
该类主要负责界面信息的处理、调用
Player
类进行游戏的执行、调用Render
类进行界面的绘制、调用FileRecoder
类进行本地信息的读入、调用PlaySound
类进行声音的播放 -
开 int 型的三个变量,
GameMode,CountPlayer,OperationMode
分别表示游戏模式(共10个)、玩家数量(1-2个)、操作模式(是否允许连按) -
由于选择的渲染方式为利用
conio.h
中的输出方式进行绘制。故我们设定,对于允许连按的模式下,监听操作的9个键(上下左右、WSAD与ESC)。前八个累计到一定数额时视为一次有效操作。开变量int FramesCount
来记录上次渲染到目前位置经过的帧数,并固定每一帧为statit const int FramesTime=25ms
。每当记录的帧数达到 40 帧,即 1s ,此时让所有玩家的方块下落一次(加速模式除外) -
而对于不可连按操作模式下,我们记录9个键上一次的状态和现在的状态。当且仅当上一次状态为未按下,且当前状态为按下状态,执行指令。并且使用 clock() 函数在每一次监听后,判断是否达到一帧时长。累计达到 40 帧时长时,让所有玩家的方块下落一次(加速模式除外)
-
开通方法
void renderMap()
作为Player
类的友元函数,复制整个Player
的地图,并交由Render
类进行绘制 -
为方便地实现一些界面中的光标移动操作,定义方法
void moveCur(int &NowCur,char c)
识别相关指令,并移动光标:void Game::moveCur(int& NowCur, char Command) { if (0); else if (Command >= '0' && Command <= '9') NowCur = (Command - 48); else if (Command == '-' || Command == 'W' || Command == 'w') NowCur--; else if (Command == '+' || Command == 'S' || Command == 's') NowCur++; else if (Command == DIRECTIONS) { Command = _getch(); if (Command == UP) NowCur--; else if (Command == DOWN) NowCur++; } }
-
其余的界面实现与其他细节同样可参见本人上传至 GitHub 的代码
FileRecoder
类
-
作为最后加入的两个类之一,该类只负责从文档读入信息,以及将信息输出至文档。故采用了文件流的方法进行快速的操作。
-
定义文件名与自定义的文件后缀为 string 类,方便操作:
static const std::string Suffix, FileName[11];
-
从文档中读入的记录最高分、记录保持者、以及玩家姓名、相关设定储存如下:
std::string NamePlayer[2], NameRecoder[10]; int ScoreRecoder[10]; int OperationMode;
-
读入时,若发现文档不存在,设定其自己建立文件,并初始化内容:
FileRecoder::FileRecoder() { std::fstream iofile; for (int i = 0; i < 10; i++) { iofile.open((FileName[i] + Suffix).c_str(), std::ios::in); if (!iofile.is_open()) { iofile.open((FileName[i] + Suffix).c_str(), std::ios::out); iofile << "Nobody" << std::endl << -1 << std::endl; iofile.close(); iofile.open((FileName[i] + Suffix).c_str(), std::ios::in); } getline(iofile, NameRecoder[i]); iofile >> ScoreRecoder[i]; iofile.close(); } iofile.open((FileName[10] + Suffix).c_str(), std::ios::in); if (!iofile.is_open()) { iofile.open((FileName[10] + Suffix).c_str(), std::ios::out); iofile << "Player1" << std::endl << "Player2" << std::endl << 0 << std::endl; iofile.close(); iofile.open((FileName[10] + Suffix).c_str(), std::ios::in); } getline(iofile, NamePlayer[0]); getline(iofile, NamePlayer[1]); iofile >> OperationMode; iofile.close(); }
-
同样处理输出:用文件流打开,并输出至改文件。同时,设置每次修改玩家名称、清除记录、退出游戏操作都会自动输出。具体实现可参考GitHub代码。
Brick
类
-
本人进行的优化主要是保证了方块更新的随机性,以及通过对称实现L型和Z型方块的对称方块的设置
-
由于原本的设定中,包含了 17 种方块,且除了第 9 种田字型方块,所有方块都是4个连续的元素表示其旋转到不同角度的状态。该种随机数生成方式会导致田字型的生成概率为其余的 ({1over 4}) ,且无法生成两种对称的方块。
故更改设定为:先随机生成一个7以内的数字,表明方块大类;再设定如果为第6、7种方块,即对称的方块,则识别为原方块的对称。同时,第二次生成一个4以内的随机数,表明出场时旋转的次数。最后根据这些情况,生成相应的数据。这样即可保证方块的等概率性。 -
对称的实现原理在于:由中点坐标公式可得:({x_1+x_2over 2}=x_M) 反演出对称点坐标 (x_2=2x_M-x_1) 。该类的生成中本身即使用了中点的信息,故加入新方法进行操作:如果是对称的,则执行操作变换为对称点。
同时,由于对称方块与原方块满足镜像对称的性质,故原方块的顺时针旋转会导致镜像方块的逆时针旋转。故设置如果为对称方块,旋转3次抵消镜像问题。
Render
类
-
本人和南理工大佬樱落三千共同进行了部分界面的设计、美化、开发与测试
-
关于上述两种操作方案,唯一需要克服的问题便为渲染的时间问题。采用原渲染方式,会导致每次渲染时长为:单人 45ms;双人 120ms;远超过一帧的设定时间。
故采用新型地图渲染方式:先输出整个地图边框。每次输出地图信息时,先设定为黑色背景,此后只需要将每一行的地图信息转化为输出信息,再设定起始位置后统一用高效的fwrite函数输出。成功将时间优化至 20ms 以内
PlaySound
类
-
此类不为本组三位人员开发。为南理工大佬樱落三千觉得游戏基本成型,欠缺音频,自愿加入开发。
-
该类可实现音频、音效的播放、暂停
收获和心得
本人从高一学习编程至今,第一次开发这种比较大型的程序,确实与以前的算法竞赛的编程具有很大的不同点。
这种开发情况下,对于整体架构的把握、细节的考虑、内容的封装、消息的传递具有很高的要求,且对于效率要求、时间的优化不比算法竞赛低。
开发过程中,最重要的两件事就是抬头多学,低头多调。
开发中的友元函数、友元类、文件读写、控制台设定、键盘监听、音乐播放、程序图标设置,我们都是第一次学习,并进入开发的。学习是非常重要的,且仅仅在课堂上的学习是远远不够的。善于利用现在强大的网络,将帮助我们更快的进步。
而另一个重点就是多调。代码写出来是很难不出现 bug 的。因此,本组的我罗斯方块共计发布了5次内测版和5次公测版,不完全统计,经本组人员与组外人员发现 bug 约几十例。每一次的 debug 都是让我们的程序更加的严谨。
另一个很深的感触在于面向对象编程对于功能修改的方便。在面向对象编程的情况下,如果真遇上我这样经常修改方案啊产品经理 (那好惨) ,每次修改也只需要修改相关的类即可。对于已经测试通过的类,甚至完全不需要改动。
这一点尤其在后期,格外的突出。当我们完成第二版公测版时,已经解决了所有游戏运行的 bug 。后面的每次公测版都是增添功能或者修改界面,真就只需要修改到总控制的Game
类、输出的Render
类,以及其他部分相关的类即可。基本完成的Player
类与Brick
类后期基本没有再动过。开发效率十分高效。
今后我会在学习之余,加强这种程序开发的能力。
依然存在的问题
- 在允许长按模式下,可能存在吞键的情况。即按键不响应。
- 无法克服控制台被鼠标误触后暂停的情况。
- 部分界面的刷新可能存在闪屏问题
- 建立的快捷方式存在无法使用的问题,仍需要点集文件夹内的原程序启动。
- 仅允许更改音效,理论上不允许更改背景音乐。
- 界面仍可以美化。
- 存在部分操作系统的人无法打开的情况。