本文最后更新于 2024-10-22T11:39:14+00:00
# 基于Glut的俄罗斯方块
概述
作为大一下期的一个C++程序设计的作业,原本李卫明老师是打算让我们用MFC实现一个俄罗斯方块的,但是我不想学习MFC,所以使用了glut来实现它。所有的代码由自己一个人完成,Game类的维护由李卫明老师的教程优化而来。李卫明老师课程传送门:
1.建立框架
2.添加功能模块
3.消息响应和界面绘制
其中,我借鉴了李老师俄罗斯方块的存储方式(4*4的二维数组来存储一个俄罗斯方块)然后在这篇博客的最后也回答一下李老师提出的这些问题:
Q1本游戏开发的主要过程分哪几步?点击跳转
Q2游戏界面里控件ID有什么作用? 点击跳转
Q3游戏中主要有哪些类?哪些类使用了继承? A:并没有
Q4用什么表示俄罗斯方块的形状和颜色?点击跳转
Q5l游戏里的俄罗斯方块判断是否可以下落、左移、右移、旋转的方法是什么?点击跳转
Q6程序里如何实现俄罗斯方块实际下落、左移、右移、旋转?点击跳转
Q7程序里是哪里使用了动态分配?如何避免内存泄漏? A:没有使用,唯一的地方是Vector,不需要自己进行管理。
Q8主界面如何绘制、备用俄罗斯方块如何绘制?点击跳转
Q9如何实现俄罗斯方块的定时下落?点击跳转
Q10如何实现按钮点击响应?点击跳转
所以这篇博客可能会比较长,就当自己的一次记录吧。
Game类
Game类的规划
在我的Game类里面,存储了整个游戏需要的数据,而且维护了整个游戏的运行,但是因为回调函数不允许绑定了对象的函数指针,所以我使用了友元函数作为回调函数,至于为什么没有使用类的静态函数成员,可能是因为我懒吧。或者这个项目比较小,所以没在乎这么多了。这次的代码及其不规范,算是给自己积累一些经验吧。
Game类的数据成员
首先我们来分析一下俄罗斯方块这个游戏的场景和运行逻辑:
这里分为了三个方块单位,和一些文字单位,可以看到,蓝色的俄罗斯方块正在随着时间往下走,白色的预备方块静止在等待区域,那么我们需要两个俄罗斯方块对象来存储他们的信息。再看下面的绿色 L 形状的俄罗斯方块,它已经到底了,和场景融为了一体,那么我们也就不需要一个俄罗斯方块的类来存储它了,可以把它存储在场景中,我们用一个二维向量来存储这个场景的信息。
方块成员
Tool类我们稍后再讲,它会是存储一个俄罗斯方块的工具类。然后我们使用vector<vector<int>>来存储这个场景的所有信息。(李卫明老师的教程中使用了动态分配的数组。)
信息成员(分数,等级)
分数、最高分和等级我们使用三个无符号整型来存储。等级是难度等级,可以用于我们之后的提高难度。
好了,现在我们来实现最基础的Tool类,Tool类是一个俄罗斯方块对象,它有着这些属性:
**
1.记录了方俄罗斯方块的类型
2.记录了俄罗斯方块的形状
3.记录了俄罗斯方块的位置
4.记录是否有过移动(用于判断游戏结束)
5.可以进行移动、旋转
**
点击返回问题目录
1.俄罗斯方块的类型
我们使用一个枚举类型,来方便自己绘制俄罗斯方块,枚举中的名字为俄罗斯方块的类型,还可以作为绘制方块时的颜色的枚举使用,如下:
其中 WALL是提供给场景使用的一个枚举常量
2.俄罗斯方块的形状
每一个俄罗斯方块都可以认为被一个4 * 4的网格包裹住了,所以我们可以使用一个4 * 4的二维数组来记录它在地图中所占的位置:
3.俄罗斯方块的位置
我们记录一个4 * 4的网格的位置,其实只需要这个网格左上角的方块的坐标就可以了,也就是说我们记录俄罗斯方块的位置,其实只需要记录一个点的x、y坐标。
4.是否移动过
很简单,使用一个bool类型的变量即可,那么我们的Tool类的数据成员看起来像这样:
点击返回问题目录
5.移动旋转的实现
判断是否可以旋转的时候,我们就需要和场景中的方块联系起来了,如果一个方块无法往某个方向移动的话,意味着移动了就会与已经存在的方块重合,或者出游戏区域,那么我们的Tool类旋转的工作就是返回一个移动后的Tool类的结果,但是不能直接对自己移动,因为对于一个Tool对象来说,它不能知道自己是否可以移动但是移动的函数,直接移动就行,之后后讲到为什么,这里举两个例子,旋转和下落:
旋转
1 2 3 4 5 6 7 8 9 Tool Rotate () { Tool NewOne (*this ) ; for (int i = 0 ; i < 4 ; i++) for (int j = 0 ; j < 4 ; j++) { NewOne._data[j][3 - i] = _data[i][j]; } return NewOne; }
下落
1 2 3 void MoveDown () { setPosition (_x, _y + 1 ); }
特别的setPosition函数,就是更改了Tool的位置信息,就不说了。然后我们来说一下Tool的构造函数。我们需要自己定义两个构造函数,一个是默认的构造函数,一个是接受三个参数的构造函数,函数原型如下:
它接受了一个坐标和一个类型作为参数,对对象进行初始化。要记得对所有的数据成员进行初始化哦,保持好习惯~
带参的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 Tool (int x, int y, ToolType type) :_x(x), _y(y), _type(type),run (false ) { _data.resize (4 , vector <int >(4 )); for (int i = 0 ; i < 4 ; i++) for (int j = 0 ; j < 4 ; j++) _data[i][j] = 0 ; switch (type) { case LL: for (int i = 0 ; i <= 2 ; i++)_data[1 ][i] = LL; _data[2 ][2 ] = LL; break ; case OO: for (int i = 1 ; i <= 2 ; i++) for (int j = 1 ; j <= 2 ; j++) _data[i][j] = OO; break ; case DOT: _data[1 ][1 ] = DOT; break ; case II: for (int i = 0 ; i < 4 ; i++)_data[1 ][i] = II; break ; case TT: for (int i = 0 ; i < 3 ; i++)_data[2 ][i] = TT; _data[1 ][1 ] = TT; break ; } }
然后,我们还需要一个默认构造函数,来创建临时的Tool对象:
1 2 3 4 5 Tool () :run (false ),_x(0 ),_y(0 ),_type(LL) { _data.resize (4 , vector <int >(4 )); }
因为没有动态分配的数据(vector可以自己析构,不用处理),所以我们不需要写析构函数,使用默认的析构函数即可。
到此,我们的Tool类就写完了。
Game类的数据维护函数(私有)
Game类有了他自己的数据成员,那么可以开始对数据成员进行维护了,当一个俄罗斯方块落地的时候,替补的俄罗斯方块就会替补当前使用的俄罗斯方块,然后再生成一个新的俄罗斯方块,这个函数我们叫NextTool()
NextTool
1 2 3 4 5 6 void Game::NextTool () { swap (m_tool, m_next); m_tool.setPosition (rePx, rePy); ToolType aType = ToolType (abs (rand ()%(TTEnd-LL))+LL); m_next = Tool (waitPx, waitPy, aType); }
我们首先交换两个Tool对象,然后把工作中的Tool对象m_tool移动到游戏区域,并且把m_next对象重置为一个随机的俄罗斯方块。这里的随机操作其实是比较优美的形式,因为abs(rand()%a)得到是数据的范围是(0,a-1),但是我们的ToolType的枚举的数值范围是(LL,TTEnd-1),所以我们设置为abs(rand()%(TTEnd-LL))+LL就解决了这个问题。
AddToMap
然后当这个Tool无法继续下落的时候,我们判断这个这个Tool对象应该加入场景了,我们创建函数AddToMap():
AddToMap
1 2 3 4 5 for (int i=0 ;i<4 ;i++) for (int j = 0 ; j < 4 ; j++) if (m_tool._data[i][j]) m_map[i + m_tool._x][j + m_tool._y] = m_tool._data[i][j];
思考一下,什么时候会出现一行被消除?是不是只有某一个对象加入场景的时候?也就是说,我们应该在对象加入场景的时候进行检测,是否存在某行可以消除。我们可以在加入场景的时候记录一下改变的行数,然后对这些改变的行进行检测。最终的AddToMap代码如下:
AddToMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 void Game::AddToMap () { vector<int >test; for (int i=0 ;i<4 ;i++) for (int j = 0 ; j < 4 ; j++) if (m_tool._data[i][j]) { m_map[i + m_tool._x][j + m_tool._y] = m_tool._data[i][j]; test.push_back (j + m_tool._y); } auto ibeg = test.begin (); while (ibeg != test.end ()) { int i = *ibeg; bool flag = true ; for (int j = dx; j < GameRow; j++) { if (!m_map[j][i]) { flag = false ; break ; } } if (flag) { for (int k = i; k > 0 ; k--) { for (int j = dx; j < GameRow; j++) { m_map[j][k] = m_map[j][k - 1 ]; } } for (int j = dx; j < GameRow + dx; j++) m_map[j][0 ] = 0 ; clearTimes++; _points += GameRow * clearTimes; if (_points > PB_points) { PB = true ; PB_points = _points; } } Diff = clearTimes / 3 ; ibeg++; } if (!m_tool.run) { GameOver (); } }
很好,我们的AddToMap函数就这样完成了。接下来就是让我们的Tool对象接受消息然后动起来了。我们需要实现向下、向左、向右以及旋转的判断和实施函数。
向下、向左、向右以及旋转
点击返回问题目录
我们的Tool类可以返回变换之后的结果,那么我们的Game类就需要判断是否能够进行变换,以及管理变换,这里我们还是以向下和旋转来举例:
CanMoveDown
1 2 3 4 5 6 7 8 9 10 11 12 bool Game::CanMoveDown () { for (int i=0 ;i<4 ;i++) for (int j = 0 ; j < 4 ; j++) { if (m_tool._data[i][j]) { if (m_tool._y +j+ 1 < Cow) { if (m_map[m_tool._x+i][m_tool._y +j + 1 ])return false ; } else return false ; } } return true ; }
这个函数没什么意思,就是一个个的查看,如果重合,那么说明不能进行移动,否则可以。CangetMoveRight函数和CangetMoveLeft函数也相似。本质上是因为平移只需要改变Tool对象的坐标就行了,不需要对节点信息进行修改。让我们来看看旋转
Rotate
1 2 3 4 5 6 7 8 9 10 11 12 13 bool Game::Rotate () { if (m_tool._type == DOT||m_tool._type==OO)return true ; Tool revTool = m_tool.Rotate (); for (int i=0 ;i<4 ;i++) for (int j = 0 ; j < 4 ; j++) if (revTool._data[i][j]) if (m_map[revTool._x+i][revTool._y+j])return false ; swap (m_tool, revTool); return true ; }
因为旋转的开销还是比较大的,所以我们不进行多次旋转操作,也就是不设置CanRotate函数,直接改成Rotate函数,如果能够旋转就实施了。
Drop
因为下落有着独特的数据维护方法,所以我们写一个Drop函数来维护这一行为,Drop函数需要检查是否可以下落,以及下落之后的状态更新,还有不能下落时的数据处理。
Drop
1 2 3 4 5 6 7 8 9 10 void Game::Drop () { if (CanMoveDown ()) { m_tool.run = true ; m_tool.MoveDown (); } else { AddToMap (); NextTool (); } }
在可以移动时,更新Tool的移动状态为true,并更新m_tool的数据,
Start
到这里,我们的数据维护函数就基本上写完了,我们的Game对象可以操控两个Tool对象进行移动,判断是否可以移动,计算游戏加分,计算游戏是否结束等等,接下来我们为Game类添加开始函数,因为在游戏开始之前,我们的Game对象中的Tool对象是没有任何数据的,所以我们使用两次NextTool来让m_tool和m_next都有数据。然后,我们还需要引入一个新的概念游戏状态,我们来看Start函数:
Start
1 2 3 4 5 void Game::Start () { status = RUN; NextTool (); NextTool (); }
我们要做的事情很简单,把游戏的状态更改为RUN
,然后调用两次NextTool就行了。
游戏状态的更新
首先我们为Game类添加一个枚举对象,用于描述游戏的状态:
1 2 3 4 5 6 7 typedef enum { STOP, PRESTART, RUN, GAMEOVER, GAMEWIN, }GameMode;
(其实是不存在游戏胜利这一说的,但是我想留一个小彩蛋,所以加入了这一状态,但是达到这个条件是人类不太可能完成的。)
然后我们为之前需要更改游戏状态的地方添加游戏状态的更改。比如在AddToMap中提到的GameOver函数:
GameOver
1 2 3 4 5 6 7 void Game::GameOver () { if (_points > PB_points) { PB = true ; PB_points = _points; } status = GAMEOVER; }
更新游戏状态,并检测是否破纪录。
构造函数
创建一个Game类的对象的时候,将游戏的状态设置为PRESTAT开始前:
构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Game::Game () :status (PRESTART),_points(0 ),PB_points (0 ),PB_Diff (0 ), clearTimes (0 ),Diff (0 ),PB (false ) { m_map.resize (Row, vector <int >(Cow)); srand ((unsigned int )time (NULL )); for (int i = 0 ; i < Cow; i++) { for (int j = 0 ; j < dx; j++) m_map[j][i] = WALL; for (int j = 1 ; j <= dx; j++) m_map[GameRow + j][i] = WALL; for (int j = 1 ; j <= dx; j++) m_map[Row - j][i] = WALL; } for (int i = 0 ; i < dx; i++) for (int j = 0 ; j < Row - GameRow - 3 * dx; j++) { m_map[GameRow + 2 * dx + j][i] = WALL; m_map[GameRow + 2 * dx + j][Cow - 1 - i] = WALL; } for (int i = 0 ; i < Row - GameRow - 3 * dx; i++)m_map[GameRow + 2 * dx + i][4 + dx] = WALL; for (int i = 0 ; i < 4 ; i++)m_map[2 * dx + GameRow][dx + i] = WALL; }
构造函数不仅要初始化所有数据、设置游戏状态外,还需要将场景的WALL部分填充,dx是设置的一个静态整形变量,用于调整边界的厚度。Row和Cow是屏幕的大小;GameRow和GameCow就是游戏区域了,这些是整形常量,这个图可以说明这一关系:
这里是我的初始化
1 2 3 4 5 6 const int mapScre = 1 ; const int NodeSize = 20 ; const int Cow = 30 * mapScre; const int Row = 24 * mapScre; const int GameCow = 30 * mapScre; const int GameRow = 16 * mapScre;
1 2 3 4 5 static const int dx, dy; static const int rePx, rePy; static const int waitPx, waitPy; static const int infoPx, infoPy;
reset
其他的状态更新操作会在消息处理的时候进行,然后我还需要完成Game类的最后一个成员函数reset,用于重置游戏:
1 2 3 4 5 6 7 8 9 10 void Game::reset () { PB = false ; status = PRESTART; _points = 0 ; clearTimes = 0 ; Diff = 0 ; for (int i = dx; i < GameRow+dx; i++) for (int j = 0 ; j < GameCow; j++) m_map[i][j] = 0 ; }
Glut
不会装glut的可以参考这篇博客
【Opengl】Glut下载与环境配置
返回irrklang
另外,我们还需要glfw来提供消息处理的宏定义。这里我只教VS的配置方法:
打开你的目标工程,然后点击上方的工具->NuGet包管理器
点击浏览->搜索glfw->下载
下载到当前项目就行了。我们需要的头文件有这么两个:
1 2 #include <gl/glut.h> #include <GLFW/glfw3.h>
当然还要包括你自己写的Game类的头文件,然后创建我们唯一的Game类对象(作为全局变量)Game myGame;
在main函数中对窗口进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main (int argc, char * argv[]) { glutInit (&argc, argv); glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB); glutInitWindowSize (Row*NodeSize, Cow*NodeSize); glutInitWindowPosition (100 , 100 ); glutCreateWindow ("Terix" ); glutMainLoop (); return 0 ; }
然后我们还需要绑定输入回调函数、显示回调函数、计时器、窗口尺寸改变的回调函数以及设置菜单(非必须)。
回调函数
回调函数中,大部分需要对Game类的的数据进行更新或者读取,那么最好的办法就是使用成员函数进行绑定,但是这是不被允许的。因为指向绑定函数的指针只能用于调用函数。也就是说我们的回调函数不能是类的成员函数。所以我们将其设置为友元。
1 2 3 4 friend void Input (int data, int x, int y) ;friend void onDisplay () ;friend void Input (unsigned char nchar, int x, int y) ;friend void processMenuEvents (int option) ;
然后我们开始,先从简单的来处理
1.窗口尺寸变化回调函数
这个函数允许我们的窗口在大小改变时,不会出现图像拉扯的情况,设置了这个回调函数之后的效果是这样的:
拖动窗口,使其尺寸发生变化,会得到这样的结果:
不仅如此,我们的投影也发生在这个函数里面,投影的原理我就不讲了,这涉及到一些图形学的内容,有兴趣的可以去看Games101的课程,传送门:
【GAMES101-现代计算机图形学入门-闫令琪】
这里我们设置一个简单的二维平行投影:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void onReshape (int w, int h) { glViewport (0 , 0 , w, h); glMatrixMode (GL_PROJECTION); glLoadIdentity (); gluOrtho2D (0 , w, h, 0 ); glMatrixMode (GL_MODELVIEW); glutPostRedisplay (); } ------------ glutReshapeFunc (onReshape);
2.计时器
计时器可以让我们在一定的时间内调用一次该函数,我们需要计时器来使俄罗斯方块自动的下坠,并且下降的速度就是我们调用这个函数所间隔的时间,我们可以利用这点来动态的设置游戏的难度:
点击返回问题目录
1 2 3 4 5 6 7 8 9 10 11 12 13 void Ontime (int timeid) { timeid++; timeid %= 1000 ; if (myGame.getStatus () == RUN) myGame.Drop (); int Deltatime = 150 - 3 * myGame.getDiff (); if (Deltatime <= 50 )myGame.ChangeStatus (GAMEWIN) ; onDisplay (); glutTimerFunc (Deltatime, Ontime, 1 ); }
这里我设置了一个很简单的难度系统,难度和等级成正比?Deltatime就是调用该函数所间隔的时间,我设置了当Deltatime<=50时判定为游戏胜利,也就是1秒钟1000/50=20帧,就是说它会下降20格。(
3.显示回调函数
可以看到,在Ontime里面调用了onDisplay函数,这个函数绘制了Game类的所有信息。首先让我们学一下glut的绘制逻辑,在每次绘制之前,我们需要对屏幕上的像素进行清理,我们叫它Clear,清理也不是随便清理,相当于清除了一帧的缓存,清除之前设置一个清楚颜色:
1 2 3 4 5 6 7 8 9 10 glClearColor (BCR_COLOR,1 ); glClear (GL_COLOR_BUFFER_BIT); glutSwapBuffers ();
因为我们采用了双缓存的模式,所以以交换缓存为绘制结束。这样我们就不会出现频闪的问题。这里我也可以解释一下频闪出现的原因,频闪的出现不是因为电脑性能不行,而是你在画图的时候给屏幕发的绘制指令数太多了,应该先把一帧的画图操作全部写在缓存中,然后一次性发给屏幕绘制。
让我们回到俄罗斯方块的游戏场景,这次我们分游戏状态来处理:
RUN
这里我们需要绘制的信息有:两个俄罗斯方块、场景方块、游戏信息。
STOP
STOP很简单,我们只需要在RUN的基础上加上一句暂停的提示语就行了。
GAMEOVER
为了让GAMEOVER的时候,玩家可以感受到一种视觉冲击,所以我设置为了只绘制失败信息,并且如果分数没有超过作者的话,会奉上一句Git Gud,以表示敬意 。
PRESTART
这个就是游戏开始之前的画面,需要绘制一些指导的信息,比如如何开始游戏、游戏的一些快捷操作、游戏的自动保存机制等等。
实现
从上面的总结来看,不难发现:只有两个状态需要绘制场景和俄罗斯方块:
如何绘制一个方块?
在正式的绘制信息之前,我们先来介绍一下如何绘制一个正方形。在glut中绘制一个正方形的方式非常的简单,我们可以直接使用它封装的函数进行绘制。绘制一个矩形的代码会像这样:
1 2 glColor3f (ITEM_COLOR);glRectd (x1,y1,x2,y2);
在绘制矩形前设置矩形的填充颜色,然后设置矩形对角线的两个点的坐标就行了。
点击返回问题目录
如何绘制信息?
在画面上,还有各种的信息显示,这里使用的是全英文的位图,因为内置的位图不支持中文,所以使用英文图个方便。绘制一串话的代码会像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 const char points[20 ] = "Points:" ;char points_num[20 ];sprintf (points_num, "%d" , myGame._points);glColor3f (TEXT_COLOR);glRasterPos2d (Infox, Infoy);for (int i=0 ;points[i]!='\0' ;i++) glutBitmapCharacter (INFO_FONT, points[i]);glRasterPos2d (Infox, Infoy+5 );for (int i=0 ;points_num[i]!='\0' ;i++) glutBitmapCharacter (INFO_FONT, points_num[i]);
因为使用了sprintf,所以这段代码在VS是跑不出来的,我们需要让VS认为这是安全的:加入宏定义:#define _CRT_SECURE_NO_WARNINGS
具体实现
运行时和暂停的绘制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 if (myGame.status!=PRESTART&&myGame.status!=GAMEOVER&&myGame.status!=GAMEWIN) { for (int i = 0 ; i < Row; i++) { bool flag = true ; for (int j = 0 ; j < Cow; j++) { switch (myGame.m_map[i][j]) { case 0 : flag = false ; break ; case Game::LL: glColor3f (LL_COLOR); break ; case Game::OO: glColor3f (OO_COLOR); break ; case Game::DOT: glColor3f (DOT_COLOR); break ; case Game::II: glColor3f (II_COLOR); break ; case Game::TT: glColor3f (TT_COLOR); break ; case Game::WALL: glColor3f (WALL_COLOR); break ; default : glColor3f (ELSE_COLOR); } if (flag) glRectd (i * NodeSize, j * NodeSize, (i + 1 ) * NodeSize, (j + 1 ) * NodeSize); flag = true ; } } switch (myGame.m_tool._type) { case Game::LL: glColor3f (LL_COLOR); break ; case Game::OO: glColor3f (OO_COLOR); break ; case Game::DOT: glColor3f (DOT_COLOR); break ; case Game::II: glColor3f (II_COLOR); break ; case Game::TT: glColor3f (TT_COLOR); break ; default : glColor3f (ELSE_COLOR); } for (int i = 0 ; i < 4 ; i++) { for (int j = 0 ; j < 4 ; j++) { if (myGame.m_tool._data[i][j]) glRectd ((myGame.m_tool._x + i) * NodeSize, (myGame.m_tool._y + j) * NodeSize, (myGame.m_tool._x + i + 1 ) * NodeSize, (myGame.m_tool._y + j + 1 ) * NodeSize); } } switch (myGame.m_next._type) { case Game::LL: glColor3f (LL_COLOR); break ; case Game::OO: glColor3f (OO_COLOR); break ; case Game::DOT: glColor3f (DOT_COLOR); break ; case Game::II: glColor3f (II_COLOR); break ; case Game::TT: glColor3f (TT_COLOR); break ; default : glColor3f (ELSE_COLOR); } for (int i = 0 ; i < 4 ; i++) { for (int j = 0 ; j < 4 ; j++) { if (myGame.m_next._data[i][j]) glRectd ((myGame.m_next._x + i) * NodeSize, (myGame.m_next._y + j) * NodeSize, (myGame.m_next._x + i + 1 ) * NodeSize, (myGame.m_next._y + j + 1 ) * NodeSize); } } const char points[20 ] = "Points:" , PBpoints[20 ] = "PB:" , Diff[20 ] = "Rank:" ,zhywyt[]="zhywyt:" ; char points_num[20 ], PBpoints_num[20 ], Diff_num[20 ],zhywyt_num[20 ]; sprintf (points_num, "%d" , myGame._points); sprintf (PBpoints_num, "%d" , myGame.PB_points); sprintf (Diff_num, "%d" , myGame.Diff); sprintf (zhywyt_num, "%d" , myGame.zhywyt); const char * strinfo[] = { points,PBpoints,Diff,zhywyt }; char * str_num[] = { points_num,PBpoints_num,Diff_num ,zhywyt_num}; if (!myGame.PB) glColor3f (TEXT_COLOR); else glColor3f (PB_TEXT_COLOR); for (int i = 0 ; i < 4 ; i++) { if (i == 3 )glColor3f (PB_TEXT_COLOR); glRasterPos2d (Game::infoPx * NodeSize, (Game::infoPy + 5 * i) * NodeSize); for (int j = 0 ; strinfo[i][j] != '\0' ; j++)glutBitmapCharacter (INFO_FONT, strinfo[i][j]); glRasterPos2d (Game::infoPx * NodeSize, (Game::infoPy + 5 * i + 2 ) * NodeSize); for (int j = 0 ; str_num[i][j] != '\0' ; j++)glutBitmapCharacter (INFO_FONT, str_num[i][j]); } if (myGame.status == STOP) { const char PrestartInfo[30 ] = "Press Space to start." ; glRasterPos2d ((Game::dx)*NodeSize, (GameCow + 3 * Game::dx) / 2 * NodeSize); glColor3f (TEXT_COLOR); for (int i = 0 ; PrestartInfo[i] != '\0' ; i++) glutBitmapCharacter (INFO_FONT, PrestartInfo[i]); } } ``` 其他的纯文本绘制 ```c++else { if (myGame.status == GAMEOVER) { const char GameOverinfo[30 ] = "Game Over!" ; char Score[30 ], Points[30 ]; sprintf (Points, "%d" , myGame._points); const char * VAO[3 ] = { GameOverinfo,Score,Points }; if (!myGame.PB) { glColor3f (GAMEOVER_TEXT_COLOR); sprintf (Score, "Your score is " ); } else { glColor3f (PB_TEXT_COLOR); sprintf (Score, "WoW!! PB! " ); } for (int i = 0 ; i < 3 ; i++) { glRasterPos2d ((Game::dx * 2 ) * NodeSize, ((GameCow - 4 * Game::dx) / 2 +i*2 *Game::dx) * NodeSize); for (int j = 0 ; VAO[i][j] != '\0' ; j++) glutBitmapCharacter (INFO_FONT, VAO[i][j]); } char Good[40 ]; if (myGame.exZhywyt) { sprintf (Good, "You are great ! You over the zhywyt!!" ); } else { sprintf (Good, "Git Gud." ); } glRasterPos2d ((Game::dx * 2 )* NodeSize, ((GameCow-4 *Game::dx ) / 2 +6 *Game::dx) * NodeSize); for (int j = 0 ;Good[j] != '\0' ; j++) glutBitmapCharacter (INFO_FONT, Good[j]); } else if (myGame.status == GAMEWIN) { const char Info1[30 ] = "You may use something else" , Info2[30 ] = "To do this imposible task." ; const char * str[] = { Info1,Info2 }; glColor3f (RGB (255 , 0 , 255 )); for (int i = 0 ; i < 2 ; i++) { glRasterPos2d ((Game::dx * 2 ) * NodeSize, (GameCow + (i * 3 + 1 ) * Game::dx) / 2 * NodeSize); for (int j = 0 ;str[i][j] != '\0' ; j++) glutBitmapCharacter (INFO_FONT, str[i][j]); } } else { const char helpInfo1[] = "Your Can Press the Space to stop Game." ; const char helpInfo2[] = "And you can click the right butttom to-" ; const char helpInfo21[] = "-open the emun" ; const char helpInfo3[] = "You should use the emnu \"exit\" to exit-" ; const char helpInfo31[] = "-not close the window" ; const char PrestartInfo[30 ] = "Press Space to start." ; const char LuckInfo[] = "Have Good Time!" ; const char * str[] = {PrestartInfo,helpInfo31,helpInfo3,helpInfo21,helpInfo2,helpInfo1 }; glColor3f (TEXT_COLOR); for (int j = 5 ; j >=0 ; j--) { glRasterPos2d ((Game::dx)*NodeSize, ((GameCow + 9 * Game::dx) / 2 -j*2 )* NodeSize); for (int i = 0 ; str[j][i] != '\0' ; i++) glutBitmapCharacter (INFO_FONT, str[j][i]); } glColor3f (PB_TEXT_COLOR); glRasterPos2d ((Game::dx)*NodeSize, ((GameCow + 12 * Game::dx)/2 * NodeSize)); for (int i = 0 ; LuckInfo[i] != '\0' ; i++) glutBitmapCharacter (INFO_FONT, LuckInfo[i]); } } ``` onDisplay ```c++void onDisplay () { glClearColor (BCR_COLOR,1 ); glClear (GL_COLOR_BUFFER_BIT); if (myGame.status!=PRESTART&&myGame.status!=GAMEOVER&&myGame.status!=GAMEWIN) { for (int i = 0 ; i < Row; i++) { bool flag = true ; for (int j = 0 ; j < Cow; j++) { switch (myGame.m_map[i][j]) { case 0 : flag = false ; break ; case Game::LL: glColor3f (LL_COLOR); break ; case Game::OO: glColor3f (OO_COLOR); break ; case Game::DOT: glColor3f (DOT_COLOR); break ; case Game::II: glColor3f (II_COLOR); break ; case Game::TT: glColor3f (TT_COLOR); break ; case Game::WALL: glColor3f (WALL_COLOR); break ; default : glColor3f (ELSE_COLOR); } if (flag) glRectd (i * NodeSize, j * NodeSize, (i + 1 ) * NodeSize, (j + 1 ) * NodeSize); flag = true ; } } switch (myGame.m_tool._type) { case Game::LL: glColor3f (LL_COLOR); break ; case Game::OO: glColor3f (OO_COLOR); break ; case Game::DOT: glColor3f (DOT_COLOR); break ; case Game::II: glColor3f (II_COLOR); break ; case Game::TT: glColor3f (TT_COLOR); break ; default : glColor3f (ELSE_COLOR); } for (int i = 0 ; i < 4 ; i++) { for (int j = 0 ; j < 4 ; j++) { if (myGame.m_tool._data[i][j]) glRectd ((myGame.m_tool._x + i) * NodeSize, (myGame.m_tool._y + j) * NodeSize, (myGame.m_tool._x + i + 1 ) * NodeSize, (myGame.m_tool._y + j + 1 ) * NodeSize); } } switch (myGame.m_next._type) { case Game::LL: glColor3f (LL_COLOR); break ; case Game::OO: glColor3f (OO_COLOR); break ; case Game::DOT: glColor3f (DOT_COLOR); break ; case Game::II: glColor3f (II_COLOR); break ; case Game::TT: glColor3f (TT_COLOR); break ; default : glColor3f (ELSE_COLOR); } for (int i = 0 ; i < 4 ; i++) { for (int j = 0 ; j < 4 ; j++) { if (myGame.m_next._data[i][j]) glRectd ((myGame.m_next._x + i) * NodeSize, (myGame.m_next._y + j) * NodeSize, (myGame.m_next._x + i + 1 ) * NodeSize, (myGame.m_next._y + j + 1 ) * NodeSize); } } const char points[20 ] = "Points:" , PBpoints[20 ] = "PB:" , Diff[20 ] = "Rank:" ,zhywyt[]="zhywyt:" ; char points_num[20 ], PBpoints_num[20 ], Diff_num[20 ],zhywyt_num[20 ]; sprintf (points_num, "%d" , myGame._points); sprintf (PBpoints_num, "%d" , myGame.PB_points); sprintf (Diff_num, "%d" , myGame.Diff); sprintf (zhywyt_num, "%d" , myGame.zhywyt); const char * strinfo[] = { points,PBpoints,Diff,zhywyt }; char * str_num[] = { points_num,PBpoints_num,Diff_num ,zhywyt_num}; if (!myGame.PB) glColor3f (TEXT_COLOR); else glColor3f (PB_TEXT_COLOR); for (int i = 0 ; i < 4 ; i++) { if (i == 3 )glColor3f (PB_TEXT_COLOR); glRasterPos2d (Game::infoPx * NodeSize, (Game::infoPy + 5 * i) * NodeSize); for (int j = 0 ; strinfo[i][j] != '\0' ; j++)glutBitmapCharacter (INFO_FONT, strinfo[i][j]); glRasterPos2d (Game::infoPx * NodeSize, (Game::infoPy + 5 * i + 2 ) * NodeSize); for (int j = 0 ; str_num[i][j] != '\0' ; j++)glutBitmapCharacter (INFO_FONT, str_num[i][j]); } if (myGame.status == STOP) { const char PrestartInfo[30 ] = "Press Space to start." ; glRasterPos2d ((Game::dx)*NodeSize, (GameCow + 3 * Game::dx) / 2 * NodeSize); glColor3f (TEXT_COLOR); for (int i = 0 ; PrestartInfo[i] != '\0' ; i++) glutBitmapCharacter (INFO_FONT, PrestartInfo[i]); } } else { if (myGame.status == GAMEOVER) { const char GameOverinfo[30 ] = "Game Over!" ; char Score[30 ], Points[30 ]; sprintf (Points, "%d" , myGame._points); const char * VAO[3 ] = { GameOverinfo,Score,Points }; if (!myGame.PB) { glColor3f (GAMEOVER_TEXT_COLOR); sprintf (Score, "Your score is " ); } else { glColor3f (PB_TEXT_COLOR); sprintf (Score, "WoW!! PB! " ); } for (int i = 0 ; i < 3 ; i++) { glRasterPos2d ((Game::dx * 2 ) * NodeSize, ((GameCow - 4 * Game::dx) / 2 +i*2 *Game::dx) * NodeSize); for (int j = 0 ; VAO[i][j] != '\0' ; j++) glutBitmapCharacter (INFO_FONT, VAO[i][j]); } char Good[40 ]; if (myGame.exZhywyt) { sprintf (Good, "You are great ! You over the zhywyt!!" ); } else { sprintf (Good, "Git Gud." ); } glRasterPos2d ((Game::dx * 2 )* NodeSize, ((GameCow-4 *Game::dx ) / 2 +6 *Game::dx) * NodeSize); for (int j = 0 ;Good[j] != '\0' ; j++) glutBitmapCharacter (INFO_FONT, Good[j]); } else if (myGame.status == GAMEWIN) { const char Info1[30 ] = "You may use something else" , Info2[30 ] = "To do this imposible task." ; const char * str[] = { Info1,Info2 }; glColor3f (RGB (255 , 0 , 255 )); for (int i = 0 ; i < 2 ; i++) { glRasterPos2d ((Game::dx * 2 ) * NodeSize, (GameCow + (i * 3 + 1 ) * Game::dx) / 2 * NodeSize); for (int j = 0 ;str[i][j] != '\0' ; j++) glutBitmapCharacter (INFO_FONT, str[i][j]); } } else { const char helpInfo1[] = "Your Can Press the Space to stop Game." ; const char helpInfo2[] = "And you can click the right butttom to-" ; const char helpInfo21[] = "-open the emun" ; const char helpInfo3[] = "You should use the emnu \"exit\" to exit-" ; const char helpInfo31[] = "-not close the window" ; const char PrestartInfo[30 ] = "Press Space to start." ; const char LuckInfo[] = "Have Good Time!" ; const char * str[] = {PrestartInfo,helpInfo31,helpInfo3,helpInfo21,helpInfo2,helpInfo1 }; glColor3f (TEXT_COLOR); for (int j = 5 ; j >=0 ; j--) { glRasterPos2d ((Game::dx)*NodeSize, ((GameCow + 9 * Game::dx) / 2 -j*2 )* NodeSize); for (int i = 0 ; str[j][i] != '\0' ; i++) glutBitmapCharacter (INFO_FONT, str[j][i]); } glColor3f (PB_TEXT_COLOR); glRasterPos2d ((Game::dx)*NodeSize, ((GameCow + 12 * Game::dx)/2 * NodeSize)); for (int i = 0 ; LuckInfo[i] != '\0' ; i++) glutBitmapCharacter (INFO_FONT, LuckInfo[i]); } } glutSwapBuffers (); }
如果仔细的读过代码的人会发现,代码里面出现了很多的宏定义,那是为了编程的方便,我们会设置一个Reasource.h文件,来统一管理我们的资源。完整的Reasource.h代码在这里贴出:
Reasource.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #ifndef REASOURCE_H #define REASOURCE_H #define GLUT_KEY_ESCAPE 27 #define RGB(a,b,c) (a/255.0),(b/255.0),(c/255.0) #define BCR_COLOR RGB(0,0,0) #define LL_COLOR RGB(0,255,0) #define OO_COLOR RGB(255,0,0) #define DOT_COLOR RGB(255,255,255) #define II_COLOR RGB(0,255,255) #define TT_COLOR RGB(164, 225, 202) #define ELSE_COLOR RGB(0,0,0) #define WALL_COLOR RGB(100,255,0) #define TEXT_COLOR RGB(255,255,255) #define GAMEOVER_TEXT_COLOR RGB(255,0,0) #define PB_TEXT_COLOR RGB(0,255,0) #define INFO_FONT GLUT_BITMAP_TIMES_ROMAN_24 #endif
4.输入回调函数
终于到了激动人心的输入回调函数了,这里将是用户和窗口进行交互的地方,会处理用户所有的键盘敲击。用户的输入的作用是会随着游戏的状态的改变而改变的。在俄罗斯方块中,用户的输入大概有这些
1.WASD
2.UP DOWN LEFT RIGHT
其实我们知道他们是对应相等的。这里我还添加了第三个输入:
3.SPACE
这个SPACE可以干很多事情,比如在游戏运行的时候可以暂停游戏,在游戏暂停的时候可以恢复游戏,在游戏结束的时候可以重置游戏,在游戏准备阶段可以开始游戏。我个人觉得是非常好的一个设计。好了,知道了输入的信息种类,我们来介绍一下glut的消息处理机制。glut中有两种消息的大类型:普通消息和特殊消息
普通消息处理
普通消息的回调函数的绑定是这样的:
1 glutKeyboardFunc (Input);
其中Input函数的声明式这样的:
1 void Input (unsigned char nchar, int x, int y) ;
从参数列表中可以看到nchar就是消息的信息,它的类型是unsigned char也就是说它所能承载的消息数量是不多的,包括了大部分的键盘输入但是没有功能键和方向键。所以我们还需要一个特殊消息处理回调函数。
特殊消息处理
与普通消息处理不同的是,特殊消息处理中的nchar参数的类型是int 这允许了它可以包含几乎所有的消息。它的绑定回调函数是这样的:
回调函数是这样的:
1 void Input ( int nchar, int x, int y) ;
输入信息
我们来分析这个过程,从游戏开始到结束,分别可以接受的输入有哪些。
1.游戏开始前
只能接受一个SPACE信息,然后进入RUN状态。
1 2 3 4 5 6 if (myGame.status == PRESTART) { if (nchar == GLFW_KEY_SPACE) { myGame.Start (); } }
2.游戏进行中
可以接受移动的信息,以及空格信息进行暂停操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if (myGame.status == RUN) { if (nchar == GLUT_KEY_UP || nchar == GLFW_KEY_W) { myGame.Rotate (); } else if (nchar == GLFW_KEY_S || nchar == GLUT_KEY_DOWN) { myGame.Drop (); } else if (nchar == GLFW_KEY_A || nchar == GLUT_KEY_LEFT) { if (myGame.CangetMoveLeft ()) myGame.m_tool.MoveLeft (); } else if (nchar == GLFW_KEY_D || nchar == GLUT_KEY_RIGHT) { if (myGame.CangetMoveRight ()) myGame.m_tool.MoveRight (); } else if (nchar == GLFW_KEY_SPACE) { if (myGame.status == GAMEOVER) { myGame.status = PRESTART; myGame.reset (); } else if (myGame.status == RUN) { myGame.status = STOP; } } }
3.游戏暂停
很简单,接受一个信息SPACE,转换游戏状态为RUN就行。
1 2 3 4 5 6 if (myGame.status == STOP ) { if (nchar == GLFW_KEY_SPACE) { myGame.status = RUN; } }
4.游戏结束
接受一个SPACE信号,把游戏状态转换为PRESTART。
1 2 3 4 5 6 if (myGame.status == GAMEOVER) { if (nchar==GLFW_KEY_SPACE) myGame.reset (); }
5.游戏胜利
这个比较模糊,大家自由发挥。)
6.特殊的特殊
在任何状态下,我们都可以使用esc退出游戏,我们把这个输入设置在普通消息中,为了让消息处理看上去更加啊的完整,我们在普通消息处理中调用特殊消息处理函数,使用特殊消息处理函数来处理除esc以外的所有消息。
1 2 3 4 5 6 7 8 9 10 void Input (unsigned char nchar, int x, int y) { if (nchar == GLUT_KEY_ESCAPE) { myGame.GameOver (); exit (0 ); } if (nchar >= 'a' && nchar <= 'z' )nchar += 'A' - 'a' ; Input ((int )nchar, x, y); }
普通消息处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 void Input (unsigned char nchar, int x, int y) { if (nchar == GLUT_KEY_ESCAPE) { myGame.GameOver (); exit (0 ); } if (nchar >= 'a' && nchar <= 'z' )nchar += 'A' - 'a' ; Input ((int )nchar, x, y); } ``` 特殊的消息处理函数 ```c++void Input ( int nchar, int x, int y) { if (myGame.status == RUN) { if (nchar == GLUT_KEY_UP || nchar == GLFW_KEY_W) { myGame.Rotate (); } else if (nchar == GLFW_KEY_S || nchar == GLUT_KEY_DOWN) { myGame.Drop (); } else if (nchar == GLFW_KEY_A || nchar == GLUT_KEY_LEFT) { if (myGame.CangetMoveLeft ()) myGame.m_tool.MoveLeft (); } else if (nchar == GLFW_KEY_D || nchar == GLUT_KEY_RIGHT) { if (myGame.CangetMoveRight ()) myGame.m_tool.MoveRight (); } else if (nchar == GLFW_KEY_SPACE) { if (myGame.status == GAMEOVER) { myGame.status = PRESTART; myGame.reset (); } else if (myGame.status == RUN) { myGame.status = STOP; } } } else if (myGame.status == STOP ) { if (nchar == GLFW_KEY_SPACE) { myGame.status = RUN; } } else if (myGame.status == PRESTART) { if (nchar == GLFW_KEY_SPACE) { myGame.Start (); } } else if (myGame.status == GAMEOVER) { if (nchar==GLFW_KEY_SPACE) myGame.reset (); } else if (myGame.status == GAMEWIN) { if (nchar)exit (0 ); } }
这样,我们的消息处理就完整啦!!(快要做完了!!好激动)
菜单(非必要)
创建菜单
做完上面的内容之后,我觉得我的小游戏还是少了些什么东西,它好像不能存档,它好像没有菜单,它好像很垃圾??然后我就给它加上了几行代码,使得这个小游戏 更加的有意思了。我们可以在菜单对游戏进行重置,我们可以在提供的三个存档位中进行选择,我们可以在正常退出时保存数据,下次打开自动保存存档又能回到之前的进度!
首先我们需要做的是创建一个菜单,glut提供的注册函数是glutCreateMenu()
1 extern int APIENTRY glutCreateMenu (void (*)(int )) ;
它会返回所创建的菜单的ID,类型是int。
然后是创建这个ID下的菜单列表,使用函数:glutAddMenuEntry()
1 extern void APIENTRY glutAddMenuEntry (const char *label, int value) ;
需要一个执行操作的函数,和一个对应执行的“值”,这个值我们一般使用枚举来提高代码可读性。
点击返回问题目录
然后我们可以使用一个菜单的ID去创建一个父菜单,像这样:
使用函数glutAddSubMenu()
1 extern void APIENTRY glutAddSubMenu (const char *label, int submenu) ;
点击返回问题目录
最后,我们需要绑定菜单触发的输入,比如GLUT_RIGHT_BUTTON,就是鼠标右键。
响应菜单
在创建菜单的时候,会将子菜单绑定到一个函数上,这个函数需要接受一个value值,然后处理对应的操作。这里先给出createGLUTMenus函数的定义:
createGLUTMenus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void createGLUTMenus () { int menu,submenu, Savemenu, Loadmenu; submenu = glutCreateMenu (processMenuEvents); glutAddMenuEntry ("Start" , RUN); glutAddMenuEntry ("Reset" , PRESTART); Savemenu = glutCreateMenu (processMenuEvents); glutAddMenuEntry ("Save1" , SAVE1); glutAddMenuEntry ("Save2" , SAVE2); glutAddMenuEntry ("Save3" , SAVE3); Loadmenu = glutCreateMenu (processMenuEvents); glutAddMenuEntry ("Load1" , LOAD1); glutAddMenuEntry ("Load2" , LOAD2); glutAddMenuEntry ("Load3" , LOAD3); glutAddMenuEntry ("LoadAutoSave" , LOADAUTOSAVE); menu = glutCreateMenu (processMenuEvents); glutAddMenuEntry ("Stop" , STOP); glutAddSubMenu ("Option" , submenu); glutAddSubMenu ("Save" , Savemenu); glutAddSubMenu ("LoadSave" , Loadmenu); glutAddMenuEntry ("Exit" , GAMEOVER); glutAttachMenu (GLUT_RIGHT_BUTTON); }
然后我们定义在createGLUTMenus中使用过的函数指针processMenuEvents。在这个函数中,我们对应各个枚举进行实现。
processMenuEvents
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void processMenuEvents (int option) { switch (option) { case RUN: if (myGame.status == PRESTART)myGame.Start (); myGame.status = RUN; break ; case GAMEOVER: myGame.GameOver (); myGame.Save ("AutoSave.zhywyt" ); exit (0 ); break ; case PRESTART: myGame.status = PRESTART; myGame.GameOver (); myGame.reset (); break ; case STOP: if (myGame.status!=PRESTART) myGame.status = STOP; break ; } if (option >= SAVE1 && option <= SAVE3) { if (myGame.getStatus () != PRESTART) { int index = option - SAVE1 + 1 ; char FileName[30 ]; sprintf (FileName, "Save%d.zhywyt" , index); string name (FileName) ; if (myGame.Save (name)) myGame.ChangeStatus (STOP); } } else if (option >= LOAD1 && option <= LOAD3) { int index = option - LOAD1 + 1 ; char FileName[30 ]; sprintf (FileName, "Save%d.zhywyt" , index); string name (FileName) ; if (myGame.Load (name)) myGame.ChangeStatus (STOP); else { myGame.reset (); myGame.ChangeStatus (PRESTART); } } else if (option == LOADAUTOSAVE) { if (myGame.Load ("AutoSave.zhywyt" )) myGame.ChangeStatus (STOP); } }
存档(非必要)
前面的菜单里面,有着需要Game类内部实现的存档系统,那么我们来稍微修改一下Game类,为它添加存档的功能。
Save函数和Load函数
1 2 bool Save (string FileName) ;bool Load (string FileName) ;
实现:
Save
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 bool Game::Save (string FileName) { ofstream ofs (FileName, fstream::out) ; if (ofs) { ofs << status << " " << clearTimes << " " << zhywyt << " " << _points << " " << PB_points << " " << Diff << " " << PB_Diff << " " << PB << endl; ofs << m_tool._type << " " << m_next._type << endl; for (int i = 0 ; i < Row; i++) { for (int j = 0 ; j < Cow; j++) ofs << m_map[i][j] << " " ; ofs << endl; } return true ; } return false ; } ``` Load ```c++bool Game::Load (string FileName) { ifstream ifs (FileName, fstream::in) ; if (ifs) { int Status, m_tool_Type, m_next_Type; ifs >> Status >> clearTimes >> zhywyt >> _points >> PB_points >> Diff >> PB_Diff >> PB >> m_tool_Type >> m_next_Type; for (int i = 0 ; i < Row; i++) for (int j = 0 ; j < Cow; j++) ifs >> m_map[i][j]; status = GameMode (Status); m_tool = Tool (rePx,rePy,ToolType (m_tool_Type)); m_next = Tool (waitPx, waitPy, ToolType (m_next_Type)); return true ; } return false ; }
自动存档
在用户是哟esc进行退出时,和使用exit进行退出时,我们先保存一次游戏的数据,再退出,这样可以让用户在下次进入游戏的时候找回之前忘记保存的存档。我们更新之前的普通消息处理函数:
1 2 3 4 5 6 7 8 9 10 11 12 void Input (unsigned char nchar, int x, int y) { if (nchar == GLUT_KEY_ESCAPE) { myGame.GameOver (); myGame.Save ("AutoSave.zhywyt" ); exit (0 ); } if (nchar >= 'a' && nchar <= 'z' )nchar += 'A' - 'a' ; Input ((int )nchar, x, y); }
最后,要注意存档不存在的时候,这里我就没有进行额外的处理了,只是在没有存档的时候不做任何事情。
更新于2023.4.15
音乐
一个游戏不能没有音乐,或者说一个完整的游戏不能没有音乐,所以我还是给我的俄罗斯方块小游戏加上了音乐。
irrklang
因为OpenGL是没有提供音频的接口的,所以我需要使用外部库来实现音乐部分。这里使用的是irrklang音频库,它可以非常方便的使用多种文件的音频。可以在这里找到它的下载链接
irrKlang 我使用了32位进行实现。下载好后按照之前配置glut的方式配置irrklang glut的配置
irrklang使用
irrklang可以实现3D音效,但是我们这里不需要用到这个高级的方法。我们只使用它的2D效果。主要介绍它的两个类:
ISoundEngine类
ISoundEngine类允许我们创建一个可以播放音乐的对象,这个对象由函数createIrrKlangDevice
得到。
1 ISoundEngine* SoundEngine = createIrrKlangDevice ();
ISoundEngine类拥有很多的成员函数,我们需要使用的只有两个,分别是:
1 2 3 4 5 6 7 8 9 10 SoundEngine->play2D (); SoundEngine->drop ();virtual ISound* play2D (const char * soundFileName, bool playLooped = false , bool startPaused = false , bool track = false , E_STREAM_MODE streamMode = ESM_AUTO_DETECT, bool enableSoundEffects = false ) = 0 ;
drop函数很单纯,就是释放这个ISoundEngine对象的内存,销毁该对象。在程序退出前记得调用这个函数;而play2D这个函数使用了大量的默认形参,所以我们甚至可以简洁的写成:SoundEngine->play2D("Filename")
这样简单的代码直接播放一首歌曲。它第二个参数的意义是是否进行循环播放,第三个参数的意义是第一次触发是否暂停播放,等待第二次触发再播放,第四个参数是是否返回ISound指针。后面的参数我们用不到了。我们可以得到两种使用方式,分别对应我们的背景音乐和音效。
1 2 3 4 5 6 ISound*BackGroundSound SoundEngine->play2D ("filename" ,GL_TRUE,GL_FALSE,GL_TRUE); SoundEngine->play2D ("filename" );
然后我们来讲一下ISound类,它可以对一段音乐的播放进行更加细致的操纵。
ISound类
ISound类对象的指针由play2D开启第四个参数时返回,得到的ISound对象可以对这首音乐进行操纵,比如暂停、调节音量、释放空间等等。这里我们只需要用到暂停和释放空间就行了。我们用到它的两个成员函数
1 2 3 4 5 6 7 BackGroundMusic->setIsPaused (GL_FLASE); BackGroundMusic->drop (); RunSound->setVolume (value);
2023.4.15更新至此
点击返回问题目录
一个游戏的开发过程主要有哪些呢?
以下全部都是个人见解,肯定是非常不全面的,但是既然是自己第一个完整的游戏,那么也把整个开发过程的想法说一说。
1.分析游戏对象
想要做一个游戏,首先我们得知道这个游戏会有哪些对象,先从对象入手。把对象作为我们游戏的最小单元,比如游戏中的NPC,我们可能先剥离所有NPC的共性,然后写出一个基类people类,再people这个类的基础上进行继承,形成新的类,逐渐完善所有的对象类。我觉得这个步骤至关重要,也能理解为什么策划在团体中的重要性了。
2.分析游戏过程
在我自己的想法中,我还是不能把Game类和过程剥离开,整个Game类的设计还是在进行一种面向过程的编程方式,但是我觉得面向对象的意义其实就是更好的面向过程,我无法想象如何真正意义上的面向对象?我的绘图,我的消息处理,甚至我的Timer计时器,从里到外都透露着面向过程的思想。但是就这一个俄罗斯方块来说,我觉得我的处理还是很正确的,以友元作为回调函数,设置唯一的Game类。但是转念一想,那我的Game类有什么意义呢?认真的思考过后,我认为:这个游戏中对对象的封装止步于Game类的数据维护,剩下的所有事情都是在面向过程。
3.代码实现
涉及到的知识就很多了,包括C++的编程基础和图形API/库的接口使用。
感谢您的阅读,谢谢!
有什么问题请大家狠狠的指出,大家的意见都是小白在学习中的一次进步!代码多有不规范的地方还请大家包涵。
代码汇总
zhywyt