利用OpenGL设计贪吃蛇游戏

利用OpenGL设计贪吃蛇游戏

文章目录

  • 利用OpenGL设计贪吃蛇游戏
    • 任务介绍
    • 游戏玩法
    • 开发环境
    • 游戏实现
      • 贪吃蛇游戏的框架搭建
        • 主程序
        • 游戏类
        • 游戏对象类
        • 工具类
        • 着色器类
        • 摄像机类
        • 精灵渲染类
      • 场景、蛇、食物的渲染
        • 场景
        • 蛇、食物
      • 蛇、食物的控制逻辑
        • 蛇的移动
        • 食物的随机摆放和旋转
      • 碰撞检测与响应
    • 实现效果
    • 总结

任务介绍

  • 贪吃蛇游戏:玩家控制贪吃蛇在游戏区域里驰骋,避免碰到自己或障碍物,尽可能地吃更多的食物以生长!

游戏玩法

  • WASD控制蛇的移动
  • 游戏开始,会在地图空闲位置刷新一个食物,蛇触碰到食物后食物消失,食物会重新刷新,分数增加,蛇会增加一个单位的长度
  • 当蛇触碰到自己,则游戏失败
  • 当蛇接触到地图边界,蛇会在地图另一端重新进入地图

开发环境

  • OpenGL3
  • GLFW
  • IMGUI

游戏实现

贪吃蛇游戏的框架搭建

主程序

对GLFW进行初始化,创建游戏对象,创建GUI,对用户的输入进行传递。主要是在一个 while 循环中,进行对游戏对象的更新与渲染。

//渲染
while (!glfwWindowShouldClose(window))
{
	// 开启深度测试
	glEnable(GL_DEPTH_TEST);
	// 清空深度缓存
	glClear(GL_DEPTH_BUFFER_BIT);
	//处理用户输入
	processInput(window);

	// Start the Dear ImGui frame
	ImGui_ImplOpenGL3_NewFrame();
	ImGui_ImplGlfw_NewFrame();
	ImGui::NewFrame();

	// 计算每一次渲染相差的时间
	GLfloat currentFrame = glfwGetTime();
	deltaTime = currentFrame - lastFrame;
	lastFrame = currentFrame;

	std::string score = "Score : " + std::to_string(Snake.score);
	const char *scorechar = score.c_str();
	// 创建gui
	ImGui::Begin("Gluttonous Snake");

	// 显示分数或者游戏结束
	if (Snake.State == GAME_WIN)
	{
		ImGui::Text("Game Over!!!!!!");
		ImGui::Text(scorechar);
	}
	else
	{
		ImGui::Text(scorechar);
	}
	ImGui::End();

	// Rendering
	ImGui::Render();

	// 游戏处理用户输入
	Snake.ProcessInput(deltaTime);

	// 更新游戏的状态
	Snake.Update(deltaTime);

	//设置清空屏幕所用的颜色
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	//清除颜色缓冲
	glClear(GL_COLOR_BUFFER_BIT);

	// 渲染游戏
	Snake.Render((float)glfwGetTime());

	//渲染gui
	ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
	//交换颜色缓冲
	glfwSwapBuffers(window);
	//检查IO事件
	glfwPollEvents();
}

游戏类

维护游戏状态,提供更新状态的接口供主程序调用,进行碰撞检测等

// 游戏的状态
enum GameState {
	GAME_ACTIVE,
	GAME_MENU,
	GAME_WIN
};

class Game
{
public:
	// 游戏状态
	GameState              State;
	GLuint                 Width, Height;
	// 游戏分数
	int score;

	Game(GLuint width, GLuint height);
	~Game();
	// 初始化游戏加载shader等
	void Init();

	void ProcessInput(GLfloat dt);
	void Update(GLfloat dt);
	void Render(float rotateRad);
	void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch);

	// 键盘状态
	bool pressW;
	bool pressS;
	bool pressA;
	bool pressD;
	void ResetPress();

	// 移动蛇
	void MoveTheSnack();

	// 碰撞检测
	void DoCollisions();
};

游戏对象类

维护游戏对象的状态,在本次游戏中就是蛇头与蛇身、以及生成的食物

class GameObject
{
public:
	// 状态
	glm::vec3   Position, Size;
	GLfloat     Rotation;

	GameObject();
	// 设置位置
	void setPosition(glm::vec3 pos);
private:
	GLuint gameobjectVAO;
	
};

工具类

管理着色器、纹理的加载,本次游戏中没有用到纹理于是只有着色器的加载

class ResourceManager
{
public:
	// Resource storage
	static std::map<std::string, Shader*>    Shaders;

	// Loads (and generates) a shader program from file loading vertex, fragment (and geometry) shader's source code. If gShaderFile is not nullptr, it also loads a geometry shader
	static Shader *  LoadShader(const GLchar *vShaderFile, const GLchar *fShaderFile, const GLchar *gShaderFile, std::string name);
	// Retrieves a stored sader
	static Shader *  GetShader(std::string name);

	// Properly de-allocates all loaded resources
	static void      Clear();
private:
	// Private constructor, that is we do not want any actual resource manager objects. Its members and functions should be publicly available (static).
	ResourceManager() { }
	// Loads and generates a shader from file
	static Shader  *  loadShaderFromFile(const GLchar *vShaderFile, const GLchar *fShaderFile, const GLchar *gShaderFile = nullptr);
};

着色器类

用于对单个的着色器进行初始化,并修改里面的值

class Shader
{
public:
	// State
	GLuint ID;
	// Constructor
	Shader() { }
	// Sets the current shader as active
	Shader  &Use();
	// Compiles the shader from given source code
	void    Compile(const GLchar *vertexSource, const GLchar *fragmentSource, const GLchar *geometrySource = nullptr); // Note: geometry source code is optional 
																													   // Utility functions
	void    SetFloat(const GLchar *name, GLfloat value, GLboolean useShader = false);
	void    SetInteger(const GLchar *name, GLint value, GLboolean useShader = false);
	void    SetVector2f(const GLchar *name, GLfloat x, GLfloat y, GLboolean useShader = false);
	void    SetVector2f(const GLchar *name, const glm::vec2 &value, GLboolean useShader = false);
	void    SetVector3f(const GLchar *name, GLfloat x, GLfloat y, GLfloat z, GLboolean useShader = false);
	void    SetVector3f(const GLchar *name, const glm::vec3 &value, GLboolean useShader = false);
	void    SetVector4f(const GLchar *name, GLfloat x, GLfloat y, GLfloat z, GLfloat w, GLboolean useShader = false);
	void    SetVector4f(const GLchar *name, const glm::vec4 &value, GLboolean useShader = false);
	void    SetMatrix4(const GLchar *name, const glm::mat4 &matrix, GLboolean useShader = false);
private:
	// Checks if compilation or linking failed and if so, print the error logs
	void    checkCompileErrors(GLuint object, std::string type);
};

摄像机类

在一个游戏中会拥有一个摄像机,拍摄整个场景,可以对视角进行移动


class Camera
{
public:
	// 键盘移动
	void moveForward(float deltaTime);
	void moveBack(float deltaTime);
	void moveRight(float deltaTime);
	void moveLeft(float deltaTime);
	// 鼠标移动
	void ProcessMouseMovement(float xoffset, float yoffset, bool constrainPitch);
	// 鼠标滚动
	void ProcessMouseScroll(float yoffset);
	// 获取view矩阵
	glm::mat4 GetViewMatrix();

	Camera(glm::vec3 position, glm::vec3 up, float yaw, float pitch);
	Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch);

	// 获取视角值
	float getZoom();

	glm::vec3 Position;
private:

	// 摄像机的属性
	glm::vec3 Front;
	glm::vec3 Up;
	glm::vec3 Right;
	glm::vec3 WorldUp;
	// 欧拉角
	float Yaw;       // 偏航角
	float Pitch;     // 俯仰角

	float MovementSpeed;      // 相机移动速度
	float MouseSensitivity;   // 鼠标灵敏度
	float Zoom;               // 缩放视野

	void updateCameraVectors();

};

精灵渲染类

用于对场景中物体进行顶点的初始化以及使用着色器去渲染

class SpriteRenderer
{
public:
	SpriteRenderer(Shader *shader, Camera* camera);
	// 渲染场景中的平面
	void DrawPlaneSprite();
	// 渲染场景中的墙
	void DrawWallSprite();
	// 渲染食物
	void DrawFoodSprite(glm::vec3 position, glm::vec3 size, GLfloat rotate);
	// 渲染蛇头
	void DrawHeadSprite(glm::vec3 position, glm::vec3 size = glm::vec3(1.0f, 1.0f, 1.0f), GLfloat rotate = 0.0f);
	// 渲染蛇身
	void DrawBodySprite(glm::vec3 position, glm::vec3 size = glm::vec3(1.0f, 1.0f, 1.0f), GLfloat rotate = 0.0f);
private:
	Shader* shader;
	Camera* camera;
	// 需要渲染的VAO
	GLuint planeVAO;
	GLuint wallVAO;
	GLuint headVAO;
	GLuint bodyVAO;
	GLuint foodVAO;
	// 初始化渲染对象的VAO,VBO
	void initPlaneRenderData();
	void initRenderWall();
	void initRenderHead();
	void initRenderBody();
	void initRenderFood();
};

场景、蛇、食物的渲染

场景

场景需要一个平面与四周的围墙,围墙使用一个长方体表示,然后将长方体进行旋转位置的平移就可以得到四面墙。这部分直接在精灵渲染类中初始化以及渲染。初始化的时候定义顶点的位置和颜色,然后绑定相应的VBO以及VAO

在渲染平面背景的时候设置透视矩阵与观察矩阵

void SpriteRenderer::DrawPlaneSprite() 
{

	this->shader->Use();
	glm::mat4 view = camera->GetViewMatrix();
	this->shader->SetMatrix4("view", view);
	// 设置透视矩阵
	glm::mat4 projection = glm::perspective(glm::radians(camera->getZoom()), (float)1200 / (float)900, 0.1f, 100.0f);
	this->shader->SetMatrix4("projection", projection);

	// 渲染平面
	glm::mat4 model = glm::mat4(1.0f);
	this->shader->SetMatrix4("model", model);
	glBindVertexArray(planeVAO);
	glDrawArrays(GL_TRIANGLES, 0, 6);

}

渲染围墙时候定义不同的世界坐标位置以及旋转

void SpriteRenderer::DrawWallSprite() 
{
	// 每个正方体的世界坐标
	glm::vec3 cubePositions[] = {
		glm::vec3(0.0f,  16.0f,  0.0f),
		glm::vec3(0.0f, -16.0f, 0.0f),
		glm::vec3(16.0f,  0.0f,  0.0f),
		glm::vec3(-16.0f, 0.0f, 0.0f)
	};

	this->shader->Use();
	// 设置不同的物体原点
	for (int i = 0; i < 4; i++) 
	{
		glm::mat4 model = glm::mat4(1.0f);
		model = glm::translate(model, cubePositions[i]);
		if (i == 2 || i == 3) 
		{
			model = glm::rotate(model, glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
		}
		this->shader->SetMatrix4("model", model);
		glBindVertexArray(wallVAO);
		glDrawArrays(GL_TRIANGLES, 0, 36);
	}
}

蛇、食物

蛇分为蛇头与蛇身,蛇头与蛇身的颜色不同但是都是正方体,所以都是GameObject,在游戏类中进行初始化,然后在通过精灵渲染类进行渲染。在游戏类中使用一个vector进行存储。食物其实也是一个正方体,它的颜色与蛇区分开

// 保存蛇身和蛇头
std::vector<GameObject *> allGameObject;
// 食物
GameObject *food;
void Game::Init()
{
    // ...............
	// 初始化头和身体
	GameObject * head = new GameObject();
	allGameObject.push_back(head);

	// 每个身体正方体的世界坐标
	glm::vec3 bodyPositions[] = {
		glm::vec3(0.0f,  -2.0f,  0.0f),
	};
	for (unsigned int i = 0; i < 1; i++)
	{
		GameObject * body = new GameObject();
		body->setPosition(bodyPositions[i]);
		allGameObject.push_back(body);
	}
	// 初始化食物位置
	food = new GameObject();
	food->Size = glm::vec3(0.7f, 0.7f, 0.7f);
	int x, y;
	while (true)
	{
		x = rand() % (14 + 14 + 1) - 14;
		y = rand() % (14 + 14 + 1) - 14;
		if (abs(x) % 2 == 0 && abs(y) % 2 == 0)
		{
			break;
		}
	}
	food->setPosition(glm::vec3(x, y, 0.0f));
	// ...............
}
void SpriteRenderer::DrawHeadSprite(glm::vec3 position, glm::vec3 size, GLfloat rotate)
{

	this->shader->Use();
	// 位置 缩放 旋转
	// 创建变换矩阵
	glm::mat4 model = glm::mat4(1.0f);
	model = glm::translate(model, position);
	model = glm::rotate(model, rotate, glm::vec3(0.0f, 0.0f, 1.0f));
	model = glm::scale(model, size);

	// 重新设置值
	this->shader->SetMatrix4("model", model);
	// 渲染正方体
	glBindVertexArray(headVAO);
	glDrawArrays(GL_TRIANGLES, 0, 36);
}
void SpriteRenderer::DrawFoodSprite(glm::vec3 position, glm::vec3 size, GLfloat rotate)
{
	this->shader->Use();
	// 位置 缩放 旋转
	// 创建变换矩阵
	glm::mat4 model = glm::mat4(1.0f);
	model = glm::translate(model, position);
	model = glm::rotate(model, rotate, glm::vec3(0.0f, 1.0f, 0.0f));
	model = glm::scale(model, size);

	// 重新设置值
	this->shader->SetMatrix4("model", model);
	// 渲染正方体
	glBindVertexArray(foodVAO);
	glDrawArrays(GL_TRIANGLES, 0, 36);
}

蛇、食物的控制逻辑

蛇的移动

蛇的移动使用了一个定时器在0.5s的时候进行蛇身的移动,进行对x轴或y轴增量的判定。从蛇尾开始,蛇尾的位置设置为前一个蛇身的位置以此类推,蛇头进行移动的逻辑要对方向进行判定,并且在超出范围后要从另一边进入,设置为另一个边界的坐标

// 在主程序中
HWND m_hWnd = NULL;
SetTimer(m_hWnd, 1, 500, TimerProc);       // 设置0.5s移动一次蛇

void CALLBACK TimerProc(HWND hWnd, UINT nMsg, UINT nTimerid, DWORD dwTime)
{
	Snake.MoveTheSnack();   // 蛇移动
}

void Game::MoveTheSnack()
{
	if (this->State == GAME_ACTIVE)
	{
		for (int i = allGameObject.size() - 1; i >= 0; i--)
		{
			// 对于蛇头进行移动
			if (i == 0)
			{
				glm::vec3 tmp = allGameObject[i]->Position;
				if (playerDirection == UP)
				{
					tmp.y += 2.0f;
					if (tmp.y > 14.0f)
					{
						tmp.y = -14.0f;
					}
				}
				else if (playerDirection == DOWN)
				{
					tmp.y -= 2.0f;
					if (tmp.y < -14.0f)
					{
						tmp.y = 14.0f;
					}
				}
				else if (playerDirection == LEFT)
				{
					tmp.x -= 2.0f;
					if (tmp.x < -14.0f)
					{
						tmp.x = 14.0f;
					}
				}
				else if (playerDirection == RIGHT)
				{
					tmp.x += 2.0f;
					if (tmp.x > 14.0f)
					{
						tmp.x = -14.0f;
					}
				}


				allGameObject[i]->setPosition(tmp);

			}
			else
			{
				// 对蛇身进行移动
				glm::vec3 tmp = allGameObject[i - 1]->Position;
				allGameObject[i]->setPosition(tmp);
			}
		}
	}
}

void Game::ProcessInput(GLfloat dt)
{
	if (this->State == GAME_ACTIVE)
	{
		// 得到键盘的输入
		if (this->pressA)
		{
			if (playerDirection == RIGHT || playerDirection == LEFT)
			{
				return;
			}
			else
			{
				playerDirection = LEFT;
			}
		}
		if (this->pressD)
		{
			if (playerDirection == RIGHT || playerDirection == LEFT)
			{
				return;
			}
			else
			{
				playerDirection = RIGHT;
			}
		}
		if (this->pressS)
		{
			if (playerDirection == DOWN || playerDirection == UP)
			{
				return;
			}
			else
			{
				playerDirection = DOWN;
			}
		}
		if (this->pressW)
		{
			if (playerDirection == DOWN || playerDirection == UP)
			{
				return;
			}
			else
			{
				playerDirection = UP;
			}
		}
	}
}

食物的随机摆放和旋转

食物的位置随机放置但是不能超出边界,在每次吃到之后进行对位置的重置

int x;
int y;
while (true)
{
	x = rand() % (14 + 14 + 1) - 14;
	y = rand() % (14 + 14 + 1) - 14;
	if (abs(x) % 2 == 0 && abs(y) % 2 == 0)
	{
		break;
	}
}

food->setPosition(glm::vec3(x, y, 0.0f));

食物在每帧的时候会随着时间不停旋转

// 在主程序中将旋转的角度传入
Snake.Render((float)glfwGetTime());
void Game::Render(float rotateRad)
{
	if (this->State == GAME_ACTIVE)
	{
		// 对食物角度的设置
		food->Rotation = rotateRad;
		// 渲染背景的平面墙
		Renderer->DrawPlaneSprite();
		Renderer->DrawWallSprite();
		// 渲染食物
		Renderer->DrawFoodSprite(food->Position,food->Size,food->Rotation);
		// 渲染蛇身和蛇头
		for (unsigned int i = 0; i < allGameObject.size(); i++)
		{
			if (i == 0)
			{
				Renderer->DrawHeadSprite(allGameObject[i]->Position, allGameObject[i]->Size, allGameObject[i]->Rotation);
			}
			else
			{
				Renderer->DrawBodySprite(allGameObject[i]->Position, allGameObject[i]->Size, allGameObject[i]->Rotation);
			}
		}
	}
}

碰撞检测与响应

当蛇头的位置和食物位置相同的时候,则表示吃到了食物,这时候食物需要重置位置,蛇身需要创建一个游戏对象,然后加入vector中,当蛇头的位置与任何一个蛇身的位置相同时候,则表示蛇头与身体相撞,游戏需要结束。

GLboolean CheckCollision(GameObject *one, GameObject *two) // AABB - AABB collision
{
	// x轴方向碰撞
	bool collisionX = (one->Position.x == two->Position.x);
	// y轴方向碰撞
	bool collisionY = (one->Position.y == two->Position.y);
	// 只有两个轴向都有碰撞时才碰撞
	return collisionX && collisionY;
}
void Game::DoCollisions()
{
	// 检测蛇与食物的碰撞
	for (int i = 0; i < allGameObject.size(); i++)
	{
		if (CheckCollision(allGameObject[i], food))
		{
			int x;
			int y;
			while (true)
			{
				x = rand() % (14 + 14 + 1) - 14;
				y = rand() % (14 + 14 + 1) - 14;
				if (abs(x) % 2 == 0 && abs(y) % 2 == 0)
				{
					break;
				}
			}

			food->setPosition(glm::vec3(x, y, 0.0f));
			GameObject * body = new GameObject();
			body->setPosition(allGameObject[allGameObject.size() - 1]->Position);
			allGameObject.push_back(body);

			score++;
			break;
		}
	}
	// 检测蛇头与蛇身的碰撞
	for (int i = 1; i < allGameObject.size(); i++)
	{
		if (CheckCollision(allGameObject[i], allGameObject[0]))
		{
			this->State = GAME_WIN;
			break;
		}
	}
}

实现效果

利用OpenGL设计贪吃蛇游戏_第1张图片

利用OpenGL设计贪吃蛇游戏_第2张图片

总结

本次实验其实对整个游戏框架的构成有了更深入的了解,知道各个类之间应该怎样配合,但是还是因为经验不足有一些耦合性,对材质掌握不熟练所以只是使用了单一的几何体去表示,在蛇移动的刷新没有很连贯,使用的是网格的方法,如果使用每帧刷新的话因为每一个蛇身体之间都有一个偏移量要去处理这个偏移量还是需要一些技巧。

你可能感兴趣的:(OpenGL)