使用openGL实现吃豆人游戏

实验介绍

这个小项目是参考实验楼的免费项目来做的,因此整个博客基本与其相同。有兴趣的同学可以直接在其官网查找学习即可。我的实验环境也主要用自己的虚拟机来完成,这样也便于保存实验进度。仅为个人学习,绝无抄袭的意思。https://www.shiyanlou.com/courses/running

实验内容

本节实验主要通过 C++ 和 openGL 库来实现了一个吃豆人的小游戏。主要的课程内容涉及到 C++ 相关的序列容器 vector 、deque 和迭代器,以及 openGL 相关的窗口初始化与绘图功能。

知识点

  1. C++编程基础
  2. C++模板库使用
  3. openGL库使用

实验环境

  1. 虚拟机配置ubuntu 16.04
  2. openGL库

代码获取

1
2
3
4
5
//下载源代码
wget http://labfile.oss.aliyuncs.com/courses/1182/Pacman.zip

//解压源代码
unzip -q Pacman.zip

环境准备

安装openGL库

1
2
3
4
5
sudo apt-get update
sudo apt-get install build-essential
sudo apt-get install libgl1-mesa-dev
sudo apt-get install libglu1-mesa-dev
sudo apt-get install freeglut3-dev

编译程序

进入/Pacman/src/目录,make一下

1
make

运行程序

需要进入/Pacman/bin/目录

1
./Pacman

运行效果

image.png

image.png

项目分析

模块拆分

在开始编写程序之前我们需要利用 C++ 的思想把实际问题转换成一个个对象。说到游戏应该就能想到角色、地图和怪物以及操作命令等。我们把这些元素细化就可以得到游戏设计模块:

  1. 角色设计
  2. 地图设计
  3. 怪物设计
  4. 食物设计
  5. 操作设计
  6. 界面设计

开发说明

  1. 编程语言:C++
  2. 开发环境:Linux ubuntu 16.04
  3. 第三方库:openGL

备注:openGL 是一种图形界面处理库,提供了许多 API 给其他语言调用。我们的地图、角色和界面等等都需要调用它。

详细设计

提前准备

在这里首先需要为我们的项目创建一个文件夹,进入 /home/shiyanlou/Code/ 目录下,将我们这次的项目命名为 Pacman。进入这个文件夹创建三个子文件夹 inlcude(存放头文件)、src(存放源代码)和 bin(存放可执行文件)。并创建相应的代码文件,具体内容与结构如下:

image.png

image.png

预定义与全局变量

首先在include文件夹中创建main.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
25
26
27
28
29
30
31
32
33
34
35
36
//main.h
#ifndef _MAIN_H_
#define _MAIN_H_
#include <vector>
#include <GL/glut.h>
#include <iostream>
#include <cstring>
#define _USE_MATH_DEFINES
#include <cmath>
#include <vector>
#include <deque>
#include <cstdlib>
using namespace std;

extern bool replay; //check whether start the game
extern bool over; //check whether the game is over
extern float squareSize; // the size of a square
extern float xIncrement; // the coordinate of x
extern float yIncrement; // the coordinate of y
extern int rotation; // direction
extern float* monster1; // the coordinate and direction of the 1st monster
extern float* monster2; // the coordinate and direction of the 2nd monster
extern float* monster3;
extern float* monster4;
extern vector<int> border; // the coordinate of wall

// 障碍物坐标 (为了清晰分为三部分)
extern vector<int> obstaclesTop;
extern vector<int> obstaclesMiddle;
extern vector<int> obstaclesBottom;
extern deque<float> food;
extern vector<vector<bool>> bitmap; //2d图像,可移动区域
extern bool* keyStates; // 按键状态
extern int points; // 得分

#endif

主函数与窗口

在主函数中对全局变量进行赋值,然后初始化一个窗口。在这里将使用到openGL 的初始化接口。具体包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void glutInit(int* argc,char** argv); //初始化

void glutInitDisplayMode(unsighed int mode); //定义显示方式 mode:是一个 GLUT 库里预定义的可能的布尔组合,使用mode去指定颜色模式,数量和缓冲区类型

void glutInitWindowSize(int width, int height); //设置窗口大小 width:窗口宽度 height:窗口高度

void glutInitWindowPosition(int x, int y); //确定窗口位置(默认左上角)

int glutCreateWindow(char* title); //设置窗口的标题,title:标题内容

void glutDisplayFunc(void(*func) (void)); //注册当前窗口的显示回调函数,void (*func)(void):回调函数名称,在这里我们用的是 display

void glutReshapeFunc(void(*func)(int width, int height)); // 重新设置窗口,void(*func)(int width,int height):回调函数名称,在这里我们用的是 reshape

void glutIdleFunc(void(*func)(void)); //调用渲染函数,void(*func)(int width,int height):回调函数名称,在这里我们用的是 reshape

void glutKeyboardFunc(void(*func)(unsigned char key, int x, int y)); //处理按键事件

void glutKeyboardUpFunc(void(*func)(unsigned char key, int x, int y)); //处理松开按键事件

void glutMainLoop(void); //循环执行

将下面的代码写入 Code/Pacman/src/Pacman.cpp 文件中:

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
//Pacman.cpp
#include "main.h"
#include "control.h"
#include "food.h"
#include "gameresult.h"
#include "gameover.h"
#include "gamestart.h"
#include "init.h"
#include "monster.h"
#include "createpacman.h"
#include "laberynth.h"

using namespace std;

bool replay = false; //检查是否启动游戏
bool over = true; //检查游戏是否结束
float SquareSize = 50.0; //一个单元大小
float xIncrement = 0; // the coordinate of x
float yIncrement = 0; // the coordinate of y
int rotation = 0; // direction
float* monster1 = new float[3] {10.5, 8.5, 1.0}; //第一个怪物的坐标和方向
float* monster2 = new float[3] {13.5, 1.5, 2.0}; //第二个怪物的坐标和方向
float* monster3 = new float[3] {4.5, 6.5, 3.0}; //第三个怪物的坐标和方向
float* monster4 = new float[3] {2.5, 13.5, 4.0}; //第四个怪物的坐标和方向
vector<int> border = { 0, 0, 15, 1, 15, 15, 14, 1, 0, 14, 15, 15, 1, 14, 0, 0 }; //墙坐标

//障碍物坐标 (为了清晰分为三个)
vector<int> obstaclesTop = { 2, 2, 3, 6, 3, 6, 4, 5, 4, 2, 5, 4, 5, 3, 6, 5, 6, 1, 9, 2, 7, 2, 8, 5, 9, 5, 10, 3, 10, 4, 11, 2, 11, 5, 12, 6, 12, 6, 13, 2 };
vector<int> obstaclesMiddle = { 2, 9, 3, 7, 3, 7, 4, 8, 4, 9, 5, 11, 5, 6, 6, 10, 6, 10, 7, 8, 7, 8, 8, 9, 6, 7, 7, 6, 8, 6, 9, 7, 10, 6, 9, 10, 9, 10, 8, 8, 11, 9, 10, 11, 11, 8, 12, 7, 12, 7, 13, 9 };
vector<int> obstaclesBottom = { 2, 10, 3, 13, 3, 13, 4, 12, 5, 12, 6, 13, 6, 13, 7, 11, 8, 11, 9, 13, 9, 13, 10, 12, 11, 12, 12, 13, 12, 13, 13, 10 };
deque<float> food = { 1.5, 1.5, 1.5, 2.5, 1.5, 3.5, 1.5, 4.5, 1.5, 5.5, 1.5, 6.5, 1.5, 7.5, 1.5, 8.5, 1.5, 9.5, 1.5, 10.5, 1.5, 11.5, 1.5, 12.5, 1.5, 13.5, 2.5, 1.5, 2.5, 6.5, 2.5, 9.5, 2.5, 13.5, 3.5, 1.5, 3.5, 2.5, 3.5, 3.5, 3.5, 4.5, 3.5, 6.5, 3.5, 8.5, 3.5, 9.5, 3.5, 10.5, 3.5, 11.5, 3.5, 13.5, 4.5, 1.5, 4.5, 4.5, 4.5, 5.5, 4.5, 6.5, 4.5, 7.5, 4.5, 8.5, 4.5, 11.5, 4.5, 12.5, 4.5, 13.5, 5.5, 1.5, 5.5, 2.5, 5.5, 5.5, 5.5, 10.5, 5.5, 11.5, 5.5, 13.5, 6.5, 2.5, 6.5, 3.5, 6.5, 4.5, 6.5, 5.5, 6.5, 7.5, 6.5, 10.5, 6.5, 13.5, 7.5, 5.5, 7.5, 6.5, 7.5, 7.5, 7.5, 9.5, 7.5, 10.5, 7.5, 11.5, 7.5, 12.5, 7.5, 13.5, 8.5, 2.5, 8.5, 3.5, 8.5, 4.5, 8.5, 5.5, 8.5, 7.5, 8.5, 10.5, 8.5, 13.5, 9.5, 1.5, 9.5, 2.5, 9.5, 5.5, 9.5, 10.5, 9.5, 11.5, 9.5, 13.5, 10.5, 1.5, 10.5, 4.5, 10.5, 5.5, 10.5, 6.5, 10.5, 7.5, 10.5, 8.5, 10.5, 11.5, 10.5, 12.5, 10.5, 13.5, 11.5, 1.5, 11.5, 2.5, 11.5, 3.5, 11.5, 4.5, 11.5, 5.5, 11.5, 6.5, 11.5, 8.5, 11.5, 9.5, 11.5, 10.5, 11.5, 11.5, 11.5, 13.5, 12.5, 1.5, 12.5, 6.5, 12.5, 9.5, 12.5, 13.5, 13.5, 1.5, 13.5, 2.5, 13.5, 3.5, 13.5, 4.5, 13.5, 5.5, 13.5, 6.5, 13.5, 7.5, 13.5, 8.5, 13.5, 9.5, 13.5, 10.5, 13.5, 11.5, 13.5, 12.5, 13.5, 13.5 };
vector<vector<bool>> bitmap; // 2d图像,可移动区域
bool* keyStates = new bool[256]; // 按键记录
int points = 0; // 得分

//主函数
int main(int argc, char** argv){
// 初始化并创建屏幕
glutInit(&argc, argv);
glutDisplayMode(GLUT_DOUBLE | GLUT_RGB);//显示方式(双缓冲区,颜色索引方式)
glutInitWindowSize(750,750);
glutInitWindowPosition(500,50);//窗口起始位置
glutCreateWindow("Pacman - by Desmon");

//定义所有控制功能
glutDisplayFunc(display);//显示窗口
glutReshapeFunc(reshape); //重置窗口
glutIdleFunc(display); //loop
glutKeyboardFunc(keyPressed);
glutKeyboardUpFunc(keyUp);

//运行游戏
init();
glutMainLoop();
return 0;
}

初始化地图

设计了开始界面后,下面就是进入游戏,首先就是显示地图,在这里我们用障碍物填充地图,这样没有障碍物的地方就可以通行了,形成了一个迷宫。

头文件

将下面的代码写入 Code/Pacman/include/init.h 文件中:

1
2
3
4
5
6
7
8
9
/* #ifndef 指示检测指定的预处理器变量是否未定义,
如果未定义,那么跟在后面的所有指示被处理,直到出现#endif;
如果已定义,那么#ifndef测试为假,该指示和#endif指示间的代码都被忽略。*/
//init.h
#ifndef _INIT_H_
#define _INIT_H_
void init(void);

#endif

源代码

OpenGL颜色相关的方法见此博客:https://blog.csdn.net/hebbely/article/details/69951068。

将下面的代码写入 Code/Pacman/src/init.cpp 文件中:

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
//init.cpp
#include "init.h"
#include "main.h"
//初始化游戏
void init(void){
//清除屏幕
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_FLAT);//着色
//重置按键
for (int i = 0; i < 256; i++){
keyStates[i] = false;
}
//用障碍物填充地图
bitmap.push_back({ true, true, true, true, true, true, true, true, true, true, true, true, true, true, true });
bitmap.push_back({ true, false, false, false, false, false, false, false, false, false, false, false, false, false, true });
bitmap.push_back({ true, false, true, true, true, true, false, true, true, false, true, true, true, false, true });
bitmap.push_back({ true, false, false, false, false, true, false, true, false, false, false, false, true, false, true});
bitmap.push_back({ true, false, true, true, false, false, false, false, false, true, true, false, false, false, true});
bitmap.push_back({ true, false, false, true, true, false, true, true, true, true, false, false, true, false, true});
bitmap.push_back({ true, true, false, false, false, false, true, false, true, true, false, true, true, false, true});
bitmap.push_back({ true, true, true, true, true, false, false, false, true, false, false, false, false, false, true});
bitmap.push_back({ true, true, false, false, false, false, true, false, true, true, false, true, true, false, true });
bitmap.push_back({ true, false, false, true, true, false, true, true, true, true, false, false, true, false, true });
bitmap.push_back({ true, false, true, true, false, false, false, false, false, true, true, false, false, false, true });
bitmap.push_back({ true, false, false, false, false, true, false, true, false, false, false, false, true, false, true });
bitmap.push_back({ true, false, true, true, true, true, false, true, true, false, true, true, true, false, true });
bitmap.push_back({ true, false, false, false, false, false, false, false, false, false, false, false, false, false, true });
bitmap.push_back({ true, true, true, true, true, true, true, true, true, true, true, true, true, true, true });
}

迷宫

上面我们已经对地图进行了初始化,也对障碍物进行了初始化,接下来就需要使用 openGL 来绘图了。这里涉及到的接口包括:

void glRectf(GLfloat x1, GLfloat y1, GLfloat x2, GLfloat y2 ); 绘画矩形 x1:矩形左上角横坐标 y1:矩形左上角纵坐标 x2:矩形右下角横坐标 y2:矩形右下角纵坐标

头文件

将下面的代码写入 /Code/Pacman/include/laberynth.h 文件中:

1
2
3
4
5
6
//laberynth.h
#ifndef _LABERYTH_H
#define _LABERYTH_H
void drawLaberynth();

#endif

源代码

将下面的代码写入 Code/Pacman/src/laberynth.cpp 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//laberynth.cpp
#include "laberynth.h"
#include "main.h"
//障碍物与墙体的绘制方法
void drawLaberynth(){
glColor3f(1.0,1.0,1.0); //设置绘图颜色-白色
//边界
for (vector<int>::size_type i = 0; i < border.size(); i = i + 4){
glRectf(border.at(i) * squareSize, border.at(i+1) * squareSize, border.at(i+2) * squareSize, border.at(i+3) * squareSize);
}

//障碍物,分为上中下三部分绘图
for (vector<int>::size_type j = 0; j < obstaclesBottom.size(); j = j + 4){
glRectf(obstaclesBottom.at(j) * squareSize, obstaclesBottom.at(j + 1)*squareSize, obstaclesBottom.at(j + 2)*squareSize, obstaclesBottom.at(j + 3)*squareSize);
for (vector<int>::size_type k = 0; k < obstaclesMiddle.size(); k = k + 4){
glRectf(obstaclesMiddle.at(k) * squareSize, obstaclesMiddle.at(k + 1)*squareSize, obstaclesMiddle.at(k + 2)*squareSize, obstaclesMiddle.at(k + 3)*squareSize);
}
for (vector<int>::size_type p = 0; p < obstaclesTop.size(); p = p + 4){
glRectf(obstaclesTop.at(p) * squareSize, obstaclesTop.at(p + 1)*squareSize, obstaclesTop.at(p + 2)*squareSize, obstaclesTop.at(p + 3)*squareSize);
}

//glRectf函数是绘制矩形
}

游戏角色

在上面已经建立好了地图和迷宫,接下来一步,应该创建一个游戏角色,也就是吃豆人。按照以往的经验,吃豆人可以设计为一个圆形然后拥有一张嘴。需要用到的新接口包括:

1
2
void glBegin(GLenum mode); //表示绘图方式
void glVertex2f(GLfloat x, GLfloat y); //指定画笔位置

image.png

头文件

将下面的代码写入 Code/Pacman/include/createpacman.h 文件中:

1
2
3
4
5
6
7
//createpacman.h
#ifndef _CREATEPACMAN_H_
#define _CREATEPACMAN_H_
#include <GL/glut.h>

void drawPacman(float positionX, float positionY, float rotation);
#endif

源代码

将下面的代码写入 Code/Pacman/src/createpacman.cpp 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//createpacman.cpp
#include "createpacman.h"
#include "GL/gl.h"
#include "main.h"

void drawPacman(float positionX, float positionY, float rotation){
int x, y;
glBegin(GL_LINES);//创建一条线
glColor3f(1.0, 1.0, 0.0); //黄色
for (int k=0; k<32; k++){
x = (float)k / 2.0 * cos((30 + 90*rotation) * M_PI / 180.0) + (positionX*squareSize);
y = (float)k / 2.0* sin((30 + 90 * rotation) * M_PI / 180.0) + (positionY*squareSize);
for (int i = 30; i < 330; i++){
glVertex2f(x, y);
x = (float)k / 2.0 * cos((i + 90 * rotation) * M_PI / 180.0) + (positionX*squareSize);
y = (float)k / 2.0* sin((i + 90 * rotation) * M_PI / 180.0) + (positionY*squareSize);
glVertex2f(x, y);
}
}
glEnd();//绘图结束
}

怪物设计

怪物设计这里我们需要考虑到几点,第一怪物的外形设计、第二怪物的移动设计,第三怪物与障碍物的碰撞检测。

image.png

头文件

将下面的代码写入 Code/Pacman/include/monster.h 文件中:

1
2
3
4
5
6
7
//monster.h
#ifndef _MONSTER_H_
#define _MONSTER_H_
void drawMonster(float positionX, float positionY, float r, float g, float b);
void updateMonster(float* monster, int id);

#endif

源代码

将下面的代码写入 /Code/Pacman/src/monster.cpp 文件中:

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
//monster.cpp
#include "monster.h"
#include "main.h"
//绘画怪物
void drawMonster(float positionX, float positionY, float r, float g, float b){
int x, y;
glBegin(GL_LINES);
glColor3f(r, g, b);
//头
for (int k=0; k<32; k++){
x = (float)k / 2.0 * cos(360 * M_PI / 180.0) + (positionX*squareSize);
y = (float)k / 2.0 * sin(360 * M_PI / 180.0) + (positionY*squareSize);
for (int i=180; i<=360; i++){
glVertex2f(x, y);
x = (float)k / 2.0 * cos(i * M_PI / 180.0) + (positionX*squareSize);
y = (float)k / 2.0* sin(i * M_PI / 180.0) + (positionY*squareSize);
glVertex2f(x, y);
}
}
glEnd();
//身体
glRectf((positionX*squareSize) - 17, positionY*squareSize, (positionX*squareSize) + 15, (positionY*squareSize) + 15);

//眼睛和腿
glBegin(GL_POINTS);
glColor3f(0, 0.2, 0.4);
glVertex2f((positionX*squareSize) - 11, (positionY*squareSize) + 14); //legs
glVertex2f((positionX*squareSize) - 1, (positionY*squareSize) + 14); //legs
glVertex2f((positionX*squareSize) + 8, (positionY*squareSize) + 14); //legs
glVertex2f((positionX*squareSize) + 4, (positionY*squareSize) - 3); //eyes
glVertex2f((positionX*squareSize) - 7, (positionY*squareSize) - 3); //eyes
glEnd();
}

//怪物移动
void updateMonster(float *monster, int id){
// 找到当前位置
int x1Quadrant = (int)((monster[0] - (2/squareSize)) - (16.0 *cos(360 * M_PI / 180.0)) / squareSize);
int x2Quadrant = (int)((monster[0] + (2/squareSize)) + (16.0 *cos(360 * M_PI / 180.0)) / squareSize);
int y1Quadrant = (int)((monster[1] - (2/squareSize)) - (16.0 *cos(360 * M_PI / 180.0)) / squareSize);
int y2Quadrant = (int)((monster[1] + (2/squareSize)) + (16.0 *cos(360 * M_PI / 180.0)) / squareSize);
//怪物移动和撞墙检测
switch ((int)monster[2]){ //不同方向
case 1:
if (!bitmap.at(x1Quadrant).at((int)monster[1])){
monster[0] -= 2/squareSize;
}else {
int current = monster[2];
do{
monster[2] = (rand() % 4) + 1;
} while (current == (int) monster[2]);
}
break;
case 2:
if (!bitmap.at(x2Quadrant).at((int)monster[1])){
monster[0] += 2 / squareSize;
}
else {
int current = monster[2];
do{
monster[2] = (rand() % 4) + 1;
} while (current == (int)monster[2]);
}
break;
case 3:
if (!bitmap.at((int)monster[0]).at(y1Quadrant)){
monster[1] -= 2 / squareSize;
}
else {
int current = monster[2];
do{
monster[2] = (rand() % 4) + 1;
} while (current == (int)monster[2]);
}
break;
case 4:
if (!bitmap.at((int)monster[0]).at(y2Quadrant)){
monster[1] += 2 / squareSize;
}
else {
int current = monster[2];
do{
monster[2] = (rand() % 4) + 1;
} while (current == (int)monster[2]);
}
break;
default:
break;
}
}

食物设计

食物应该设计在可移动区域内,也就是迷宫内,在主函数中我们已经对食物坐标进行了初始化,这里需要进行绘图,另外还需考虑食物被吃的情况。这里用到的新接口包括:

void glPointSize( GLfloat size);点大小设置

头文件

将下面的代码写入 /Code/Pacman/include/food.h 文件中:

1
2
3
4
5
6
7
8
9
//food.h
#ifndef _FOOD_H_
#define _FOOD_H_
#include <deque>

bool foodEaten(int x, int y, float pacmanX, float pacmanY);
void drawFood(float pacmanX, float pacmanY);

#endif

源代码

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
//food.cpp
#inclue "food.h"
#inclue "main.h"

//检查食物是否被吃
bool foodEaten(int x, int y, float pacmanX, float pacmamY){
if (x >= pacmanX - 16.0 *cos(359 * M_PI / 180.0) && x <= pacmanX + 16.0*cos(359 * M_PI / 180.0)){
if (y >= pacmanY - 16.0*cos(359 * M_PI / 180.0) && y <= pacmanY + 16.0*cos(359 * M_PI / 180.0)){
return true;
}
}
return false;
}

//画上食物
void drawFood(float pacmanX, float pacmanY){
deque<float> temp;
//检查食物是否没有被吃掉
for (deque<float>::size_type i = 0; i < food.size(); i = i + 2){
if (!foodEaten(food.at(i)*squareSize, food.at(i + 1)*squareSize, pacmanX, pacmanY)){
temp.push_back(food.at(i)); //没有被吃掉
temp.push_back(food.at(i + 1));
}
else {
points++; //食物被吃掉,分数+1
}
}
food.swap(temp);
glPointSize(5.0);
glBegin(GL_POINTS);
glColor3f(1.0, 1.0, 1.0);
//画上食物
for (deque<float>::size_type j = 0; j < food.size(); j = j + 2){
glVertex2f(food.at(j)*squareSize, food.at(j + 1)*squareSize);//画点
}
glEnd();
}

角色移动命令

现在已经设计好了迷宫、食物和怪物,接下来就可以做角色的控制设计了,在这里我们只设计了角色的上下左右移动。

头文件

将下面的代码写入 Code/Pacman/include/control.h 文件中:

1
2
3
4
5
6
7
8
9
//control.h
#ifndef _CONTROL_H_
#define _CONTROL_H_
void keyPressed(unsigned char key, int x, int y);
void keyUp(unsigned char key, int x, int y);
void resetGame();
void keyOperations();

#endif

源代码

将下面的代码写入 Code/Pacman/src/control.cpp 文件中:

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
//control.cpp
#include "control.h"
#include "main.h"
//设置按键
void keyPressed(unsigned char key, int x, int y){
keyStates[key] = true;
}

//释放按键
void keyUp(unsigned char key, int x, int y){
keyStates[key] = false;
}

//重置所有元素并开始游戏
void resetGame(){
over = false;
xIncrement = 0;
yIncrement = 0;
rotation = 0;
monster1 = new float[3] {10.5, 8.5, 1.0};
monster2 = new float[3] {13.5, 1.5, 2.0};
monster3 = new float[3] {4.5, 6.5, 3.0};
monster4 = new float[3] {2.5, 13.5, 4.0};
points = 0;
for (int i = 0; i < 256; i++){
keyStates[i] = false;
}
food = { 1.5, 1.5, 1.5, 2.5, 1.5, 3.5, 1.5, 4.5, 1.5, 5.5, 1.5, 6.5, 1.5, 7.5, 1.5, 8.5, 1.5, 9.5, 1.5, 10.5, 1.5, 11.5, 1.5, 12.5, 1.5, 13.5, 2.5, 1.5, 2.5, 6.5, 2.5, 9.5, 2.5, 13.5, 3.5, 1.5, 3.5, 2.5, 3.5, 3.5, 3.5, 4.5, 3.5, 6.5, 3.5, 8.5, 3.5, 9.5, 3.5, 10.5, 3.5, 11.5, 3.5, 13.5, 4.5, 1.5, 4.5, 4.5, 4.5, 5.5, 4.5, 6.5, 4.5, 7.5, 4.5, 8.5, 4.5, 11.5, 4.5, 12.5, 4.5, 13.5, 5.5, 1.5, 5.5, 2.5, 5.5, 5.5, 5.5, 10.5, 5.5, 11.5, 5.5, 13.5, 6.5, 2.5, 6.5, 3.5, 6.5, 4.5, 6.5, 5.5, 6.5, 7.5, 6.5, 10.5, 6.5, 13.5, 7.5, 5.5, 7.5, 6.5, 7.5, 7.5, 7.5, 9.5, 7.5, 10.5, 7.5, 11.5, 7.5, 12.5, 7.5, 13.5, 8.5, 2.5, 8.5, 3.5, 8.5, 4.5, 8.5, 5.5, 8.5, 7.5, 8.5, 10.5, 8.5, 13.5, 9.5, 1.5, 9.5, 2.5, 9.5, 5.5, 9.5, 10.5, 9.5, 11.5, 9.5, 13.5, 10.5, 1.5, 10.5, 4.5, 10.5, 5.5, 10.5, 6.5, 10.5, 7.5, 10.5, 8.5, 10.5, 11.5, 10.5, 12.5, 10.5, 13.5, 11.5, 1.5, 11.5, 2.5, 11.5, 3.5, 11.5, 4.5, 11.5, 5.5, 11.5, 6.5, 11.5, 8.5, 11.5, 9.5, 11.5, 10.5, 11.5, 11.5, 11.5, 13.5, 12.5, 1.5, 12.5, 6.5, 12.5, 9.5, 12.5, 13.5, 13.5, 1.5, 13.5, 2.5, 13.5, 3.5, 13.5, 4.5, 13.5, 5.5, 13.5, 6.5, 13.5, 7.5, 13.5, 8.5, 13.5, 9.5, 13.5, 10.5, 13.5, 11.5, 13.5, 12.5, 13.5, 13.5 };
}

//控制吃豆人移动
void keyOperations(){
// 获得当前位置
float x = (1.5 + xIncrement) * squareSize;
float y = (1.5 + yIncrement) * squareSize;
// 更新按键
if (keyStates['a']){ //往左
x -= 2;
int x1Quadrant = (int)((x - 16.0 *cos(360 * M_PI / 180.0)) / squareSize);
if (!bitmap.at(x1Quadrant).at((int)y/squareSize)){
xIncrement -= 2 / squareSize;
rotation = 2;
}
}
if (keyStates['d']){ //往右
x += 2;
int x2Quadrant = (int)((x + 16.0 *cos(360 * M_PI / 180.0)) / squareSize);
if (!bitmap.at(x2Quadrant).at((int)y / squareSize)){
xIncrement += 2 / squareSize;
rotation = 0;
}
}
if (keyStates['w']){ //往上
y -= 2;
int y1Quadrant = (int)((y - 16.0 *cos(360 * M_PI / 180.0)) / squareSize);
if (!bitmap.at((int)x/squareSize).at(y1Quadrant)){
yIncrement -= 2 / squareSize;
rotation = 3;
}
}
if (keyStates['s']){ //往下
y += 2;
int y2Quadrant = (int)((y + 16.0 *cos(360 * M_PI / 180.0)) / squareSize);
if (!bitmap.at((int)x / squareSize).at(y2Quadrant)){
yIncrement += 2 / squareSize;
rotation = 1;
}
}
if (keyStates[' ']){ //开始游戏或重新游戏
if (!replay && over){
resetGame();
replay = true;
}
else if (replay && over){
replay = false;
}
}
}

开始界面

接着要准备一个欢迎界面,同时对游戏具体操作方式做一个声明,然后显示游戏元素。这里我们需要用到 openGL 的绘图接口。具体包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void glClearColor(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); //红、绿、蓝和 alpha 值,指定值范围均为[ 0.0f,1.0f ]

void glcolor3f(GLfloat red,GLfloat green,GLfloat blue); //设置画笔颜色

void glRasterPos2f( GLfloat x, GLfloat y); //要显示字体的起始坐标

void glutBitmapCharacter(void *font, int character); //渲染字符 font:字体类型 character:具体字符

void glutSwapBuffers(void); //刷新

void glMatrixMode(GLenum mode); //投影方式 GLenum mode:投影方式,有3种模式: GL_PROJECTION 投影, GL_MODELVIEW 模型视图, GL_TEXTURE 纹理.

void glLoadIdentity(void); //恢复初始坐标系

void glViewport(GLint x,GLint y,GLsizei width,GLsizei height); //定义窗口 x:起始横坐标 y:起始纵坐标 width:宽度 height:高度

void glOrtho(GLdouble left,GLdouble right,GLdouble bottom,GLdouble top,GLdouble near,GLdouble far); //设置或修改修剪空间的范围.这个函数的操作是创建一个正射投影矩阵,并且用这个矩阵乘以当前矩阵。其中近裁剪平面是一个矩形,矩形左下角点三维空间坐标是(left,bottom,-near),右上角点是(right,top,-near);远裁剪平面也是一个矩形,左下角点空间坐标是(left,bottom,-far),右上角点是(right,top,-far)

头文件

将下面的代码写入 Code/Pacman/include/gamestart.h 文件中:

1
2
3
4
5
6
7
8
9
//gamestart.h
#ifndef _GAMESTART_H_
#define _GAMESTART_H_
#include <iterator>

void welcomeScreen();
void display();
void reshape(int w, int h);
#endif

源代码

将下面的代码写入 Code/Pacman/src/gamestart.cpp 文件中:

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
//gamestart.cpp
#include "gamestart.h"
#include "monster.h"
#include "createpacman.h"
#include "laberynth.h"
#include "main.h"
#include "gameover.h"
#include "food.h"
#include "gameresult.h"
#include "control.h"

//欢迎界面
void welcomeScreen(){
glClearColor(0, 0.2, 0.4, 1.0);
string message = "*************************************";
string::iterator it = message.begin();
glRasterPos2f(150, 200);
while (it != message.end()){
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
}

message = "Pacman - by Desmon";
glColor3f(1,1,1); //white
glRasterPos2f(225, 250);
it = message.begin();
while (it != message.end()){
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
}

message = "*************************************";
glRasterPos2f(150, 300);
it = message.begin();
while (it != message.end()){
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
}

message = "To control Pacman use A to go right, D to go left, W to go up and S to go down.";
glRasterPos2f(50, 400);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *it++);

message = "To start or restart the game, press the space key.";
glRasterPos2f(170, 450);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *it++);
}

//显示屏幕和元素
void display(){
if (points == 1){
over = false;
}
keyOperations();
glClear(GL_COLOR_BUFFER_BIT); //清除颜色缓冲区(当前被激活为写操作的颜色缓存)
gameOver();
if (replay){
if (!over){
drawLaberynth();
drawFood((1.5 + xIncrement) * squareSize, (1.5 + yIncrement) * squareSize);
drawPacman(1.5 + xIncrement, 1.5 + yIncrement, rotation);
updateMonster(monster1, 1);
updateMonster(monster2, 2);
updateMonster(monster3, 3);
updateMonster(monster4, 4);
drawMonster(monster1[0], monster1[1], 0.0, 1.0, 1.0); //cyan
drawMonster(monster2[0], monster2[1], 1.0, 0.0, 0.0); //red
drawMonster(monster3[0], monster3[1], 1.0, 0.0, 0.6); //magenta
drawMonster(monster4[0], monster4[1], 1.0, 0.3, 0.0); //orange
}
else {
resultsDisplay();
}
}
else {
welcomeScreen();
}
glutSwapBuffers();
}

//重置窗口
void reshape(int w, int h){
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glViewport(0, 0, (GLsizei)w, (GLsizei)h);
glOrtho(0, 750, 750, 0, -1.0, 1.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}

游戏结果判断

到此游戏元素已经基本设计完成,这里我们给游戏设计一个结束判断模块,应该包括胜利条件和失败条件。

头文件

将下面的代码写入 /Code/Pacman/include/gameover.h 文件中:

1
2
3
4
5
6
//gameover.h
#ifndef _GAME_OVER_H_
#define _GAME_OVER_H_
void gameOver();

#endif

源代码

将下面的代码写入 Code/Pacman/src/gameover.cpp 文件中:

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
//gameover.cpp
#include "gameover.h"
#include "main.h"
//游戏结束
void gameOver(){
int pacmanX = (int)(1.5 + xIncrement);
int pacmanY = (int)(1.5 + yIncrement);
int monster1X = (int)(monster1[0]);
int monster1Y = (int)(monster1[1]);
int monster2X = (int)(monster2[0]);
int monster2Y = (int)(monster2[1]);
int monster3X = (int)(monster3[0]);
int monster3Y = (int)(monster3[1]);
int monster4X = (int)(monster4[0]);
int monster4Y = (int)(monster4[1]);
if (pacmanX == monster1X && pacmanY == monster1Y){
over = true;
}
if (pacmanX == monster2X && pacmanY == monster2Y){
over = true;
}
if (pacmanX == monster3X && pacmanY == monster3Y){
over = true;
}
if (pacmanX == monster4X && pacmanY == monster4Y){
over = true;
}
if (points == 106){
over = true;
}
}

游戏结果界面

到这里已经是尾声了,我们的游戏也已经结束了。在结构界面除了得分以外,我们可以人性化的提供一些提示来进行接下来的操作。

头文件

将下面的代码写入 Code/Pacman/include/gameresult.h 文件中:

1
2
3
4
5
6
7
8
//gameresult.h
#ifndef _GAMERESULT_H_
#define _GAMERESULT_H_
#include <cstring>
#include <iterator>
void resultsDisplay();

#endif

源代码

将下面的代码写入 Code/Pacman/src/gameresult.cpp 文件中:

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
//gameresult.cpp
#include "gameresult.h"
#include "main.h"
//游戏结果
void resultsDisplay(){
if (points == 106){
//胜利
string message = "*************************************";
string::iterator it = message.begin();
glRasterPos2f(170, 250);
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "CONGRATULATIONS, YOU WON! ";
glColor3f(1, 1, 1);
glRasterPos2f(200, 300);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "*************************************";
glRasterPos2f(170, 350);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "To start or restart the game, press the space key.";
glRasterPos2f(170, 550);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *it++);
}else {
//Lost
string message = "*************************************";
string::iterator it = message.begin();
glRasterPos2f(210, 250);
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "SORRY, YOU LOST ... ";
glColor3f(1, 1, 1);
glRasterPos2f(250, 300);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "*************************";
glRasterPos2f(210, 350);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "You got: ";
glRasterPos2f(260, 400);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
string result = to_string(points);
message = (char*)result.c_str();
glRasterPos2f(350, 400);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = " points!";
glRasterPos2f(385, 400);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_TIMES_ROMAN_24, *it++);
message = "To start or restart the game, press the space key.";
glRasterPos2f(170, 550);
it = message.begin();
while (it!=message.end())
glutBitmapCharacter(GLUT_BITMAP_HELVETICA_18, *it++);
}
}

Makefile(着重学习)

最后一步就是对源代码进行编译了,这里有两种方式,看个人喜好来选择。

  1. g++方式,这种方式好处是简单明了,弊端是每次编译都需要敲入有些麻烦。首先在终端进入Pacman/src/目录
1
g++ *.cpp -std=c++11 -Wall -I../include -lglut -lGL -o ../bin/
  1. Makefile 方式,首先在 Pacman/src/ 目录下建立 Makefile 文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #编译方式
    CC = g++
    #C++版本,显示所有警告
    VERSION = -g -std=c++11 -Wall
    #头文件和库文件
    INCLUDE = -I../include -lglut -lGL
    #目标文件,最后生成文件
    TARGET = ../bin/Pacman
    #源代码路径
    SRCS = $(wildcard *.cpp)
    #编译为.o文件
    OBJS = $(patsubst %cpp,%o,$(SRCS))

    all:$(TARGET)
    #执行编译
    $(TARGET):$(OBJS)
    $(CC) $(OBJS) $(VERSION) $(INCLUDE) -o $(TARGET)

    %.o:%.cpp
    $(CC) $(VERSION) $(INCLUDE) -c $<
    #清除
    .PHONY:clean
    clean:
    rm -rf $(OBJS) $(TARGET)