[OpenGL]简单2D游戏的实现

[OpenGL]简单2D游戏的实现

目录

一、整体代码的构成

二、Shader和Texture类的实现

1.Shader的实现

2.Texture的实现

三、角色的相关实现

3.1定时器和二维向量类的实现(Timer,Vector2)

3.2玩家类的实现(Player)

3.3子弹的命中逻辑(Bullet)

3.4敌人受击效果(Enemy)

四、碰撞检测(Collision)

五、注意事项


在学习了基本的OpenGL知识以后,其实便已经可以去实现一个小游戏了,为了巩固OpenGL的知识,这两天我尝试去实现一个小游戏,当然由于只是用于知识的巩固,这里并没有丰富很多内容,但是也粗糙的写了一下碰撞检测和动画实现等等,如果是C/C++课设有需要的话,可以拿来参考。当然你可能还会需要加入背景音乐、打击音效,进行玩法扩充,增加敌人血条等工作。

(OpenGL老师:bilibili赵新政老师赵新政的个人空间-赵新政个人主页-哔哩哔哩视频)

(游戏开发的相关知识:bilibili VoidMatrix老师Voidmatrix的个人空间-Voidmatrix个人主页-哔哩哔哩视频)

一、整体代码的构成

在文件夹当中,application是一个单例,存储了窗体信息,完成相关的初始化,同时,由于子弹和野猪的纹理资源(Textures)是所有对象所共享的(享元模式,可以减少大量加载图片资源的性能损耗),所以在这里也是提前加载好了相应的纹理资源,方便后续的使用

assets(资产)都是用来存储存放相关的资源的,这里我是放了相关的图片以及着色器(shader)代码(许多游戏都是这样放的)

character里面放的则是玩家(player),敌人(enemy),子弹(pea)的具体实现。

glframwork中放的则是OpenGL相关的着色器类(shader)的实现以及纹理类(texture)的实现

thirdparty中放的是第三方库

Application头文件代码如下:

#pragma once

#include<iostream>
#include<vector>
#include"../glframework/texture.h"

using WindowResizeCallback = void(*)(int width, int height);
using KeyBoardCallback = void(*)(int key, int action, int mods);

class GLFWwindow;
#define app Application::get_instance()

using TextureList = std::vector<Texture*>;

class Application
{

public:
	static Application* get_instance();

	bool on_init(const int& width, const int& height);
	bool on_update();
	void on_destroy();

	void set_window_resize_callback(WindowResizeCallback callback){m_frame_resize_callback = callback;}
	void set_keyboard_callback(KeyBoardCallback callback){m_keyboard_callback = callback;}

	uint32_t get_window_width() const;
    uint32_t get_window_height() const;

private:
	static void frame_buffer_size_callback(GLFWwindow* window, int width, int height);
	static void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);

	void load_bullet_texture();
    void load_enemy_texture();

private:
	Application();
	~Application();
	static Application* instance;

	uint32_t m_window_width;
	uint32_t m_window_height;
    WindowResizeCallback m_frame_resize_callback;
    KeyBoardCallback m_keyboard_callback;
	GLFWwindow* m_window{ nullptr };

public:
	TextureList texture_list_bullet;
	TextureList texture_list_enemy;
};

二、Shader和Texture类的实现

1.Shader的实现

创建Shader 的步骤便是

1.从glsl文件中获取shader的源代码

2.创建shader程序

3.为shader程序输入shader代码

4.执行shader代码编译

5.创建program并将shader编译好的结果放进去

6.执行program的链接操作,形成可执行的shader程序

当完成了shader的封装后,当我们再去需要使用shader的时候,就只需要提供对应的vertex_shader和fragment_shaderd的源代码。当然我们也需要对外提供shader的绑定与解绑,并提供修改相应uniform变量的接口(至少采样器(sampler)绑定纹理单元(unit)的接口是要有的)。

shader的头文件和具体实现如下:

shader.h

#pragma once
//加载着色器

#include"core.h"
#include<string>

class Shader
{
public:
    Shader(const char* vertex_path, const char* fragment_path);
    ~Shader();

    void on_begin() const;
    void on_end() const;

    void check_shader_error(GLuint shader, const std::string& type);
    void set_uniform_1i(const std::string& name, int value) const;
    void set_uniform_1f(const std::string& name, float value) const;

private:
    GLuint m_program;
};

shader.cpp:

#include"shader.h"

#include<iostream>
#include<fstream>
#include<sstream>
#include<string>

Shader::Shader(const char* vertex_path, const char* fragment_path)
{ 
	std::string vertex_code;
	std::string fragment_code;
	std::ifstream vertex_shader_file;
	std::ifstream fragment_shader_file;
	vertex_shader_file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
	try
	{
		vertex_shader_file.open(vertex_path);
		fragment_shader_file.open(fragment_path);

		std::stringstream vertex_shader_str, fragment_shader_str;
		vertex_shader_str << vertex_shader_file.rdbuf();
		fragment_shader_str << fragment_shader_file.rdbuf();

		vertex_shader_file.close();
		fragment_shader_file.close();

		vertex_code = vertex_shader_str.str();
		fragment_code = fragment_shader_str.str();
	}
	catch (std::ifstream::failure& e)
	{
		std::cout << "ERROR: Shader File Error" << e.what() << std::endl;
	}

	const char* vertex_shader_source = vertex_code.c_str();
	const char* fragment_shader_source = fragment_code.c_str();

	//2.创建Shader程序
	GLuint vertex_shader, fragment_shader;
	vertex_shader = glCreateShader(GL_VERTEX_SHADER);
	fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);

	//3.为Shader程序输入Shader代码
	glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
	glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);

	int su***ess = 0;
	char infoLog[1024];
	//4.执行Shader代码编译
	gl***pileShader(vertex_shader);
	//检查vertex_shader编译结果
	check_shader_error(vertex_shader, "***PILE");
	//检查fragment_shader编译结果
	gl***pileShader(fragment_shader);
	check_shader_error(fragment_shader, "***PILE");
	//5.创建一个Program壳子
	m_program = glCreateProgram();

	//6.将vs与fs编译好的结果放到program这个壳子里
	glAttachShader(m_program, vertex_shader);
	glAttachShader(m_program, fragment_shader);

	//7.执行program 的链接操作,形成最终可执行的shader程序
	glLinkProgram(m_program);
	check_shader_error(m_program, "LINK");

	//清理
	glDeleteShader(vertex_shader);
	glDeleteShader(fragment_shader);
}
Shader::~Shader()
{
    glDeleteProgram(m_program);
}
void Shader::check_shader_error(GLuint shader, const std::string& type)
{ 
    int su***ess;
    char info_log[512];
    if (type != "PROGRAM")
    {
        glGetShaderiv(shader, GL_***PILE_STATUS, &su***ess);
        if (!su***ess)
        {
            glGetShaderInfoLog(shader, 512, NULL, info_log);
            std::cout << "ERROR::SHADER::" << type << "::***PILATION_FAILED\n" << info_log << std::endl;
        }
    }

    else
    {
        glGetShaderiv(shader, GL_LINK_STATUS, &su***ess);
        if (!su***ess)
        {
            glGetShaderInfoLog(shader, 512, NULL, info_log);
            std::cout << "ERROR::SHADER::" << type << "::LINKING_FAILED\n" << info_log << std::endl;
        }
    }
    return;
}

void Shader::on_begin() const
{
    glUseProgram(m_program);
}
void Shader::on_end() const
{
    glUseProgram(0);
}

void Shader::set_uniform_1f(const std::string& name, float value) const
{
	GLuint location = glGetUniformLocation(m_program, name.c_str());
	glUniform1f(location, value);
}
void Shader::set_uniform_1i(const std::string& name, int value) const
{
	GLuint location = glGetUniformLocation(m_program, name.c_str());
	glUniform1i(location, value);
}

2.Texture的实现

实现texture的步骤便是:

1.从硬盘中加载图片资源到内存当中(由CPU完成)

2.创建纹理,激活纹理单元,绑定纹理对象

3.传输纹理数据到GPU,开辟显存空间

4.设置纹理的过滤方式

5.设置纹理的包裹方式(其实这里没有用到)

完成了Texture类的实现之后,当我们需要创建一个纹理,就只需要提供图片所在的路径和需要绑定的纹理单元即可(OpenGL的渲染需要将纹理和采样器绑定到同一个纹理单元上),此外,我们还需要提供纹理绑定的接口,告诉着色器我们当前需要进行渲染的纹理。

texture.h:

#pragma once

//加载纹理并绑定对应的纹理单元

#include"core.h"
#include<string>

class Texture
{
public:
	Texture(const std::string& path, unsigned int unit);
	~Texture();


	void bind();
	int get_width()const{return m_width;}
	int get_height()const{return m_height;}

private:
	GLuint m_texture{ 0 };
    int m_width{ 0 };
    int m_height{ 0 };
    unsigned int m_unit{ 0 };

};

texture.cpp:

#include"texture.h"
#include"../application/stb_image.h"
#include<iostream>

Texture::Texture(const std::string& path, unsigned int unit)
{
	//1.从硬盘中加载图片资源到内存当中(由CPU完成)
	int channel;
	m_unit = unit;
	stbi_set_flip_vertically_on_load(true);
	unsigned char* data = stbi_load(path.c_str(), &m_width, &m_height, &channel, STBI_rgb_alpha);
	//2.创建纹理,激活纹理单元,绑定纹理对象
	glGenTextures(1, &m_texture);
	glActiveTexture(GL_TEXTURE0 + m_unit);
	glBindTexture(GL_TEXTURE_2D, m_texture);
	//3.传输纹理数据到GPU,开辟显存空间
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_width, m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
	glGenerateMipmap(GL_TEXTURE_2D);

	//4.设置纹理的过滤方式
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);

	//5.设置值纹理的包裹方式
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

	stbi_image_free(data);
}

Texture::~Texture()
{
	if (m_texture != 0) {
		glDeleteTextures(1, &m_texture);
	}
}

void Texture::bind()
{
	glActiveTexture(GL_TEXTURE0 + m_unit);
	glBindTexture(GL_TEXTURE_2D, m_texture);
}

三、角色的相关实现

3.1定时器和二维向量类的实现(Timer,Vector2)

在进行角色实现之前,我们首先需要知道想让这样一组序列帧在窗口上“动起来”应该怎么做。

之所以能展现出“动起来的效果”,是因为当前对象每间隔一小小小段时间就会切换当前所需要渲染的纹理,并且当一组纹理走到了最后,就会从头开始继续循环上述进程。

而为了实现这样的效果,我们便需要实现定时器类(Timer),其作用便是记录从开始计时经过的时间,当达到对应的时间之后便执行相应的逻辑(回调函数)

timer.h:

#ifndef _TIMER_H_
#define _TIMER_H_

#include<functional>

class Timer
{
public:
	Timer() = default;
	~Timer() = default;

public:
	void set_wait_time(double wait_time)
	{
		this->wait_time = wait_time;
	}

	void set_one_shot(bool flag)
	{
		one_shot = flag;
	}

	void pause()
	{
		paused = true;
	}

	void resume()
	{
		paused = false;
	}

	void restart()
	{
		pass_time = 0;
		shotted = false;
	}

	void set_on_timeout(std::function<void()>on_timeout)
	{
		this->on_timeout = on_timeout;
	}

	void on_update(double delta)
	{
		if (paused)return;

		pass_time += delta;
		if (pass_time >= wait_time)
		{
			shotted = true;
			bool can_shot = (shotted && one_shot) || !one_shot;
			if (can_shot && on_timeout)
				on_timeout();

			pass_time -= wait_time;
		}
	}

private:
	double wait_time = 0;
	double pass_time = 0;
	bool one_shot = false;
	bool shotted = false;
	bool paused = false;
	std::function<void()>on_timeout;
};

#endif // !_TIMER_H_

此外,因为角色的移动需要较为准确的坐标计算,所以再实现一个Vector2类来存储坐标与碰撞体积的大小,并完成相应的运算符重载

vector2.h

#ifndef _VECTOR2_H_
#define _VECTOR2_H_

#include<cmath>

class Vector2
{
public:
	double x;
	double y;

public:
	Vector2(double x = 0, double y = 0) :x(x), y(y) {}
	~Vector2() = default;

public:
	Vector2 operator+(const Vector2& vec)const
	{
		return Vector2(x + vec.x, y + vec.y);
	}

	void operator+=(const Vector2& vec)
	{
		x += vec.x, y += vec.y;
	}

	Vector2 operator-(const Vector2& vec)const
	{
		return Vector2(x - vec.x, y - vec.y);
	}

	void operator-=(const Vector2& vec)
	{
		x -= vec.x, y -= vec.y;
	}

	double operator*(const Vector2& vec)const
	{
		return x * vec.x + y * vec.y;
	}

	Vector2 operator*(double val)const
	{
		return Vector2(val * x, y * val);
	}

	void operator*=(double val)
	{
		x *= val, y *= val;
	}

	double length()const
	{
		return sqrt(x * x + y * y);
	}

	Vector2 normalize()const
	{
		double len = this->length();
		if (abs(len) <= 0.00001)
			return Vector2();

		return Vector2(x / len, y / len);
	}
};

#endif // !_VECTOR2_H_

3.2玩家类的实现(Player)

在我一开始去设计游戏对象的时候,最先考虑的就是在OpenGL中如何去实现他们的单独渲染,并且不会影响到其他对象的渲染,这在EasyX和SDL中基本上是不需要考虑的,而由于在OpenGL当中,所有的数据都需要与当前的OpenGL状态机绑定,也就是说,如果没有考虑OpenGL的当前状态,那么就会产生事与愿违的结果。

于是,我的考虑是将vao,ebo等数据都存储到游戏对象类的内部,在更新数据的时候根据玩家的位置和尺寸大小实时更新当前vao中的位置数据,同时,私有成员中设置timer_animation定时器用来控制动画的播放,idx来记录当前动画的索引,并且使用bool变量来记录当前的案件数据,m_unit来表示当前动画纹理绑定的纹理单元编号。

于是在on_update更新函数当中,只需要更新当前的vao位置数据与相关定时器的更新,在on_render渲染函数当中,只需要传入对应的着色器并绑定当前vao进行渲染即可。

player.h:

#pragma once

#include"../glframework/core.h"
#include"../glframework/texture.h"
#include"../glframework/shader.h"

#include<vector>
#include"vector2.h"
#include"timer.h"
using TextureList = std::vector<Texture*>;


class Player
{
public:
	Player();
	~Player() = default;

	void on_update(float delta);
	void on_render(Shader* shader);

	void key_up(bool flag) { is_up_key_down = flag; }
	void key_down(bool flag) { is_down_key_down = flag; }

	const Vector2 get_position_ndc() const { return position_ndc; }
	const Vector2 get_size()const { return size_ndc; }

private:
	void on_key(int key, int action, int mods);
	

private:
	GLuint m_vao, m_ebo;
	TextureList texture_list;
	int m_unit = 1;
	Vector2 position_ndc = { -1.0,0.0 };
	Vector2 size_ndc = { 71.0 / 640,71.0 / 360 };
	bool is_up_key_down = false;
	bool is_down_key_down = false;
	float velocity_y = 1.0;

	Timer timer_animation;
	int idx = 0;
};

player.cpp:

#include"player.h"
#include<iostream>
#include"../application/Application.h"

Player::Player()
{
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_0.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_1.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_2.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_3.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_4.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_5.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_6.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_7.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_8.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_9.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_10.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_11.png", m_unit));
	texture_list.push_back(new Texture("assets/textures/PeaShooter/Peashooter_12.png", m_unit));

	timer_animation.set_wait_time(0.07f);
	timer_animation.set_one_shot(false);
	timer_animation.set_on_timeout([&]()
		{
			idx = (idx + 1) % 13;
			texture_list[idx]->bind();
		});
	
	glGenVertexArrays(1, &m_vao);

	static unsigned int indices[] = {
		0,1,2,
		2,1,3,
	};
	glGenBuffers(1, &m_ebo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

}

void Player::on_update(float delta)
{
	
	timer_animation.on_update(delta);
	float scale = 1.0;
	
	int move_dir = is_up_key_down - is_down_key_down;
	position_ndc.y = (position_ndc.y + move_dir * delta * velocity_y);
	if (position_ndc.y - size_ndc.y < -1.0f)
		position_ndc.y = -1.0f + size_ndc.y;
    if(position_ndc.y > 1.0f)
		position_ndc.y = 1.0f;

	float positions[] = {
		position_ndc.x,position_ndc.y - size_ndc.y * scale,0.0f,
		position_ndc.x + size_ndc.x * scale,position_ndc.y - size_ndc.y * scale,0.0f,
		position_ndc.x,position_ndc.y,0.0f,
		position_ndc.x + size_ndc.x * scale,position_ndc.y,0.0f,
	};

	static float uvs[] = {
		0.0f,0.0f,
		1.0f,0.0f,
		0.0f,1.0f,
		1.0f,1.0f,
	};
	

	GLuint vbo_pos, vbo_uv;
	glGenBuffers(1, &vbo_pos);
	glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
	glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
	glGenBuffers(1, &vbo_uv);
	glBindBuffer(GL_ARRAY_BUFFER, vbo_uv);
	glBufferData(GL_ARRAY_BUFFER, sizeof(uvs), uvs, GL_STATIC_DRAW);

	glBindVertexArray(m_vao);
	//vao绑定
	glBindBuffer(GL_ARRAY_BUFFER, vbo_pos);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3, (void*)0);
	glBindBuffer(GL_ARRAY_BUFFER, vbo_uv);
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, (void*)0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
	glBindVertexArray(0);

	glDeleteBuffers(1, &vbo_pos);
    glDeleteBuffers(1, &vbo_uv);

}

void Player::on_render(Shader*shader)
{
	shader->on_begin();
	shader->set_uniform_1i("sampler", m_unit);
	glBindVertexArray(m_vao);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	shader->on_end();
	glBindVertexArray(0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

其实,接下来的子弹类(Bullet)与敌人类(Enemy)的实现与上述大同小异,而仅有的不同便是:1.子弹在命中敌人后会播放破碎动画 2.敌人在受到攻击后会泛红

那么接下来仅讨论这两处的实现。

3.3子弹的命中逻辑(Bullet)

子弹在命中敌人之后直接消失是一种十分简单的实现,而有时候我们会需要在子弹命中后进行相应的动画播放,比如爆炸,破碎等。而在这一段时间子弹是存在的,我们还需要保证其碰撞逻辑不会再其命中之后的几帧当中重复执行,所以我们就需要两个状态变量bool isnt_collide = true;
bool is_valid = true;,isnt_collide表示其是否可以执行碰撞逻辑,而is_valid则表示当前子弹是否需要从内存中销毁(销毁后就不会再进行渲染了),所以我们便需要一个定时器timer_collide_callback来控制其再碰撞之后播放动画的时间,当动画播放结束is_valid设置为false,该子弹便应该从内存中被清除。

所以在on_update当中:

if(isnt_collide)
	timer_animation.on_update(delta);
else 
    timer_collide_callback.on_update(delta);

相关定时器的设置:

timer_animation.set_wait_time(0.1f);
timer_animation.set_one_shot(false);
timer_animation.set_on_timeout([&]()
	{
		idx = (idx + 1) % (app->texture_list_bullet.size() - 1);
		app->texture_list_bullet[idx]->bind();
	});

timer_collide_callback.set_one_shot(true);
timer_collide_callback.set_wait_time(0.1f);
timer_collide_callback.set_on_timeout([&]()
	{
		is_valid = false;
	});

3.4敌人受击效果(Enemy)

体现子弹的命中一方面是命中后的子弹动画,一个便是敌人受击的效果。

在展示的gif图当中,我们可以看到敌人受到攻击后会变红。而我们知道,进行渲染的是着色器,所以想要实现这个效果,就需要对片元着色器(fragment_shader)进行相应的修改。

#version 460 core

in vec2 uv;
out vec4 FragColor;
uniform sampler2D sampler;

uniform int invulnerable=0;


void main()
{
	vec4 tmp_color = texture(sampler, uv);
	if(invulnerable==1)
	{
		if(tmp_color.a>0.1)
			FragColor = vec4(1.0,0.0,0.0,0.4);
        else
			FragColor = vec4(0.0,0.0,0.0,0.0);
	}
	else
		FragColor = tmp_color;
}

在片元着色器中,我们设置了uniform变量invulnerable来表示当前对象处于受击的无敌状态,当其处于该状态的时候,对当前传入的像素点数据进行检查,如果他是透明数据,则保持原来的效果。如果不是,则更改FragColor,让其RGBA值为(1.0,0.0,0.0,0.4),也就是全红,透明度为0.4(透明度太满感觉会比较突兀)。

而相应的,在enemy类当中的具体逻辑实现当中便需要设置enemy对象处于屏闪状态的时间以及单个屏闪的持续时间(这个设置时间太短的话会导致闪的很快)

所以便需要两个状态变量,is_invulnerable记录是否处于屏闪状态与is_blinking用来记录是否要展示受击效果,当然其也有对应的定时器timer_invulnerable和time_blink。

于是其定时器的设置便是:

timer_blink.set_wait_time(0.1f);
timer_blink.set_one_shot(false);
timer_blink.set_on_timeout([&]()
	{
		is_blinking= !is_blinking;
	});

timer_invulnerable.set_wait_time(0.15f);
timer_invulnerable.set_one_shot(true);
timer_invulnerable.set_on_timeout([&]()
	{
		is_invulnerable = false;
		is_blinking = false;
	});

而在on_update更新函数当中,我们便需要根据当前状态对定时器进行更新:

timer_animation.on_update(delta);
if(is_invulnerable)
	timer_blink.on_update(delta);
timer_invulnerable.on_update(delta);

在on_render渲染函数当中,就需要根据当前状态设置shader中uniform变量的值:

void Enemy::on_render(Shader* shader)
{
	app->texture_list_enemy[idx]->bind();
	shader->on_begin();
	shader->set_uniform_1i("sampler", m_unit);
	if (is_blinking)
		shader->set_uniform_1i("invulnerable", 1);
	glBindVertexArray(m_vao);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
	shader->set_uniform_1i("invulnerable", 0);
	shader->on_end();
	glBindVertexArray(0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}

四、碰撞检测(Collision)

这里的话是只实现了子弹与敌人的碰撞逻辑检测,其实现方式是判定点在矩形内部(实现简单而且比较符合人的感官)

这里是将检测的部分放到了Enemy类当中,传入当前的子弹对象,获取其坐标,进行碰撞检测并更改相关值。

void Enemy::check_collision_to_bullet(Pea&bullet)
{
	const Vector2& pos = bullet.get_position_ndc();
	if (pos.x >= position_ndc.x && pos.x <= (position_ndc.x + size_ndc.x)
		&& pos.y >= (position_ndc.y - size_ndc.y) && pos.y <= position_ndc.y)
	{
		bullet.on_collide();
		hp--;
		is_invulnerable = true;
		is_blinking = true;
		timer_blink.restart();
		timer_invulnerable.restart();
		if (hp <= 0)
		{
			is_valid = false;
		}
	}
}

主函数中的碰撞检测代码:

for (auto& pea : pea_list)
{
	if (!pea->can_collide())
		break;
	for (auto& enemy : enemy_list)
	{
        enemy->check_collision_to_bullet(*pea);
	}
}
for (auto& enemy : enemy_list)
{
	enemy->on_update(delta);
	enemy_list.erase(remove_if(enemy_list.begin(), enemy_list.end(),
		[](Enemy* enemy) {
			if (enemy->can_remove())
			{
				delete enemy;
				return true;
			}
			return false;
		}),
		enemy_list.end());
}
for (auto& pea : pea_list)
{
	pea->on_update(delta);
	pea_list.erase(remove_if(pea_list.begin(), pea_list.end(),
		[](Pea* pea) {
			if (pea->can_remove())
			{
				delete pea;
				return true;
			}
			return false;
		}),
		pea_list.end());
}

五、注意事项

由于我是将vao的更新放在了on_update函数当中,那么每一帧的画面更新当中都会创建相应的vbo和uv数据,必须在绑定完vao之后将这些数据进行销毁,否则你的内存将会很快炸掉。

在主函数的运行当中并没有控制帧率,如果需要的话可以去写一下

CSDN的下载规则我并不是很懂,不知道是不是强制要VIP,需要源代码的话可以私信

转载请说明出处内容投诉
CSS教程网 » [OpenGL]简单2D游戏的实现

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买