我罗斯方块最终篇汇报

作业描述 详情
这个作业属于哪个课程 班级链接
这个作业要求在哪里 作业要求
这个作业的目标 代码的 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个连续的元素表示其旋转到不同角度的状态。该种随机数生成方式会导致田字型的生成概率为其余的 \({1\over 4}\) ,且无法生成两种对称的方块。
    故更改设定为:先随机生成一个7以内的数字,表明方块大类;再设定如果为第6、7种方块,即对称的方块,则识别为原方块的对称。同时,第二次生成一个4以内的随机数,表明出场时旋转的次数。最后根据这些情况,生成相应的数据。这样即可保证方块的等概率性。

  • 对称的实现原理在于:由中点坐标公式可得:\({x_1+x_2\over 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类后期基本没有再动过。开发效率十分高效。

今后我会在学习之余,加强这种程序开发的能力。


依然存在的问题

  1. 在允许长按模式下,可能存在吞键的情况。即按键不响应。
  2. 无法克服控制台被鼠标误触后暂停的情况。
  3. 部分界面的刷新可能存在闪屏问题
  4. 建立的快捷方式存在无法使用的问题,仍需要点集文件夹内的原程序启动。
  5. 仅允许更改音效,理论上不允许更改背景音乐。
  6. 界面仍可以美化。
  7. 存在部分操作系统的人无法打开的情况。
posted @ 2020-06-08 01:02  JustinRochester  阅读(305)  评论(1编辑  收藏  举报