前言
贪吃蛇这个项目其实使用基础的C/C++或者easyx实现应该会容易很多,但是我为了规范一些以及尽可能地使用OpenGL渲染所以采用了一种比较麻烦的模板,但是好消息是这样子完成项目后这个项目的可扩展性和上线应该会高一些(话说贪吃蛇这种小游戏真的需要这种高上限吗)。
这个文档基本包含了整个项目的说明和大部分主要代码的说明。
第一节 总流程
一、原理介绍
1、ui界面
ui界面是使用imgui实现的,在Test的模板下,继承了包括主页面,游戏页面,游戏设置页面和游戏帮助页面一共四个ui页面,通过多态来实现页面之间的切换,通过匿名函数来实现页面注册与初始化。
2、游戏实现
游戏的渲染代码在运行页面内部,而Snake主要负责提供snake的数据(位置,大小等)方便游戏页面渲染snake以及提供各种snake的行为函数,GameMap则负责提供地图的数据(墙的位置和果实的位置等),并且判断发生的突发事件(如蛇吃到了一个果实等等)并进行一些处理。
游戏页面注册时即完成游戏内部初始化,当游戏结束时则渲染结束界面和ui提示游戏结束。
3、设置页面和帮助页面
设置页面提供游戏速度的选择功能,这里提供了3个速度挡位供选择。
说明页面则是游戏的一些说明
4、项目文件结构如下
二、总的流程图
第二节 OpenGL渲染类
1、总述
这里采用的是的现代OpenGL的渲染方法,opengl红宝书第9版给出的渲染流程:顶点数据->顶点着色器(标准化设备坐标)->细分控制着色器->细分计算着色器->几何着色器->图元装配->裁剪和剪切->光栅化->片元着色器,上面流程包含了目前opengl几乎所有的着色器,但是一般只有顶点着色器和片元着色器是必须要的,其他着色器都是可选,这里也只采用了顶点着色器和片元着色器。
顶点着色器:顶点着色器用于处理顶点相关的数据,一般就是变换矩阵、顶点的颜色、纹理坐标,因为贪吃蛇3D化是没必要的,所以使用的mvp变换三矩阵中,固定摄像机(v)在天上竖直地向下看,又由正交矩阵(p)将投影后坐标变换为屏幕像素实现2D化,我们只需要调节模型矩阵(m)就能实现游戏中所有的移动变换了。纹理坐标的作用就是确定纹理该怎么贴到模型上,通过纹理坐标与模型顶点数据对应来完成。
片元着色器:片元着色器是opengl渲染的最后一个流程,它的主要作用就是赋予我们图形最终的颜色,纹理渲染也在这个阶段,在对纹理(其实就是图片)颜色采样后,使用顶点着色器的纹理坐标来进行渲染颜色(纹理/图片)。
和传统c语言贪吃蛇不一样的是,这里使用OpenGL渲染实际渲染地点其实是显卡而不是cpu,这对性能的提升会起到非常大的作用——尽管贪吃蛇项目可能也用不了啥性能。
2、顶点缓冲区VertexBuffer
顶点缓冲区类,它负责抽象化OpenGL顶点缓冲区初始化
基本上,他只有一个构造函数和帮顶/解绑函数,构造函数需要const void* data, unsigned int size两个参数,前者是一个数组,里面是这个缓冲区的全部顶点,后者则是数组的字节大小,这个构造函数会申请从内存里申请一块缓冲区放数组的顶点数据,然后得到这个缓冲区的id并绑定。
//VertexBuffer.h
#pragma once
class VertexBuffer
{
private:
unsigned int m_RendererID;//顶点缓冲区的id(buffer)
public:
VertexBuffer(const void* data,unsigned int size);//构造函数
~VertexBuffer();//析构方法
void Bind() const;//绑定函数
void Unbind() const;//取消绑定函数
};
//.c++文件
#include"VertexBuffer.h"
#include"Renderer.h"
VertexBuffer::VertexBuffer(const void* data, unsigned int size) {//data是数据,size是数据的大小
glGenBuffers(1, &m_RendererID);//生成缓冲区
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);//绑定缓冲区
glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW);
}
VertexBuffer::~VertexBuffer() {
glDeleteBuffers(1, &m_RendererID);//删除缓冲区
}
void VertexBuffer::Bind() const {
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);//绑定缓冲区
}
void VertexBuffer::Unbind() const {
glBindBuffer(GL_ARRAY_BUFFER, 0);//解绑定缓冲区
}
3、顶点数组布局类 VertexBufferLayout
顶点数组需要做的是将顶点缓冲区与某种布局联系在一起,所以顶点缓冲区就是有数据的缓冲区,它们没有实际的概念比如前三个浮点数是位置,没有类型或者大小之类的概念,它只是实际数据的普通缓冲区。而这就是 VertexBufferLayout的价值,当他让下面的顶点数组知道了,每个字节是什么、这些顶点有多大等等,它把缓冲区和实际布局联系在了一起。
他的初始化不需要任何参数,他唯一需要操作的就是按照你的需求去Push一些东西,比如layout.Push(2),这是这个项目的例子,我一共push了两次相同的,这代表一个顶点对应了两个包括了两个浮点数的向量,这让顶点数组可以在除了各种数字啥都没有的缓冲区里面识别出自己需要的信息,前一个是插槽0,后一个是对应的顶点数组的插槽2——然后你给GPU写代码的时候就可以通过插槽区别分别得到顶点们的位置和对应的纹理坐标了。
这就是他的全部作用了。
#pragma once
#include<vector>
#include<GL/glew.h>
#include"Renderer.h"
struct VertexBufferElement
{
unsigned int type;//元素类型
unsigned int count;
unsigned char normalized;//是否被标准化
static unsigned int GetSizeOfType(unsigned int type) {
switch (type) {
case GL_FLOAT: return 4;
case GL_UNSIGNED_INT: return 4;
case GL_UNSIGNED_BYTE: return 1;
}
ASSERT(false);
return 0;
}
};
class VertexBufferLayout {
private:
std::vector<VertexBufferElement> m_Elements;
unsigned int m_Stride;
public:
VertexBufferLayout()
:m_Stride(0) {}
template<typename T>
void Push(unsigned int count) {
static_assert(false);
}
template<>
void Push<float>(unsigned int count) {//浮点数类型的
m_Elements.push_back({ GL_FLOAT,count,GL_FALSE });
m_Stride += VertexBufferElement::GetSizeOfType(GL_FLOAT)*count;
}
template<>
void Push<unsigned int>(unsigned int count) {
m_Elements.push_back({ GL_UNSIGNED_INT,count,GL_FALSE });
m_Stride += VertexBufferElement::GetSizeOfType(GL_UNSIGNED_INT)*count;
}
template<>
void Push<unsigned char>(unsigned int count) {
m_Elements.push_back({ GL_UNSIGNED_BYTE,count,GL_TRUE });
m_Stride += VertexBufferElement::GetSizeOfType(GL_UNSIGNED_BYTE)*count;
}
inline const std::vector<VertexBufferElement> GetElements() const { return m_Elements; }
inline unsigned int GetStride() const { return m_Stride; }//获取步长
};
4、顶点数组 VertexArray
这是顶点相关的倒数第二步,它所做的事情就是接受一个顶点缓冲区和顶点缓冲区的布局,然后将各种数据分开并通过插槽id把使你可以真正的在GPU的程序(着色器)中使用这些数据。
首先是初始化,这一步不需要任何参数,只是会生成一个顶点数组。
然后除了乏善可陈的绑定解绑方法外,值得注意的只有一个AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout)函数,就是这个函数让他可以把顶点缓冲区的数据真正的有序地送到了GPU。
#pragma once
#include"VertexBuffer.h"
//#include"VertexBufferLayout.h"
class VertexBufferLayout;
class VertexArray
{
private:
unsigned int m_RendererID;
public:
VertexArray();
~VertexArray();
void AddBuffer(const VertexBuffer& vb,const VertexBufferLayout& laout);
void Bind() const;
void Unbind() const;
};
#include"VertexArray.h"
#include"VertexBufferLayout.h"
#include"Renderer.h"
#include"VertexBufferLayout.h"
VertexArray::VertexArray() {
glGenVertexArrays(1, &m_RendererID);//生成一个顶点数组
}
VertexArray::~VertexArray() {
glDeleteVertexArrays(1, &m_RendererID);
}
void VertexArray::AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout) {
Bind();
vb.Bind();
const auto& elements = layout.GetElements();
unsigned int offset = 0;//偏移量
for (unsigned int i = 0; i < elements.size(); i++) {
const auto& element = elements[i];
glEnableVertexAttribArray(i);//启用序号为0的属性
glVertexAttribPointer(i, element.count, element.type, element.normalized, layout.GetStride(), (const void*)offset);//定义顶点的位置属性,i处于顶点数组的序号,
//element.count,是位置属性的组件数量或者说元素数量,element.type是指元素数据类型为浮点数,element.normalized代表是否元素进行归一化,
//layout.GetStride比如sizeof(float)*2代表一个顶点有一个位置有两个元素共8个字节的空间,也就是前往下一个顶点需要前进的字节数
//最后一个offset表示这个顶点的第一个字节到我们正在定义的这个属性的第一个字节的距离,这里的位置属性是顶点的第一个属性也是唯一属性,所以是0。
//注意,这里的参数应该是指针而不是常数
offset += element.count*VertexBufferElement::GetSizeOfType(element.type);
}
}
void VertexArray::Bind() const {
glBindVertexArray(m_RendererID);//绑定
}
void VertexArray::Unbind() const {
glBindVertexArray(0);
}
5、顶点索引缓冲区IndexBuffer
首先应该要明白顶点索引的重要性,计算机图形学的世界是由各种三角形组成的,这些三角形组成各种更复杂的东西,比如正方形,但是这些三角形难免会有很多顶点重合,为了不重复存储顶点在顶点缓冲区,顶点索引就成为了必须。(比如正方形四个顶点需要绘制2个三角形,没有索引的话只能存储6个顶点画出两个三角形,但是有了之后就可以只存储4个顶点以及6个索引了)
顶点索引缓冲区非常简单,除了在构造时传入代表索引和顺序的数组和数组元素数目外就只有绑定和解绑函数了,顶点已经有了,他负责连线。
#pragma once
class IndexBuffer
{
private:
unsigned int m_RendererID;//索引缓冲区的id(buffer)
unsigned int m_Count;//索引数量
public:
IndexBuffer(const unsigned int* data, unsigned int count);//构造函数
//size是大小,以字节为单位,count是数量,代表了元素个数,个人习惯如此
~IndexBuffer();//析构方法
void Bind() const;//绑定函数
void Unbind() const;//取消绑定函数
inline unsigned int GetCount() const { return m_Count; }
};
#include"IndexBuffer.h"
#include"Renderer.h"
IndexBuffer::IndexBuffer(const unsigned int* data, unsigned int count)
: m_Count(count)
{//data是数据,size是数据的大小
glGenBuffers(1, &m_RendererID);//生成缓冲区
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID);//绑定缓冲区
glBufferData(GL_ELEMENT_ARRAY_BUFFER, count*sizeof(GLuint), data, GL_STATIC_DRAW);//GLuint是元素的数据类型,并不总是无符号整数
}
IndexBuffer::~IndexBuffer() {
glDeleteBuffers(1, &m_RendererID);//删除缓冲区
}
void IndexBuffer::Bind() const {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID);//绑定缓冲区
}
void IndexBuffer::Unbind() const {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);//解绑定缓冲区
}
6、纹理Texture
可以简单理解为一个图片,除了绑定之类的函数外,他需要在构造时指定一个图像文件,然后之后他就”代表“了那个图片了。
#pragma once
#include<SOIL2.h>
#include<string>
class Texture
{
private:
unsigned int m_RendererID;
std::string m_FilePath;
unsigned char* m_LocalBuffer;
int m_Width, m_Height, m_BPP;
public:
Texture(const char* path);
~Texture();
void Bind(unsigned int slot = 0) const;
void Unbind() const;
inline int GetWidth() const { return m_Width; }
inline int GetHeight() const { return m_Height; }
};
#include"Texture.h"
#include "GL/glew.h"
Texture::Texture(const char* path)
:m_FilePath(path),m_LocalBuffer(nullptr),m_Width(0),m_Height(0),m_BPP(0),m_RendererID(0)
{
glGenTextures(1,&m_RendererID);//生成 1 个纹理
glBindTexture(GL_TEXTURE_2D, m_RendererID);
// 设置纹理取样步长(防止图像扭曲倾斜)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
// 设置纹理环绕方式(重复方式)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // S 轴
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // T 轴
//float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
//glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
// 设置纹理过滤方式(线性方式)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载图片,其宽高将会存到参数变量中,返回一个非常大的 char(byte)数组
unsigned char* m_LocalBuffer = SOIL_load_image(path, &m_Width, &m_Height, 0, SOIL_LOAD_RGB);
// glTexImage2D()
// 绑定图像(目标,多级渐远纹理级别[0,基本级别],格式[RGB],宽度,高度,格式,数据类型[char数组],图像)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, m_Width, m_Height, 0, GL_RGB, GL_UNSIGNED_BYTE, m_LocalBuffer);
// 自动生成多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);
// 绑定好图像后释放内存和解绑
SOIL_free_image_data(m_LocalBuffer);
glBindTexture(GL_TEXTURE_2D, 0);
}
Texture::~Texture()
{
glDeleteTextures(1, &m_RendererID);
}
void Texture::Bind(unsigned int slot) const
{
glActiveTexture(GL_TEXTURE0 + slot);
glBindTexture(GL_TEXTURE_2D, m_RendererID);
}
void Texture::Unbind() const
{
glBindTexture(GL_TEXTURE_2D, 0);
}
7、着色器Shader
构造的时候需要一个文件,这个文件是你给GPU写的一个渲染小程序。
一个大种类的方法是设置各种类型的统一变量,统一变量是一种CPU,GPU都可以使用的变量,主要用来和GPU通信,比如MVP矩阵一般就是通过统一变量传给GPU做图形运算的。
#pragma once
#include<string>
#include<unordered_map>
#include "glm/glm.hpp"
//#include"Renderer.h"
//class Renderer;
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
class Shader {
private:
unsigned int m_RendererID;
std::string m_FilePath;
std::unordered_map<std::string, int> m_UniformLocationCache;
public:
Shader(const std::string& filepath);
~Shader();
void Bind() const;
void Unbind() const;
//设置统一变量
void SEtUniform1i(const std::string& name, int value);
void SEtUniform1f(const std::string& name, float value);
void SetUniform4f(const std::string& name,float v0,float v1,float v2,float v3);
void SetUniformMat4f(const std::string & name, const glm::mat4& matrix);
private:
int GetUniformLocation(const std::string& name);
unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader);
ShaderProgramSource ParseShader(const std::string& filepath);
unsigned int CompileShader(unsigned int type, const std::string& source);
};
#include"Shader.h"
#include<iostream>
#include<fstream>
#include<string>
#include<sstream>
#include"Renderer.h"
Shader::Shader(const std::string & filepath)
:m_FilePath(filepath),m_RendererID(0)
{
ShaderProgramSource source = ParseShader(filepath);
m_RendererID = CreateShader(source.VertexSource, source.FragmentSource);
}
Shader::~Shader()
{
glDeleteProgram(m_RendererID);
}
ShaderProgramSource Shader::ParseShader(const std::string& filepath)
{
std::ifstream stream(filepath); /* 这里没判断文件是否能正常打开 is_open */
enum class ShaderType {
NONE = -1, VERTEX = 0, FRAGMENT = 1
};
std::string line;
std::stringstream ss[2];
ShaderType type = ShaderType::NONE;
while (getline(stream, line)) {
if (line.find("#shader") != std::string::npos) { /* 找到#shader标记 */
if (line.find("vertex") != std::string::npos) { /* 顶点着色器标记 */
type = ShaderType::VERTEX;
}
else if (line.find("fragment") != std::string::npos) { /* 片段着色器标记 */
type = ShaderType::FRAGMENT;
}
}
else {
ss[(int)type] << line << '\n';
}
}
return { ss[0].str(),ss[1].str() };
}
unsigned int Shader::CompileShader(unsigned int type, const std::string& source) {
unsigned int id;
/* 提升作用域 */
GLCall(id = glCreateShader(type)); /* 创建对应类型的着色器 */
const char* src = source.c_str();
GLCall(glShaderSource(id, 1, &src, nullptr)); /* 设置着色器源码 */
GLCall(glCompileShader(id)); /* 编译着色器 */
/* 编译错误处理 */
int result;
GLCall(glGetShaderiv(id, GL_COMPILE_STATUS, &result)); // 获取当前着色器编译状态
if (result == GL_FALSE) {
int length;
GLCall(glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length)); // 获取日志长度
char* msg = (char*)_malloca(length * sizeof(char));
GLCall(glGetShaderInfoLog(id, length, &length, msg)); // 获取日志信息
std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
std::cout << msg << std::endl;
GLCall(glDeleteShader(id)); // 删除着色器
return 0;
}
return id;
}
unsigned int Shader::CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
unsigned int program;
GLCall(program = glCreateProgram()); /* 创建程序 */
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);
/* 将着色器附加到程序上 */
GLCall(glAttachShader(program, vs));
GLCall(glAttachShader(program, fs));
GLCall(glLinkProgram(program)); /* 链接程序 */
GLCall(glValidateProgram(program)); /* 验证 */
/* 删除着色器 */
GLCall(glDeleteShader(vs));
GLCall(glDeleteShader(fs));
return program;
}
void Shader::Bind() const
{
glUseProgram(m_RendererID);
}
void Shader::Unbind() const
{
glUseProgram(0);
}
void Shader::SEtUniform1i(const std::string & name, int value)
{
glUniform1i(GetUniformLocation(name), value);
}
void Shader::SEtUniform1f(const std::string & name, float value)
{
glUniform1f(GetUniformLocation(name), value);
}
void Shader::SetUniform4f(const std::string & name, float v0, float v1, float v2, float v3)
{
glUniform4f(GetUniformLocation(name), v0, v1, v2, v3);
}
void Shader::SetUniformMat4f(const std::string & name, const glm::mat4& matrix)
{
glUniformMatrix4fv(GetUniformLocation(name), 1, GL_FALSE, &matrix[0][0]);
}
int Shader::GetUniformLocation(const std::string& name)
{
if (m_UniformLocationCache.find(name) != m_UniformLocationCache.end()) {
return m_UniformLocationCache[name];
}
int location;
location= glGetUniformLocation(m_RendererID, name.c_str());
if (location == -1) {
std::cout << "警告:统一变量:" << name << "不存在" << std::endl;
}
m_UniformLocationCache[name] = location;
return location;
}
8、渲染类Renderer
是的顾名思义,咱们至于可以把图像渲染出来了,这个类做的就是根据咱们在上面打下来的基础,把图形渲染出来。另外他还额外集成了一个错误检查的宏,只要我们把语句改成GLCall(语句)他就可以帮我们检查这个语句是否出错了。
渲染则通过void Draw(const VertexArray& va, const IndexBuffer& ib,const Shader& shader) const;进行,一个方法,完成基于上面的基础的渲染。
#pragma once
#include<GL/glew.h>
#include"VertexArray.h"
#include"IndexBuffer.h"
#include<iostream>
//#include"Shader.h"
class Shader;
#define ASSERT(x) if (!(x)) __debugbreak();//一个断言,括号里的x是false就进入if,也就是中断
#define GLCall(x) {GLClearError();\
x;\
ASSERT(GLLogCall(#x,__FILE__,__LINE__))}//x就是我们要检测的是否有错误的函数
//
void GLClearError();
bool GLLogCall(const char* function, const char* file, int line);
class Randerer {
public:
void Draw(const VertexArray& va, const IndexBuffer& ib,const Shader& shader) const;
void Clear() const;
};
#include"Renderer.h"
#include"Shader.h"
void GLClearError() {//先清除所有错误防止其他函数的错误干扰到我们
while (glGetError() != GL_NO_ERROR);
}
bool GLLogCall(const char* function, const char* file, int line) {//在清除所有错误后再运行我们要检查的函数,最后在该函数执行完毕之后用这个函数就能打印要检查的函数里面的错误了,并且返回一个false,让我们可
//以通过断言实现在出现错误的时候中断
while (GLenum error = glGetError()) {
std::cout << "[OpenGL Error] (" << error << ")" << std::endl << "错误函数名:" << function << std::endl <<
"错误文件名" << file << std::endl << "第" << line << "行" << std::endl;
return false;
}
return true;
}
void Randerer::Draw(const VertexArray& va, const IndexBuffer& ib,const Shader& shader) const {
shader.Bind();/* 为GPU绑定着色器程序 */
va.Bind();/* 包含实际处理数据的数组 */
ib.Bind();
GLCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr));//检查这个函数是否有错误并打印
}
void Randerer::Clear() const{
glClear(GL_COLOR_BUFFER_BIT);//清除屏幕
}
9、着色器文件
其实就是GPU的代码,负责接收来自缓冲区或者统一变量数据,以此计算出计算机屏幕上每个像素会显示的颜色,然后显示出来。
这个代码文件是被shader类传到GPU的,里面包含的两个代码文件(顶点着色器和片段着色器)也是被shader分开的。
顶点着色器值得注意的是里面的变换,之前顶点数组的位置其实是他在3D世界中的位置,要经过MVP变换后(模型矩阵代表模型的偏移,视图矩阵代表摄像机的偏移,p有正交投影和透视投影,把变化后的模型投影变化后的摄像机——也可以说是我们的屏幕——上面)才是真正的在屏幕上面的坐标。在2D的时候可能有点多此一举,但是这对于3D非常重要
#shader vertex
#version 330 core
layout(location=0) in vec4 position; //location=0就是上面的属性序号0
layout(location = 1) in vec2 texCoord;//纹理坐标
uniform mat4 u_Mvp;
out vec2 v_TexCoord;
void main()
{
gl_Position =u_Mvp*position;
v_TexCoord = texCoord;
} ;
#shader fragment
#version 330 core
layout(location=0) out vec4 color; //location=0就是上面的属性序号0
uniform sampler2D u_Texture;
in vec2 v_TexCoord;
void main()
{
vec4 texColor = texture(u_Texture, v_TexCoord);
color= texColor;
} ;
第三节 Test框架
1、总述
其实是一个基于imgui的一个简陋的ui框架。
一方面我掌握这个框架不是很好,另外一方面时间有限,所以就做的有点”不太漂亮了“
2、核心代码解析
这是基于多态的一种框架,你可以通过继承Test类来写出各种你想要实现的页面,在Test类中存在大量的非纯虚空函数,你完全可以自由地选择实现他们或者不实现,并在主函数中被以多态的形式进行调用以实现各种的独特功能。
他有一个主页面TestMenu作为入口,然后通过模板+匿名函数实现方便地进行页面注册,如menu->RegisterTesttest::TestClearColor(“Help”); test::TestClearColor代表了注册页面的类型,"Help"代表了这个新注册的页面的名字——在主页面进行ui渲染的时候就会渲染上一个代表这个新页面的按钮了。
另外,在他的等待着被重写的函数中,virtual void OnRender(){ }和virtual void OnImGuiRender() { }尤为重要,基本上是必重写的,前一个是OpenGL的渲染函数,后一个则负责渲染ui。
#pragma once
#include<GL/glew.h>
#include"GLFW/glfw3.h"
#include<functional>
#include <vector>
namespace test {
class Test
{
public:
Test(){ }
virtual ~Test(){ }
virtual void OnUpdata(float deltaTime){ }//不用纯虚函数,可以选择性地实现这些函数
virtual void OnRender(){ }//渲染用
virtual void OnImGuiRender() { }//渲染用
virtual void SetWindow(GLFWwindow* window){ }//设置参数
virtual void SetOffset(float x,float y) { }
virtual void SetDirction(char dirction){ }
virtual bool GetP_open() { return true; }
virtual void SetSpeed(int& speed) { }
};
class TestMenu :public Test {
public:
TestMenu(Test*& currentTestPointer);
void OnImGuiRender() override;//渲染用
template<typename T>
void RegisterTest(const std::string& name) {
std::cout << "Registering test " << name << std::endl;
m_Tests.push_back(std::make_pair(name, []() {return new T(); }));//注册测试类,返回基于类型的实例
}
private:
Test*& m_CurrentTest;//一会传递指针引用用
std::vector<std::pair<std::string, std::function<Test*()>>> m_Tests;//一个成对的向量,包含一个字符串和一个返回测试指针的函数
};
}
#include"Test.h"
#include"vender/imgui/imgui.h"
namespace test {
TestMenu::TestMenu(Test *& currentTestPointer)
:m_CurrentTest(currentTestPointer){
}
void TestMenu::OnImGuiRender(){
for (auto& test : m_Tests) {
if (ImGui::Button(test.first.c_str())) {//如果按下了这个按钮返回true,并且渲染这个按钮
m_CurrentTest=test.second();//这个按钮会改变m_CurrentTest指针
}
}
}
}
第三节 游戏主体
1、总述
主要由组成游戏页面(TestTexture),蛇类(Snake)和游戏地图(GameMap)组成,游戏页面主要任务是渲染和计时,蛇类负责蛇的数据和行为,地图包括地图各种信息(二维数组解决),果实的刷新和突发事件的检测。
2、游戏页面
继承自Test的一个页面,主要函数有构造函数TestTexture(),
图形界面渲染函数TestTexture::OnRender(),
ui渲染函数 void TestTexture::OnImGuiRender(),
偏移量设置函数void TestTexture::SetOffset(float x,float y)(这里被用来计时,但是因为2D偏移量一般要两个,x和y,所以额外传了一个不需要的y)
屏幕设置函数void TestTexture::SetWindow(GLFWwindow* window) ,设置渲染在哪个屏幕上。
方向设置函数void TestTexture::SetDirction(char dirction),接受来自键盘的输入并改变蛇的方向。
这个页面的渲染其实只使用了一个顶点数组和一个顶点索引数组和一个着色器,唯一的区别其实是纹理,和模型矩阵。通过提供不同的模型矩阵偏移来渲染不同的矩形,二通过绑定不同的纹理来渲染不同的内容——比如说果实和蛇(墙还没来得及做出来)。模型矩阵的偏移渲染通过地图类记录的状态来实现,不同的状态对应不同的事物,从而可以绑定不同的纹理来进行渲染,理论上是这样的,但是开发的时候因为是先写了蛇的类没有写地图的类,所以直接人蛇只通过自己记录状态就渲染了自己,而墙的渲染暂时还没来得及写,果实渲染通过直接返回果实位置渲染了。对于蛇的渲染,我其实就做了一个状态记录+蛇头的位置,状态记录就是一个字符串s_BodyLocation,他记录了”wasd“四种字符,可以理解为这是记录的蛇头到蛇尾的一步步移动,移动到哪里我就渲染到哪里。
他的构造函数则是渲染环境的初始化,还包括一些数据的初始化(比如蛇的初始位置等),地图的初始化和刷新一个果实等等。
#pragma once
#include "Test.h"
#include<iostream>
#include "VertexBuffer.h"
#include "VertexBufferLayout.h"
#include "Texture.h"
#include"Shader.h"
#include"Snake.h"
#include"GameMap.h"
//#include"vender/imgui/imgui.h"
//#include"vender/imgui/imgui_impl_glfw_gl3.h"
#include <memory>
namespace test {
class TestTexture :public Test {
public:
TestTexture();
~TestTexture();
void OnUpdata(float deltaTime) override;
void OnRender() override;
void OnImGuiRender() override;
void SetWindow(GLFWwindow* window) override;
void SetOffset(float x, float y) override;
void SetDirction(char dirction) override;
bool GetP_open() override;
private:
std::unique_ptr<VertexArray> m_VAO;
std::unique_ptr<VertexBuffer> m_VertexBuffer;
std::unique_ptr < IndexBuffer> m_IndexBuffer ;
std::unique_ptr < Shader> m_Shader;
std::unique_ptr <Texture> m_Texture;
std::unique_ptr <Texture> m_TextureApple;
glm::mat4 m_Proj, m_View;
GLFWwindow* m_Window;
float m_ClearColor[4],m_x=0,m_y=0;
glm::vec3 m_TranslationA, m_TranslationB;
Snake snake;
GameMap gamemap;
Randerer renderer;
int StatusCode = 0;
void RenderSnake();
void RenderApple();
void RenderWall();
};
}
#include<GL/glew.h>
#include"GLFW/glfw3.h"
#include"TextTexture.h"
#include "Renderer.h"
#include "vender/imgui/imgui.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
namespace test {
TestTexture::TestTexture()
:m_ClearColor{ 0.0f,0.0f,0.0f,1.0f }, m_TranslationA(200, 200, 0), m_TranslationB(400, 200, 0){//构建渲染的环境
glm::mat4 mvp;
float positions[16]{
0.0f, 40.0f, 0.0f,0.0f,
20.0f, 40.0f ,1.0f,0.0f,
20.0f ,60.0f, 1.0f,1.0f,
0.0f, 60.0f, 0.0f,1.0f
};//代表位置的数组
unsigned int indices[] = {
0,1,2,
2,3,0
};
GLCall(glEnable(GL_BLEND));
GLCall(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
m_IndexBuffer = std::make_unique<IndexBuffer>(indices, 6);
//VertexArray va;
m_VertexBuffer = std::make_unique<VertexBuffer>(positions, sizeof(positions));
VertexBufferLayout layout;
layout.Push<float>(2);
layout.Push<float>(2);
m_VAO = std::make_unique<VertexArray>();
m_VAO->AddBuffer(*m_VertexBuffer, layout);
//glm::mat4 proj = glm::ortho(0.0f, 1000.0f, 0.0f, 1000.0f, -1.0f, 1.0f);
m_Shader = std::make_unique<Shader>("Basic.shader");
m_Shader->Bind();
m_Shader->SetUniformMat4f("u_Mvp", mvp);
m_Shader->SetUniform4f("u_Color", 0.2f, 0.3f, 0.8f, 1.0f);
//m_Shader->SEtUniform1i("u_Texture", 0);//纹理绑定到插槽0
m_Texture = std::make_unique<Texture>("aaa.jpg");
m_TextureApple = std::make_unique<Texture>("apple.jpg");
m_Texture->Bind();
m_TextureApple->Bind();
m_Proj = glm::ortho(0.0f, 1000.0f, 0.0f, 1000.0f, -1.0f, 1.0f);
m_View = glm::translate(glm::mat4(1.0f), glm::vec3(-200, -200, 0));//视图矩阵,相机右平移100像素
gamemap.Initialize();
gamemap.FruitFlushed();
gamemap.SnakeFlushed(snake);
StatusCode = 0;
m_VAO->Unbind();//解绑顶点数组
m_Shader->Unbind();
m_VertexBuffer->Unbind();
m_IndexBuffer->Unbind();
m_TextureApple->Unbind();
m_Texture->Unbind();
}
TestTexture::~TestTexture() {
}
void TestTexture::OnUpdata(float deltaTime) {
}
void TestTexture::OnRender() {
if (StatusCode == 1 || StatusCode == 0) {
//std::cout << "this"<< gamemap.SnakeFlushed(snake);
glClearColor(m_ClearColor[0], m_ClearColor[1], m_ClearColor[2], m_ClearColor[3]);
glClear(GL_COLOR_BUFFER_BIT);
RenderSnake();
{
RenderApple();
}
}
else
{
glClearColor(1, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
//StatusCode = gamemap.SnakeFlushed(snake);
}
}
/*{
glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationB);//模型矩阵
glm::mat4 mvp = m_Proj * m_View*model;//矩阵乘法可以看做是”从右到左“的
m_Shader->Bind();
m_Shader->SetUniformMat4f("u_Mvp", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
}*/
void TestTexture::OnImGuiRender() {
if (StatusCode == 2 || StatusCode == 3) {
//ImGui::SliderFloat3("TranslationA", &m_TranslationA.x, 0.0f, 1000.0f);
//ImGui::SliderFloat3("TranslationB", &m_TranslationB.x, 0.0f, 1000.0f);
ImGui::Text("You're dead");
ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
//ImGui::ShowDemoWindow();
}
}
void TestTexture::SetWindow(GLFWwindow* window) {
m_Window = window;
}
void TestTexture::SetOffset(float x,float y) {
m_x++;
//std::cout << m_x << " ";
if (m_x > snake.GetSpeed()) {
if (StatusCode == 0 || StatusCode == 1) {
snake.Walk();
StatusCode = gamemap.SnakeFlushed(snake);
std::cout << std::endl<<StatusCode;
if (StatusCode == 1) snake.SnakeEat(0),gamemap.FruitFlushed() ;
else if (StatusCode == 2 || StatusCode == 3) snake.Initialize(),gamemap.Initialize(),gamemap.FruitFlushed();
}
//std::cout << m_x << " ";
m_x = 0;
}
}
void TestTexture::SetDirction(char dirction)
{
if (dirction >= 'A'&&dirction <= 'Z') dirction = dirction - ('A' - 'a');
if (dirction >= 'a'&&dirction <= 'z') {
snake.SetDirction(dirction);
}
}
bool TestTexture::GetP_open()
{
if (StatusCode == 2 || StatusCode == 3) {
return true;
}
else
{
return false;
}
}
void TestTexture::RenderSnake()
{
m_Texture->Bind();
{
m_TranslationB = m_TranslationA;
m_TranslationB.x += snake.GetCoordinateX();
m_TranslationB.y += snake.GetCoordinateY();
glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationB);//模型矩阵
glm::mat4 mvp = m_Proj * m_View*model;//矩阵乘法可以看做是”从右到左“的
m_Shader->Bind();
m_Shader->SetUniformMat4f("u_Mvp", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
}
std::string body = snake.GetBodyLocation();
//std::cout << body << std::endl;
for (int i = 1; i < body.length(); i++)
{
if (body.at(i) == 'w') {
m_TranslationB.y += 20;
}
else if (body.at(i) == 's') {
m_TranslationB.y -= 20;
}
else if (body.at(i) == 'a') {
m_TranslationB.x -= 20;
}
else if (body.at(i) == 'd') {
m_TranslationB.x += 20;
}
glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationB);//模型矩阵
glm::mat4 mvp = m_Proj * m_View*model;//矩阵乘法可以看做是”从右到左“的
m_Shader->Bind();
m_Shader->SetUniformMat4f("u_Mvp", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
}
m_Texture->Unbind();
}
void TestTexture::RenderApple()
{
m_TextureApple->Bind();
m_TranslationB = m_TranslationA;
m_TranslationB.x += (20 * gamemap.GetFruitX());
m_TranslationB.y += (20 * gamemap.GetFruitY() - 40);
//m_TranslationB.y += ( 49*20- 40);
glm::mat4 model = glm::translate(glm::mat4(1.0f), m_TranslationB);//模型矩阵
glm::mat4 mvp = m_Proj * m_View*model;//矩阵乘法可以看做是”从右到左“的
m_Shader->Bind();
m_Shader->SetUniformMat4f("u_Mvp", mvp);
renderer.Draw(*m_VAO, *m_IndexBuffer, *m_Shader);
m_TextureApple->Unbind();
}
void TestTexture::RenderWall()
{
}
}
3、蛇类Snake
他有需要给出蛇头初始位置的构造函数也有有个不需要参数的构造函数,需要的哪个是为了以后的扩展,这里暂时只使用到了不需要参数的构造函数。
构造函数主要是初始化,包括把蛇头位置设置为默认位置,然后设置一条在右下角的初始向上运动长度为3的蛇。至于蛇等级(s_Level),蛇吃的果实能量数(s_Energy)和蛇速度参数(s_Speed),也是为了以后扩展留下的(吃的多了就升级,升级了就提高速度,另外等级升级所需经验为非线性提高),现在已经凌晨了,我不确定是否有时间把这玩意加上去,可能会只加部分(比如只有两级,减少工作量),所以部分设想可能已经实现了,但是也可能没实现。
对于Snake::Walk(),我的实现很简单,先让蛇头走,然后放弃尾巴的状态码——因为尾巴已经不需要渲染了,再给状态码插入一个表示蛇头从新位置回到老位置的新状态就搞定了。
然后是SnakeEat函数,也就一个增加长度的函数,实际上,他和walk函数唯一的区别就是因为吃到了果实而不需要放弃渲染尾巴了。
#pragma once
#include<iostream>
#include<string>
#include<vector>
class Snake
{
public:
Snake(int x,int y);
Snake();
void SnakeEat(int energy);
void Walk();
void SetDirction(char dirction);
int GetLength();
std::string GetBodyLocation();
int GetCoordinateX();
int GetCoordinateY();
int GetSpeed();
void Initialize();
~Snake();
private:
int s_length,s_SnakeheadCoordinateX, s_SnakeheadCoordinateY,s_Speed,s_Energy,s_Level;
char s_Direction;
std::string s_BodyLocation;
};
#include"Snake.h"
Snake::Snake(int x, int y)
{
s_SnakeheadCoordinateX = x, s_SnakeheadCoordinateY =y ;
s_length = 2, s_Level = 1, s_Speed = 2;
s_Energy = 0;
s_Direction = 'w';
s_BodyLocation.append("s");
s_BodyLocation.append("s");
s_BodyLocation.append("s");
}
Snake::Snake()
{
Initialize();
}
void Snake::SnakeEat(int energy)
{
s_length++;
s_Energy += energy;
if (s_Energy > 100) s_Speed = 1;
if (s_Direction == 'w' || s_Direction == 'W') {
s_SnakeheadCoordinateY += 20;
s_BodyLocation.insert(1, 1, 's');
}
else if (s_Direction == 's' || s_Direction == 'S') {
s_SnakeheadCoordinateY -= 20;
s_BodyLocation.insert(1, 1, 'w');
}
else if(s_Direction == 'a' || s_Direction == 'A') {
s_SnakeheadCoordinateX -= 20;
s_BodyLocation.insert(1, 1, 'd');
}else if (s_Direction == 'd' || s_Direction == 'D') {
s_SnakeheadCoordinateX += 20;
s_BodyLocation.insert(1, 1, 'a');
}
std::cout << "this";
}
void Snake::Walk()
{
if (s_Direction == 'w'|| s_Direction == 'W') {
s_SnakeheadCoordinateY += 20;
s_BodyLocation.pop_back();
s_BodyLocation.insert(1, 1, 's');
}
else if (s_Direction == 's' || s_Direction == 'S') {
s_SnakeheadCoordinateY -= 20;
s_BodyLocation.pop_back();
s_BodyLocation.insert(1, 1, 'w');
}
else if (s_Direction == 'a' || s_Direction == 'A') {
s_SnakeheadCoordinateX -= 20;
s_BodyLocation.pop_back();
s_BodyLocation.insert(1, 1, 'd');
}
else if (s_Direction == 'd' || s_Direction == 'D') {
s_SnakeheadCoordinateX += 20;
s_BodyLocation.pop_back();
s_BodyLocation.insert(1, 1, 'a');
}
std::cout << s_Direction;
}
void Snake::SetDirction(char dirction)
{
if (s_Direction != dirction) {
//std::cout << s_Direction << " " << dirction <<s_SnakeheadCoordinateX << std::endl;
s_Direction = dirction;/*
if (s_Direction == 'w' || s_Direction == 'W') {
s_SnakeheadCoordinateY -= 20;
}
else if (s_Direction == 's' || s_Direction == 'S') {
s_SnakeheadCoordinateY += 20;
}
else if (s_Direction == 'a' || s_Direction == 'A') {
s_SnakeheadCoordinateX += 20;
}
else if (s_Direction == 'd' || s_Direction == 'D') {
s_SnakeheadCoordinateX -= 20;
}*/
//std::cout << s_SnakeheadCoordinateX << std::endl;
}
}
int Snake::GetLength()
{
return s_length;
}
std::string Snake::GetBodyLocation()
{
return s_BodyLocation;
}
int Snake::GetCoordinateX()
{
return s_SnakeheadCoordinateX;
}
int Snake::GetCoordinateY()
{
return s_SnakeheadCoordinateY;
}
int Snake::GetSpeed()
{
return s_Speed;
}
void Snake::Initialize()
{
s_SnakeheadCoordinateX = 0, s_SnakeheadCoordinateY = 0;
s_length = 2, s_Level = 1, s_Speed = 2;
s_Energy = 0;
s_Direction = 'w';
s_BodyLocation.append("s");
s_BodyLocation.append("s");
s_BodyLocation.append("s");
}
Snake::~Snake()
{
}
4、游戏地图类GameMap
把整个地图分成了50*50的格子,用一个二维数组来代表地图,用存储的数代表状态(果实,空白,蛇,墙等)。
构造函数就是初始化的同时刷新一个果实出来,不需要参数。
int GameMap::SnakeFlushed(Snake snake)在snake满足计时并开始行走的时候调用的,检查蛇头新地点的状态码,看看是否有事件发生,并重写状态数组的蛇状态,同时这个函数会返回状态码。
#pragma once
#include"Snake.h"
class GameMap
{
public:
GameMap();
~GameMap();
void FruitFlushed();
int SnakeFlushed(Snake snake);
int EventDetection(Snake snake);
int GetFruitX();
int GetFruitY();
void Initialize();
void InitializeSnake();
private:
int StatusCode[50][50] = { 0 }, FruitNumber, FruitX, FruitY, SpaceRemaining;//StatusCode数组0是free,1是果实,2是蛇,3是墙
//int SnakeSpaceX, SnakeSpaceY, WallSpaceX, WallSpaceX, FreeSpaceX, FreeSpaceX;
};
#include"GameMap.h"
#include <cstdlib>
GameMap::GameMap()
{
FruitNumber = 0;
FruitFlushed();
}
GameMap::~GameMap()
{
}
void GameMap::FruitFlushed()
{
int x, y;
while (true)
{
x = rand() % 50;
y = rand() % 50;
if (StatusCode[x][y] == 0) {
StatusCode[x][y] = 1;
FruitNumber=1;
FruitX = x, FruitY = y;
break;
}
}
}
int GameMap::SnakeFlushed(Snake snake)
{
int x, y,sta=0;
x = snake.GetCoordinateX(), y = (snake.GetCoordinateY()+40);
x /= 20, y /= 20;
if ((x >= 0 && x < 50) && y >= 0 && y < 50) {
if (StatusCode[x][y] == 1) {
StatusCode[x][y] = 2;
sta= 1;
}
else if(StatusCode[x][y]==0)
{
StatusCode[x][y] = 2;
sta = 0;
}
else if(StatusCode[x][y]==2||StatusCode[x][y]==3)
{
return StatusCode[x][y];
}
}
else
{
return 3;//边界墙
}
std::string body = snake.GetBodyLocation();
InitializeSnake();
//std::cout << body << std::endl;
for (int i = 1; i < body.length(); i++)
{
if (body.at(i) == 'w') {
y += 1;
}
else if (body.at(i) == 's') {
y -= 1;
}
else if (body.at(i) == 'a') {
x -= 1;
}
else if (body.at(i) == 'd') {
x += 1;
}
if ((x >= 0 && x < 50) && y >= 0 && y < 50) {//
StatusCode[x][y] = 2;
}
}
return sta;
}
int GameMap::EventDetection(Snake snake)
{
int x, y;
return 0;
}
int GameMap::GetFruitX()
{
return FruitX;
}
int GameMap::GetFruitY()
{
return FruitY;
}
void GameMap::Initialize()
{
for (int i1 = 0; i1 < 50; i1++) {
for (int i2 = 0; i2 < 50; i2++) {
StatusCode[i1][i2] = 0 ;
}
}
}
void GameMap::InitializeSnake()
{
for (int i1 = 0; i1 < 50; i1++) {
for (int i2 = 0; i2 < 50; i2++) {
if(StatusCode[i1][i2]==2)
StatusCode[i1][i2] = 0;
}
}
}
第四节 主函数
76行以前就是回调+OpenGL的各种初始化。
90行即while循环之前则是imgui的初始化和页面的注册。
130行之后即while循环之后则是关闭各种东西,所以说核心代码就是while循环里面的,而while循环122行以下和99行以上的部分也只是刷屏之类的”无营养代码“,所有的核心都在99行到122行的if之中。
进入这个if的条件是currentTest不是空,整个if也是分成两大部分,一部分是推进当前页面任务(如渲染或者计时等),另外一部分就是返回主页面()。返回主页面由if (currentTest != menu && ImGui::Button(“ESC”)) 进行判断,如果当前页面currentTest 不是主页面menu 的话,会渲染一个按钮ImGui::Button(“ESC”),如果你再按下这个按钮,就会满足进入if的条件,将当前页面currentTest 换会主页面menu 了。主页面进入其他注册在主页面的副页面上面也已经说过了,就是点一下按钮就行。
另外一个渲染当前页面和计时,计时因为经过签名档初始化一秒进行144次循环,所以搞一个变量很方便解决,渲染则是用多态调用各种函数就行了。
#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include"TextTexture.h"
#include"Texture.h"
#include"TestClearColor.h"
#include"Shader.h"
#include"VertexBufferLayout.h"
#include "Renderer.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include"vender/imgui/imgui.h"
#include"vender/imgui/imgui_impl_glfw_gl3.h"
#include"GameSet.h"
//#include <GL/glut.h>
//#include<conio.h>
glm::mat4 mvp;
int x = 0, y = 0,speed=10;
char keyc='w';
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
/*if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
glfwSetWindowShouldClose(window, GL_TRUE);
if (key == GLFW_KEY_W) {
y+=10;
}else if (key == GLFW_KEY_S) {
y-=10;
}
else if (key == GLFW_KEY_A) {
x -= 10;
}
else if (key == GLFW_KEY_D) {
x += 10;
}*/
if(key>='A'&&key<='Z') keyc = key;
}
//vertexShader顶点着色器,fragmentShader是片段着色器
int main()
{
GLFWwindow* window;
if (!glfwInit())
return -1;
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//设置OpenGL的大版本为3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//设置OpenGL的小版本为3 也就是3.3的OpenGL版本
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(1000, 800, "Hello World", NULL, NULL);
glfwSetKeyCallback(window, key_callback);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's context current */
glfwMakeContextCurrent(window);
std::cout << glGetString(GL_VERSION) << std::endl;
glfwSwapInterval(1);//垂直同步,控制帧率
if (glewInit() != GLEW_OK) {
std::cout << "Error" << std::endl;
}
glEnable(GL_BLEND);//启用混合
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
Randerer renderer;
ImGui::CreateContext();
ImGui_ImplGlfwGL3_Init(window, true);
ImGui::StyleColorsDark();
bool p_open=true;
test::Test* currentTest=nullptr;
test::TestMenu* menu = new test::TestMenu(currentTest);
currentTest = menu;
menu->RegisterTest<test::TestTexture>("Start");
menu->RegisterTest<test::TestClearColor>("Help");
menu->RegisterTest<test::GameSet>("Set");
while (!glfwWindowShouldClose(window))
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
renderer.Clear();
ImGui_ImplGlfwGL3_NewFrame();
if (currentTest) {
currentTest->OnUpdata(0.0f);//更新
currentTest->OnRender();//渲染
if(currentTest->GetP_open()){
}
ImGui::Begin("Test",&p_open);
if (currentTest != menu && ImGui::Button("ESC")) {
delete currentTest;
currentTest = menu;
currentTest->SetWindow(window);
}
glfwSetKeyCallback(window, key_callback);
currentTest->SetDirction(keyc);
keyc = ';';
currentTest->SetSpeed(speed);
x++, y++;
if (x > speed) {
currentTest->SetOffset(x, y);
x = 0, y = 0;
}
currentTest->OnImGuiRender();
ImGui::End();
}
ImGui::Render();
ImGui_ImplGlfwGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);//交换缓冲区
glfwPollEvents();//轮询和处理事件
}
delete currentTest;
if (currentTest != menu) delete menu;
// Cleanup
ImGui_ImplGlfwGL3_Shutdown();
ImGui::DestroyContext();
glfwTerminate();
return 0;
}
第五节 其他页面
1、游戏设置页面GameSet
因为时间不是很够(工作日学校天天晚上断电),所以暂时只开放了蛇的速度的设定,分为三档。
#pragma once
#include"Test.h"
namespace test {
class GameSet :public Test {
public:
GameSet();
~GameSet();
void OnUpdata(float deltaTime) override;
void OnRender() override;
void OnImGuiRender() override;
void SetSpeed(int& speed) override;
private:
float m_ClearColor[4];
};
}
#include"GameSet.h"
#include<GL/glew.h>
#include"vender/imgui/imgui.h"
namespace test {
GameSet::GameSet()
:m_ClearColor{ 0.2f,0.3f,0.8f,1.0f } {
}
GameSet::~GameSet() {
}
void GameSet::OnUpdata(float deltaTime) {
}
void GameSet::OnRender() {
glClearColor(m_ClearColor[0], m_ClearColor[1], m_ClearColor[2], m_ClearColor[3]);
glClear(GL_COLOR_BUFFER_BIT);
}
void GameSet::OnImGuiRender() {
ImGui::Text("Game settings");
}
void GameSet::SetSpeed(int & speed)
{
ImGui::RadioButton("Speed Quick", &speed, 5);
ImGui::SameLine();
ImGui::RadioButton("Speed Medium", &speed, 10);
ImGui::SameLine();
ImGui::RadioButton("Speed Slow", &speed, 20);
}
}
2、游戏帮助页面GameHelp
提供游戏介绍玩法之类的东西
#pragma once
#include"Test.h"
namespace test {
class GameHelp :public Test {
public:
GameHelp();
~GameHelp();
void OnUpdata(float deltaTime) override;
void OnRender() override;
void OnImGuiRender() override;
private:
float m_ClearColor[4];
};
}
#include"GameHelp.h"
#include<GL/glew.h>
#include"vender/imgui/imgui.h"
namespace test {
GameHelp::GameHelp()
:m_ClearColor{ 0.2f,0.3f,0.8f,1.0f }{
}
GameHelp::~GameHelp(){
}
void GameHelp::OnUpdata(float deltaTime){
}
void GameHelp::OnRender(){
glClearColor(m_ClearColor[0], m_ClearColor[1], m_ClearColor[2], m_ClearColor[3]);
glClear(GL_COLOR_BUFFER_BIT);
}
void GameHelp::OnImGuiRender(){
ImGui::Text("The player uses the arrow keys to control a long snake that keeps swallowing beans, ");
ImGui::Text("while the snake's body grows longer with the swallowed beans,");
ImGui::Text("and the game ends when the snake's head hits the snake body or barrier.");
}
}
第六节 效果展现
视频审核中,审核过了应该能在主页看的到。
第七节 总结
1、失误
很多。
第一,虽然选择了imgui框架并且也是第二次使用它写代码了,但是还是非常不熟悉,加上时间有限还要debug,所以没精力慢慢写,写出来的ui好丑啊。
第二,因为写的过程中出现了 好几个bug,在检查的时候我写了一些cout函数来寻找错误点还有因为赶工不确定部分地方命名是否非常规范,本来打算写完这个之后再改改不着急的,但是真没想到这个会写这么久。
第三,部分地方有很大的进步空间,比如地图背景的渲染,墙体的设置,美术资源的优化,可以设置三级页面——开始游戏->选择关卡地图->游戏进行,以及关卡的通过设置(没精力做关卡地图了,所以直接无尽理论上不能通关了)等等地方,能做的事情还有很多
第四,一些地方代码很粗糙,比如地图的果实刷新,其实可以把还是空的的地点记录下来把果实刷在空的地方的,而我在开发的时候非常暴力地选择随机——什么随机到了就什么时候设置。
第五,待补充。