PAOGD个人作业4——利用OpenGL设计贪吃蛇游戏

中山大学数据科学与计算机学院本科生实验报告

(2019年春季学期)

课程名称PAOGD任课老师郑贵锋
年级16专业(方向)软件工程(计算机应用方向)
学号16340132姓名梁颖霖
电话13680473185Emaildic0k@qq.com
开始日期2019/5/20完成日期2019/5/24

一、实验题目

HW4 利用OpenGL设计贪吃蛇游戏

二、实现内容

任务介绍
  • 贪吃蛇游戏:玩家控制贪吃蛇在游戏区域里驰骋,避免碰到自己或障碍物,尽可能地吃更多的食物以生长!
游戏玩法:
  • WASD控制蛇的移动
  • 游戏开始,每隔一定时间会在地图空闲位置刷新一个食物,蛇触碰到食物后食物消失,蛇会增加一个单位的长度
  • 当蛇触碰到自己或者障碍物,则游戏失败
  • 当蛇接触到地图边界,蛇会在地图另一端重新进入地图
开发环境
  • OpenGL3
  • GLFW
  • GLAD
要求:
  1. 完成贪吃蛇游戏的框架搭建(60%)
  2. 完成蛇以及食物的 3D 精灵加载和渲染,其中模型可以用简单的纯色几何模型实现如立方体、球体;或者网上下载合适的3D模型如Apple-PolySnake-Poly(20%)
  3. 实现蛇的控制(20%)
  4. Bonus:蛇的碰撞检测与响应
参考资料
  1. OpenGL游戏-框架设计
  2. OpenGL游戏-精灵渲染
  3. OpenGL游戏-碰撞检测
  4. OpenGL模型创建
  5. Google Poly-3D模型库

三、实验结果

1. 蛇的身体组成基类Block

这里主要是被Snake类所调用,本质是建立一个正方体,里面可以设置这个正方体的大小,颜色,并且最重要要定义draw函数,当snake的draw被调用的时候,实际上就是再调用block的draw。

这里这给出头文件的函数定义,其实现也比较简单

#ifndef BLOCK
#define BLOCK
#include <GL/glut.h>
#include "../sys.h"

class Block { 
public:
	Block(float pX, float pY, float pZ, float pSize = 1.0f);
	~Block();
	void setX(float pX);
	void setY(float pY);
	void setZ(float pZ);
	float getX();
	float getY();
	float getZ();
	void setColor(float pR, float pG, float pB); 
	void draw();

private:
	float x, y, z, size, r, g, b;
};

#endif

其中的draw函数

//function to draw the block
void Block::draw() { 
	glPushMatrix();
	glTranslatef(-x, y, z);
	if (r > 1 || g > 1 || b > 1) {
		glColor3ub(r, g, b);
	}
	else {
		glColor3f(r, g, b);
	}
	glCallList(cube);
	glPopMatrix();
}
2. 蛇的基类Snake

蛇所要做的操作,包括对位置的判断,是否更新,如何增加身体的长度。还需要一个draw函数,描绘蛇的形状。

class Snake { //contains the logic for the snake movement and collision
public:
	std::vector<Block*> blocks; //snake is just an array of blocks

	Snake(float startX, float startZ, int blockCount);
	~Snake();
	void draw();
    // 移动
	void move(Direction direction);
    // 添加蛇的身体长度
	void pushSnake();
    // 更新
	void update(Fruit* fruit, Stone* stones[]);
    // 碰撞检测,检查是否吃食物,或者碰到石头
	void collisionDetection(Fruit* fruit, Stone* stones[]);
	void addBlock();
	Direction getDirection();

private:
	Direction currentDirection;
	bool isUpdated;
	int score;
};

先说draw函数,调用的是蛇的身体block的draw函数

void Snake::draw() { 
	for (int i = 0; i <= blocks.size() - 1; i++) {
		Block* temp = blocks.at(i);
		temp->draw();
	}
}

蛇的移动,需要判断当前的方向,以及玩家操纵的方向。

判断完成后,改变蛇头的位置,并将蛇的身体附加到蛇头的后面

void Snake::move(Direction direction) { 
	Block* snakeHead = blocks.at(0);
	if (direction == D_LEFT && currentDirection != D_RIGHT) {
		pushSnake();
		snakeHead->setX(snakeHead->getX() - 1.0f);
		currentDirection = direction;
		isUpdated = true;
	}
	else if (direction == D_RIGHT && currentDirection != D_LEFT) {
		pushSnake();
		snakeHead->setX(snakeHead->getX() + 1.0f);
		currentDirection = direction;
		isUpdated = true;
	}
	else if (direction == D_UP && currentDirection != D_DOWN) {
		pushSnake();
		snakeHead->setZ(snakeHead->getZ() + 1.0f);
		currentDirection = direction;
		isUpdated = true;
	}
	else if (direction == D_DOWN && currentDirection != D_UP) {
		pushSnake();
		snakeHead->setZ(snakeHead->getZ() - 1.0f);
		currentDirection = direction;
		isUpdated = true;
	}
};

完成上述函数就可以实现蛇的移动

下面要做的是碰撞检测,检测蛇头的位置是否为食物,或者建筑物。并且还要对蛇头是否到达边缘进行判断,实现穿越边界从另一边出来的功能需求。

void Snake::collisionDetection(Fruit* fruit, Stone* stones[]) {
	float x = blocks.at(0)->getX();
	float z = blocks.at(0)->getZ();
	// 检测食物
	if (fruit->getX() == x && fruit->getZ() == z) { 
		score++;
		printf("Score = %i\n", score);
		bool repeat = false;
		// 避免食物生成是在蛇的身体上
		do { 
			repeat = false;
			fruit->setX(rand() % 11 - 5);
			fruit->setZ(rand() % 11 - 5);
			for (int i = 0; i <= blocks.size() - 1;i++) {
				x = blocks.at(i)->getX();
				z = blocks.at(i)->getZ();
				if (fruit->getX() == x && fruit->getZ() == z)
					repeat = true;
			}
		} while (repeat);
		addBlock();
	}
	// 判断蛇头是否接触到边界
	Block* snakeHead = blocks.at(0);
	if (-6 >= x && currentDirection == D_LEFT) {
		pushSnake();
		snakeHead->setX(6.0f);
		currentDirection = D_LEFT;
		isUpdated = true;
	}
	else if (x >= 6 && currentDirection == D_RIGHT) {
		pushSnake();
		snakeHead->setX(-6.0f);
		currentDirection = D_RIGHT;
		isUpdated = true;
	}
	else if (z >= 6 && currentDirection == D_UP) {
		pushSnake();
		snakeHead->setZ(-6.0f);
		currentDirection = D_UP;
		isUpdated = true;
	}
	else if (z <= -6 && currentDirection == D_DOWN) {
		pushSnake();
		snakeHead->setZ(6.0f);
		currentDirection = D_DOWN;
		isUpdated = true;
	}
	// 检查蛇是否碰撞到了石头,如果是则结束游戏
	for (int i = 0; i < 1; i++) {
		if (stones[i]->getX() == x && stones[i]->getZ() == z) {
			printf("Stone collision. You loss!\n");
			// exit(0); 
			glutHideWindow();
		}
	}
	// 检查蛇是否碰撞到了自己的身体,如果是则结束游戏
	for (int i = 1; i <= blocks.size() - 1; i++) { 
		if (blocks.at(i)->getX() == blocks.at(0)->getX() && blocks.at(i)->getZ() == blocks.at(0)->getZ()) {
			printf("Body collision. You loss!\n"); 
			glutHideWindow();
			blocks.at(i)->setColor(1, 0, 0); 
		}
	}
};
3. 食物的基类Fruit与障碍物的基类Stone

这两个类也类似,其区别只是在于如何draw显示。我将石头显示成正方体,将食物显示成三棱锥。

class Stone {
public:
	Stone(int pX, int pZ);
	~Stone();
	void draw();
	float getX();
	float getZ();
	void setX(float pX);
	void setZ(float pZ);
	float getAngle();
private:
	float x, z, angle;
};

Stone的draw函数

void Stone::draw() {
	angle += 2.0f;
	glPushMatrix();
	glTranslatef(-x, 0, z);
	glColor3f(0, 1, 1);
	glutSolidCube(0.9);
}

Fruit的draw函数

void Fruit::draw() {
	angle += 2.0f;
	glPushMatrix();
	glTranslatef(-x, 0, z);
	glRotatef(angle, 0, 1, 0);
	float gC = 0.5;
	glColor3f(128, 1, 1);
	glBegin(GL_TRIANGLES);
	glVertex3f(0 - gC, 0 - gC, 0 - gC);
	glVertex3f(1 - gC, 0 - gC, 0 - gC);
	glVertex3f(0.5 - gC, 0 - gC, 1 - gC);
	glTexCoord2f(0.5, 1);glVertex3f(0.5 - gC, 1 - gC, 0.5 - gC);
	glTexCoord2f(0, 0); glVertex3f(0 - gC, 0 - gC, 0 - gC);
	glTexCoord2f(1, 0); glVertex3f(1 - gC, 0 - gC, 0 - gC);
	glTexCoord2f(0.5, 1);glVertex3f(0.5 - gC, 1 - gC, 0.5 - gC);
	glTexCoord2f(1, 0); glVertex3f(0 - gC, 0 - gC, 0 - gC);
	glTexCoord2f(0, 0); glVertex3f(0.5 - gC, 0 - gC, 1 - gC);
	glTexCoord2f(0.5, 1);glVertex3f(0.5 - gC, 1 - gC, 0.5 - gC);
	glTexCoord2f(0, 0); glVertex3f(1 - gC, 0 - gC, 0 - gC);
	glTexCoord2f(1, 0); glVertex3f(0.5 - gC, 0 - gC, 1 - gC);
	glEnd();
	glBindTexture(GL_TEXTURE_2D, 0);
	glPopMatrix();
}
4.画图的基类Draw

该类用于画出贪吃蛇的运动区域,为一个11*11的矩形,并且画出边界的正方体线。

这里比较简单,只是循环布置正方体即可,就不放代码。

5. 主函数Main
键位回调函数

使用wsad来控制蛇的上下左右移动。

void keyEvents(unsigned char key, int x, int y) { 
	switch (key) {
	case 27: 
		exit(0);
		break;
	case 'a':
		snake->move(D_LEFT);
		break;
	case 'd':
		snake->move(D_RIGHT);
		break;
	case 'w':
		snake->move(D_UP);
		break;
	case 's':
		snake->move(D_DOWN);
		break;
	}
}
窗口大小改变

这里定义resize函数,当游戏窗口在改变大小的时候,保证游戏界面的横纵比保持一定,不会出现变形的情况,也可以用作全屏展示。

void resize(int w, int h) { //function called on resize of window
	if (h == 0)
		h = 1;
	float ratio = w * 1.0f / h;
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glViewport(0, 0, w, h);
	gluPerspective(50, ratio, 1, 200);
	glMatrixMode(GL_MODELVIEW);
}
绘制

首先要定义出食物,蛇,石头,然后分别调用这些基类的draw函数来画图

Fruit* fruit = new Fruit(1, 1);

Snake* snake = new Snake(0, 0, 3);

// 这里以后可以设置关卡,定义不同的石头位置
Stone* stone1 = new Stone(2, 2);
Stone* stones[1] = { stone1 };

void draw() {
	snake->update(fruit, stones); //update snake position
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glLoadIdentity();
	gluLookAt(
		0, yCamPos, -5, //eye
		0, 0, 0,  //centre
		0, 1, 0   //up
	);
	Draw d;
	d.drawGrid();
	snake->draw();
	fruit->draw();
	
	// build stones
	stone1->draw();

	glutSwapBuffers();
}
主函数

定义窗口的大小,位置,标题等信息,绑定函数

int main(int argc, char **argv) {
	srand(time(NULL));
	glutInit(&argc, argv);
	glutInitWindowPosition(-1, -1);
	glutInitWindowSize(800, 600);
	glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH | GLUT_DOUBLE);
	glutCreateWindow("Snake Game");

	initGL();
	Draw d;
	d.buildDLs();

	glutDisplayFunc(draw);
	glutIdleFunc(draw);
	glutReshapeFunc(resize);
	glutKeyboardFunc(keyEvents);
	glutSpecialFunc(specialKeys);
	glutMouseFunc(mouseEvents);

	glutMainLoop();

	return 1;
}
实验截图

实验截图如下:

粉红色三棱锥为果实,蓝色正方体为障碍物。绿色正方体为蛇的头部。蛇在接触果实后会自动增长一个格子,碰到障碍物的时候,游戏结束,命令行显示游戏结束。

在这里插入图片描述

第二个截图演示的是,蛇在碰到自己身体后,游戏结束的情形。

demo2

四、实验感想

​ 本周的贪吃蛇任务功能并不复杂,主要的难度在于如何使用opengl来绘制一个3d的蛇,以及食物。这里只是简单的使用正方体、三棱锥以及不同的颜色来代表不同的物品。关于碰撞检测方面,我是利用坐标来进行判断,获取蛇头的坐标与地图中食物、石头、身体的这些坐标进行比较,在每一次更新的时候进行检测。在更新动画方面,我也是以蛇头为主,身体跟随蛇头来进行绘制,绘制蛇的身体其实就是绘制多个正方体,只需要按顺序根据蛇头的坐标进行绘制即可。

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作者对游戏的说明: 首先,您应当以一种批判的眼光来看待本程序。这个游戏是我制作 的第一部RPG游戏,无任何经验可谈,完全按照自己对游戏的理解进 行设计的。当我参照了《圣剑英雄2》的源码之后,才体会到专业游 戏引擎的博大精深。 该程序的内核大约有2000余行,能够处理人物的行走、对话、战斗, 等等。由于该程序的结构并不适于这种规模的程序,故不推荐您详 细研究该程序。所附地图编辑器的源程序我已经添加了详细的注释, 其程序结构也比较合理,可以作为初学VC的例子。 该程序在VC的程序向导所生成的SDI框架的基础上修改而成。它没有 使用任何关于VC底层的东西。程序的绝大部分都是在CgameView类中 制作的,只有修改窗口特征的一段代码在CMainFrm类中。其他的类 统统没有用到。另外添加的一个类是CEnemy类。 整个游戏的故事情节分成8段,分别由Para1.h ~ Para8.h八个文件 实现。由于程序仅仅能够被动的处理各种各样的消息,所以情节的 实现也只能根据系统的一些参数来判断当前应当做什么。在程序中 使用了冗长的if……else if……结构来实现这种判断。 当然,在我的记录本上,详细的记录了每个事件的判断条件。这种 笨拙的设计当然是不可取的。成都金点所作《圣剑英雄II》采用了 剧本解读的方式,这才是正统的做法。但这也需要更多的编程经验 和熟练的code功夫。 下面列举的是程序编制过程中总结出来的经验和教训。 第一,对话方式应该采用《圣剑英雄II》的剧本方式。 现在的方式把一个段落中所有的对话都混在一个文件中,然后给每 句话一个号码相对应。这样做虽然降低了引擎的难度,却导致剧情的 编写极其繁琐。 第二,运动和显示应当完全分开。 现在的程序中,运动和显示是完全同步的。即:在定时器中调用所有 敌人的运动函数,然后将主角的动画向前推一帧,接着绘制地图,调 用所有敌人的显示函数、重绘主角。这样的好处是不会掉帧,但带来 的问题是,如果要提高敌人的运动速度,那么帧数也跟着上去了。所 以当DEMO版反馈说速度太慢的时候,我修改起来非常困难。而这个问 题到最后也仅仅是将4步一格该成了2步一格。 第三,VC中数组存在上限。如果用“int aaa[1000000000]”定义一个 数组,编译器肯定不会给分配那么大的内存空间。而在这个程序中, 地图矩阵、NPC矩阵都超过了VC中数组的上限。但这一点知道的太晚了。 在1.0版本中已经发现地图最右端缺少了几行,但不知道是什么原因 造成的。(地图编辑器中未出现此问题,因为地图编辑器是用“序列 化”的方式存盘读盘的。)解决这个问题的方法是用“new”来分配 内存空间。 第四,由于不知道应该如何使用“new”和“delete”,几乎所有的DC 都使用了全局变量。这是完全没有必要的。程序运行期大约会耗用20 多M的内存空间,相当于一个大型游戏所使用的内存空间了。 另外,在游戏的剧情、美工方面也有许多问题,总之一个词“业余”。 我就不总结了。下一部作品,我将争取在程序上有一个质的飞跃。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值