基于Glut的俄罗斯方块开发

# 基于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类的数据成员

首先我们来分析一下俄罗斯方块这个游戏的场景和运行逻辑:
image
这里分为了三个方块单位,和一些文字单位,可以看到,蓝色的俄罗斯方块正在随着时间往下走,白色的预备方块静止在等待区域,那么我们需要两个俄罗斯方块对象来存储他们的信息。再看下面的绿色 L 形状的俄罗斯方块,它已经到底了,和场景融为了一体,那么我们也就不需要一个俄罗斯方块的类来存储它了,可以把它存储在场景中,我们用一个二维向量来存储这个场景的信息。

方块成员

image
Tool类我们稍后再讲,它会是存储一个俄罗斯方块的工具类。然后我们使用vector<vector<int>>来存储这个场景的所有信息。(李卫明老师的教程中使用了动态分配的数组。)

信息成员(分数,等级)

image
分数、最高分和等级我们使用三个无符号整型来存储。等级是难度等级,可以用于我们之后的提高难度。

Tool类

好了,现在我们来实现最基础的Tool类,Tool类是一个俄罗斯方块对象,它有着这些属性:
**
1.记录了方俄罗斯方块的类型
2.记录了俄罗斯方块的形状
3.记录了俄罗斯方块的位置
4.记录是否有过移动(用于判断游戏结束)
5.可以进行移动、旋转
**

点击返回问题目录

1.俄罗斯方块的类型

我们使用一个枚举类型,来方便自己绘制俄罗斯方块,枚举中的名字为俄罗斯方块的类型,还可以作为绘制方块时的颜色的枚举使用,如下:
image
其中 WALL是提供给场景使用的一个枚举常量

2.俄罗斯方块的形状

每一个俄罗斯方块都可以认为被一个4 * 4的网格包裹住了,所以我们可以使用一个4 * 4的二维数组来记录它在地图中所占的位置:

3.俄罗斯方块的位置

我们记录一个4 * 4的网格的位置,其实只需要这个网格左上角的方块的坐标就可以了,也就是说我们记录俄罗斯方块的位置,其实只需要记录一个点的x、y坐标。

4.是否移动过

很简单,使用一个bool类型的变量即可,那么我们的Tool类的数据成员看起来像这样:
image

点击返回问题目录

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的构造函数。我们需要自己定义两个构造函数,一个是默认的构造函数,一个是接受三个参数的构造函数,函数原型如下:
image
它接受了一个坐标和一个类型作为参数,对对象进行初始化。要记得对所有的数据成员进行初始化哦,保持好习惯~

带参的构造函数

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类的数据维护函数(私有)

NextTool

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;
}
}
//可以消除第 i 行
if (flag) {
//把 i 行上面的所有行向下移动
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() {
//剪枝,如果是中心对称的图形,直接返回true,代表以及完成了旋转。
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;
//如果没有冲突,直接交换revTool和m_tool,这里和移动赋值的理念有点相似,
//因为我的m_tool已经不需要了,直接让revTool来进行析构。
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;
//next Toll右边的横杆
for (int i = 0; i < 4; i++)m_map[2 * dx + GameRow][dx + i] = WALL;
}

构造函数不仅要初始化所有数据、设置游戏状态外,还需要将场景的WALL部分填充,dx是设置的一个静态整形变量,用于调整边界的厚度。Row和Cow是屏幕的大小;GameRow和GameCow就是游戏区域了,这些是整形常量,这个图可以说明这一关系:
image
这里是我的初始化

1
2
3
4
5
6
const int mapScre = 1;						//场景缩放
const int NodeSize = 20; //方形边长
const int Cow = 30 * mapScre; //窗口行数 y
const int Row = 24 * mapScre; //列数 x
const int GameCow = 30 * mapScre; //游戏场景行数
const int GameRow = 16 * mapScre; //列数
1
2
3
4
5
//Game::public
static const int dx, dy; //用于弥补场景边框距离
static const int rePx, rePy; //操纵的Tool的初始位置
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包管理器

image
点击浏览->搜索glfw->下载
image
下载到当前项目就行了。我们需要的头文件有这么两个:

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[])
{
// 初始化 glut
glutInit(&argc, argv);
// 设置 OpenGL 显示模式(双缓存, RGB 颜色模式)
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
// 设置窗口初始尺寸
glutInitWindowSize(Row*NodeSize, Cow*NodeSize);
// 设置窗口初始位置
glutInitWindowPosition(100, 100);
// 设置窗口标题
glutCreateWindow("Terix");
// 进入 glut 事件循环
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.窗口尺寸变化回调函数

这个函数允许我们的窗口在大小改变时,不会出现图像拉扯的情况,设置了这个回调函数之后的效果是这样的:
image
拖动窗口,使其尺寸发生变化,会得到这样的结果:
image
不仅如此,我们的投影也发生在这个函数里面,投影的原理我就不讲了,这涉及到一些图形学的内容,有兴趣的可以去看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();
}

// In main
------------
// 设置窗口尺寸变化回调函数
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();
//It is imposible!!!
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
//in OnDisplay()
// 设置清屏颜色
glClearColor(BCR_COLOR,1);
// 用指定颜色清除帧缓存
glClear(GL_COLOR_BUFFER_BIT);
/*
绘制操作
*/
//Clear Buffer
glutSwapBuffers();

因为我们采用了双缓存的模式,所以以交换缓存为绘制结束。这样我们就不会出现频闪的问题。这里我也可以解释一下频闪出现的原因,频闪的出现不是因为电脑性能不行,而是你在画图的时候给屏幕发的绘制指令数太多了,应该先把一帧的画图操作全部写在缓存中,然后一次性发给屏幕绘制。

让我们回到俄罗斯方块的游戏场景,这次我们分游戏状态来处理:

RUN

image
这里我们需要绘制的信息有:两个俄罗斯方块、场景方块、游戏信息。

STOP

image
STOP很简单,我们只需要在RUN的基础上加上一句暂停的提示语就行了。

GAMEOVER

image
为了让GAMEOVER的时候,玩家可以感受到一种视觉冲击,所以我设置为了只绘制失败信息,并且如果分数没有超过作者的话,会奉上一句Git Gud,以表示敬意

PRESTART

image
这个就是游戏开始之前的画面,需要绘制一些指导的信息,比如如何开始游戏、游戏的一些快捷操作、游戏的自动保存机制等等。

实现

从上面的总结来看,不难发现:只有两个状态需要绘制场景和俄罗斯方块:

如何绘制一个方块?

在正式的绘制信息之前,我们先来介绍一下如何绘制一个正方形。在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
	//Draw Normal Info
if (myGame.status!=PRESTART&&myGame.status!=GAMEOVER&&myGame.status!=GAMEWIN) {
//Draw BackGroud
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;
}
}

//Draw Active Terix
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);
}
}
//Draw Waiting Terix
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);
}
}
//Draw Info
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]);
}
//Draw Stop Info
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) {
//Draw gameover info
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) {
//It is imposible!!!!!
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);

//Draw Normal Info
if (myGame.status!=PRESTART&&myGame.status!=GAMEOVER&&myGame.status!=GAMEWIN) {
//Draw BackGroud
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;
}
}

//Draw Active Terix
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);
}
}
//Draw Waiting Terix
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);
}
}
//Draw Info
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]);
}
//Draw Stop Info
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]);
}
}
//Draw Prestart info
else {
if (myGame.status == GAMEOVER) {
//Draw gameover info
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) {
//It is imposible!!!!!
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]);
}
}
//Clear Buffer
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) //用于255转0.0-1.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
glutSpecialFunc(Input);

回调函数是这样的:

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的Menu

首先我们需要做的是创建一个菜单,glut提供的注册函数是glutCreateMenu()


1
extern int APIENTRY glutCreateMenu(void (*)(int));

它会返回所创建的菜单的ID,类型是int。
然后是创建这个ID下的菜单列表,使用函数:glutAddMenuEntry()


1
extern void APIENTRY glutAddMenuEntry(const char *label, int value);

需要一个执行操作的函数,和一个对应执行的“值”,这个值我们一般使用枚举来提高代码可读性。

点击返回问题目录
然后我们可以使用一个菜单的ID去创建一个父菜单,像这样:
image
使用函数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() {
//菜单ID
int menu,submenu, Savemenu, Loadmenu;
//设置回调函数processMenuEvents(),获取与之对于的菜单ID
submenu = glutCreateMenu(processMenuEvents);
glutAddMenuEntry("Start", RUN);
glutAddMenuEntry("Reset", PRESTART);
//以后再做
//glutAddMenuEntry("Check My Best", CHECKBEST);
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的方式配置irrklangglut的配置

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
//BackGroundMusic背景音乐
//需要持续播放且进行"track"的的音乐
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
//开启音乐播放
//TRUE暂停
BackGroundMusic->setIsPaused(GL_FLASE);
//销毁音乐,之后置空
BackGroundMusic->drop();
//设置音量(0.0 - 1.0)
RunSound->setVolume(value);

2023.4.15更新至此

点击返回问题目录

一个游戏的开发过程主要有哪些呢?

以下全部都是个人见解,肯定是非常不全面的,但是既然是自己第一个完整的游戏,那么也把整个开发过程的想法说一说。

1.分析游戏对象

想要做一个游戏,首先我们得知道这个游戏会有哪些对象,先从对象入手。把对象作为我们游戏的最小单元,比如游戏中的NPC,我们可能先剥离所有NPC的共性,然后写出一个基类people类,再people这个类的基础上进行继承,形成新的类,逐渐完善所有的对象类。我觉得这个步骤至关重要,也能理解为什么策划在团体中的重要性了。

2.分析游戏过程

在我自己的想法中,我还是不能把Game类和过程剥离开,整个Game类的设计还是在进行一种面向过程的编程方式,但是我觉得面向对象的意义其实就是更好的面向过程,我无法想象如何真正意义上的面向对象?我的绘图,我的消息处理,甚至我的Timer计时器,从里到外都透露着面向过程的思想。但是就这一个俄罗斯方块来说,我觉得我的处理还是很正确的,以友元作为回调函数,设置唯一的Game类。但是转念一想,那我的Game类有什么意义呢?认真的思考过后,我认为:这个游戏中对对象的封装止步于Game类的数据维护,剩下的所有事情都是在面向过程。

3.代码实现

涉及到的知识就很多了,包括C++的编程基础和图形API/库的接口使用。

感谢您的阅读,谢谢!

有什么问题请大家狠狠的指出,大家的意见都是小白在学习中的一次进步!代码多有不规范的地方还请大家包涵。

代码汇总

zhywyt


基于Glut的俄罗斯方块开发
http://hexo.zhywyt.me/posts/15450/
作者
zhywyt
发布于
2023年4月9日
更新于
2024年10月22日
许可协议