通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结

对GitHub上的learnOpenGL教程的学习已经接近尾声了,可以说这个教程完美的衔接了平时教学中一直用的旧OpenGL渲染的方式和现在流行的OpenGL,这个消砖块游戏也是教程最后的一个实战,内容很简洁但是包含了OpenGL几乎所有最基础的内容,下面也会逐一提及到,这里主要就是对这些最基础内容的总结和回顾。

用到的素材和全部代码我都上传到了我的资源中:源码及素材
不知道为什么资源中积分设置成0还要积分,我也上传了百度网盘:链接 提取码:bwwg

这是几个不同地图的效果:
通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结_第1张图片
通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结_第2张图片

下面我们来看看几个关键的地方:

游戏中涉及到OpenGL渲染中最基础的地方

游戏中精灵的加载:

显然这就是最基础的东西,它会涉及到各种顶点属性、纹理、坐标系、各种变换、着色器的使用等等等等。这些东西确实是墨守成规的,没有什么可简化的,但是如果我们要把他们结合起来用高效的类来灵活的绘制所有的精灵呢?这就需要总结这些知识到代码中。

(1)单个精灵属性的类GameObject:

#ifndef GAMEOBJECT_H
#define GAMEOBJECT_H

#include "SpriteRenderer.h"
#include 

// Container object for holding all state relevant for a single
// game object entity. Each object in the game likely needs the
// minimal of state as described within GameObject.
class GameObject
{
public:
    // Object state
    glm::vec2   Position, Size, Velocity;//精灵左上角的位置、长宽、速度
    glm::vec3   Color;
    GLfloat     Rotation;
    GLboolean   IsSolid;//是否是实体(不可毁灭)
    GLboolean   Destroyed;//是否被毁灭
    // Render state
    Texture2D   Sprite;	//精灵的纹理
    // Constructor(s)
    GameObject();
    GameObject(glm::vec2 pos, glm::vec2 size, Texture2D sprite, glm::vec3 color = glm::vec3(1.0f), glm::vec2 velocity = glm::vec2(0.0f, 0.0f));
    // Draw sprite
    virtual void Draw(SpriteRenderer &renderer);//绘制精灵
};

#endif

对于消砖块来说,我们需要的也就是这些属性了,并且我们还要灵活的通过简单的调用Draw函数实现精灵的实时绘制:

void GameObject::Draw(SpriteRenderer &renderer)
{
    renderer.DrawSprite(this->Sprite, this->Position, this->Size, this->Rotation, this->Color);
}

(2)Draw函数用到了精灵的实时绘制类SpriteRenderer:

#ifndef SPRITE_RENDERER_H
#define SPRITE_RENDERER_H

#include "Texture.h"
#include "Shader.h"
#include 
#include 

class SpriteRenderer
{
public:
    // Constructor (inits shaders/shapes)
    SpriteRenderer(Shader &shader);
    // Destructor
    ~SpriteRenderer();
    // Renders a defined quad textured with given sprite
    void DrawSprite(Texture2D &texture, glm::vec2 position, glm::vec2 size = glm::vec2(10.0f, 10.0f), float rotate = 0.0f, glm::vec3 color = glm::vec3(1.0f));
private:
    // Render state
    Shader shader; 
    unsigned int quadVAO;//精灵的顶点数组索引
    // Initializes and configures the quad's buffer and vertex attributes
    void initRenderData();//初始化绘制的数据
};

对应的函数实现:

#include "SpriteRenderer.h"

SpriteRenderer::SpriteRenderer(Shader &shader)//把用到的着色器传进来
{
    this->shader = shader;
    this->initRenderData();
}

SpriteRenderer::~SpriteRenderer()
{
    glDeleteVertexArrays(1, &this->quadVAO);
}
//下面正式的绘制
void SpriteRenderer::DrawSprite(Texture2D &texture, glm::vec2 position, glm::vec2 size, float rotate, glm::vec3 color)
{
    // prepare transformations
    this->shader.Use();
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::translate(model, glm::vec3(position, 0.0f));  // first translate (transformations are: scale happens first, then rotation, and then final translation happens; reversed order)
	//下面先把物体中心从左上角(0,0)平移到中心(0,0),再进行旋转,在变回左上角为中心的
    model = glm::translate(model, glm::vec3(0.5f * size.x, 0.5f * size.y, 0.0f)); // move origin of rotation to center of quad
    model = glm::rotate(model, glm::radians(rotate), glm::vec3(0.0f, 0.0f, 1.0f)); // then rotate
    model = glm::translate(model, glm::vec3(-0.5f * size.x, -0.5f * size.y, 0.0f)); // move origin back

    model = glm::scale(model, glm::vec3(size, 1.0f)); // last scale

    this->shader.SetMatrix4("model", model);

    // render textured quad
    this->shader.SetVector3f("spriteColor", color);

    glActiveTexture(GL_TEXTURE0);
    texture.Bind();

    glBindVertexArray(this->quadVAO);
    glDrawArrays(GL_TRIANGLES, 0, 6);
    glBindVertexArray(0);
}
//对于顶点数组的绑定
void SpriteRenderer::initRenderData()
{
    // configure VAO/VBO
    unsigned int VBO;
    float vertices[] = { 
        // pos      // tex
        0.0f, 1.0f, 0.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 0.0f,
        0.0f, 0.0f, 0.0f, 0.0f, 

        0.0f, 1.0f, 0.0f, 1.0f,
        1.0f, 1.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 1.0f, 0.0f
    };

    glGenVertexArrays(1, &this->quadVAO);
    glGenBuffers(1, &VBO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindVertexArray(this->quadVAO);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

这里绘制用到的知识简直基础的不能再基础了,我们只需要配置了GameObject中的各种属性,再调用Draw函数就可以轻松的把任何精灵绘制到任何地方。

(3)关于纹理和着色器:

上面还用到了 “Texture.h” 和"Shader.h",这都是为了很方便的加载纹理和着色器,关于纹理的配置和着色器的搭建用到的函数都是最基础的内容,看资源中的代码即可。同样由于精灵的绘制少不了这两者的结合使用,所以我们也写了一个Resourc_management.h类用来直接去配置纹理和着色器:

class ResourceManager
{
public:
    // resource storage
    static std::map<std::string, Shader>    Shaders;
    static std::map<std::string, Texture2D> Textures;
    // 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 char *vShaderFile, const char *fShaderFile, const char *gShaderFile, std::string name);
    // retrieves a stored sader
    static Shader    GetShader(std::string name);
    // loads (and generates) a texture from file
    static Texture2D LoadTexture(const char *file, bool alpha, std::string name);
    // retrieves a stored texture
    static Texture2D GetTexture(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 char *vShaderFile, const char *fShaderFile, const char *gShaderFile = nullptr);
    // loads a single texture from file
    static Texture2D loadTextureFromFile(const char *file, bool alpha);
};

这里只有纹理和2D精灵,所以顶点和片段着色器很简单:

#version 330 core
layout (location = 0) in vec4 vertex; // 

out vec2 TexCoords;

uniform mat4 model;
uniform mat4 projection;

void main()
{
    TexCoords = vertex.zw;
    gl_Position = projection * model * vec4(vertex.xy, 0.0, 1.0);
}
#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D image;
uniform vec3 spriteColor;

void main()
{    
    color = vec4(spriteColor, 1.0) * texture(image, TexCoords);
}  

(4)特殊的精灵我们继承GameObject:

比如球我们还会有别的属性,继承GameObject很方便。

#include "texture.h"
#include "SpriteRenderer.h"
#include "BallObject.h"
#include "GameObject.h"
// BallObject holds the state of the Ball object inheriting
// relevant state data from GameObject. Contains some extra
// functionality specific to Breakout's ball object that
// were too specific for within GameObject alone.
class BallObject : public GameObject
{
public:
    // Ball state	
    //除了GameObject中的属性,对于球还会有半径、是否在板子上
    GLfloat   Radius;
    GLboolean Stuck;
    // Constructor(s)
    BallObject();
    BallObject(glm::vec2 pos, GLfloat radius, glm::vec2 velocity, Texture2D sprite);
    // Moves the ball, keeping it constrained within the window bounds (except bottom edge); returns new position
    glm::vec2 Move(GLfloat dt, GLuint window_width);
    // Resets the ball to original state with given position and velocity
    void  Reset(glm::vec2 position, glm::vec2 velocity);
};

游戏编程的一些小技巧

游戏类:

#ifndef GAME_H
#define GAME_H

#include "GameLevel.h"
#include 
#include 
#include "BallObject.h"

// Represents the current state of the game
enum GameState {
    GAME_ACTIVE,
    GAME_MENU,
    GAME_WIN
};
//球与砖碰撞的方向
enum Direction {
    UP,
    RIGHT,
    DOWN,
    LEFT
};   
// Game holds all game-related state and functionality.
// Combines all game-related data into a single class for
// easy access to each of the components and manageability.
class Game
{
public:
    // game state
    GameState               State;	
    bool                    Keys[1024];//对输入的判定!!!很关键!!!
    unsigned int            Width, Height;
	//游戏级别
	std::vector<GameLevel> Levels;
    GLuint                 Level;
    // constructor/destructor
    Game(unsigned int width, unsigned int height);
    ~Game();
    // initialize game state (load all shaders/textures/levels)
    void Init();
	//碰撞检测
	GLboolean CheckCollision(GameObject &one, GameObject &two);
	GLboolean CheckCollision(BallObject &one, GameObject &two);// AABB - Circle collision
	void DoCollisions();
    // game loop
    void ProcessInput(float dt);
    void Update(float dt);
    void Render();
	// reset
    void ResetLevel();
    void ResetPlayer();
};

#endif

(1)用Keys[1024]来控制用户的交互

因为按键都有自己的宏定义,完全可以用对应下标的数组表示:

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    // when a user presses the escape key, we set the WindowShouldClose property to true, closing the application
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
    if (key >= 0 && key < 1024)//Keys[1024]
    {//只要按下对应的键,对应数组下标的元素就会变为true
        if (action == GLFW_PRESS)
            Breakout.Keys[key] = true;
        else if (action == GLFW_RELEASE)
            Breakout.Keys[key] = false;
    }

我们在下面就可根据输入的键控制精灵移动:

void Game::ProcessInput(float dt)
{
   if (this->State == GAME_ACTIVE)
    {
        GLfloat velocity = PLAYER_VELOCITY * dt;
        // 移动挡板
        if (this->Keys[GLFW_KEY_A])
        {
            if (Player->Position.x >= 0.0f)
                Player->Position.x -= velocity;
			if (Ball->Stuck)
                Ball->Position.x -= velocity;
        }
        if (this->Keys[GLFW_KEY_D])
        {
            if (Player->Position.x <= this->Width - Player->Size.x)
                Player->Position.x += velocity;
			if (Ball->Stuck)
                    Ball->Position.x += velocity;
        }
        if (this->Keys[GLFW_KEY_W])
        {
            if (Player->Position.y >= 0.0f)
                Player->Position.y -= velocity/5;
			if (Ball->Stuck)
                    Ball->Position.y -= velocity/5;
        }
        if (this->Keys[GLFW_KEY_S])
        {
			if (Player->Position.y <= this->Height - Player->Size.y)
                Player->Position.y += velocity/5;
			if (Ball->Stuck)
                    Ball->Position.y += velocity/5;
        }
		//空格发射小球
		if (this->Keys[GLFW_KEY_SPACE])
            Ball->Stuck = false;
		//改变地图
		/*if(this->Keys[GLFW_KEY_0])
			level=0;
		if(this->Keys[GLFW_KEY_1])
			level=1;
		if(this->Keys[GLFW_KEY_2])
			level=2;
		if(this->Keys[GLFW_KEY_3])
			level=3;*/
    }
}

(2)游戏级别/地图/关卡的加载:

这里的思路就是用二维数组去显示地图中一些固定不变的精灵,对应的数组值显示在对应下标(i,j)代表的位置:
通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结_第3张图片

class GameLevel
{
public:
    // level state
    std::vector<GameObject> Bricks;
    // constructor
    GameLevel() { }
    // loads level from file
    void Load(const char *file, unsigned int levelWidth, unsigned int levelHeight);
    // render level
    void Draw(SpriteRenderer &renderer);
    // check if the level is completed (all non-solid tiles are destroyed)
    bool IsCompleted();
private:
    // initialize level from tile data
    void init(std::vector<std::vector<unsigned int>> tileData, unsigned int levelWidth, unsigned int levelHeight);
};

现在我们就把地图加载进二维数组中:

void GameLevel::Load(const char *file, unsigned int levelWidth, unsigned int levelHeight)
{
    // clear old data
    this->Bricks.clear();
    // load from file
    unsigned int tileCode;
    GameLevel level;
    std::string line;
    std::ifstream fstream(file);
    std::vector<std::vector<unsigned int>> tileData;
    if (fstream)
    {//一行行的把地图数据读入tileData中
        while (std::getline(fstream, line)) // read each line from level file
        {
            std::istringstream sstream(line);
            std::vector<unsigned int> row;
            while (sstream >> tileCode) // read each word seperated by spaces
                row.push_back(tileCode);
            tileData.push_back(row);
        }
        if (tileData.size() > 0)
            this->init(tileData, levelWidth, levelHeight);
    }
}

这里只有std::vector Bricks;并没有二维数组成员变量是因为tileData作为参数进行数据的传递即可,真正的目的还是得到每个砖块的颜色、位置、纹理等属性:

void GameLevel::init(std::vector<std::vector<unsigned int>> tileData, unsigned int levelWidth, unsigned int levelHeight)
{
    // calculate dimensions
    unsigned int height = tileData.size();
    unsigned int width = tileData[0].size(); // note we can index vector at [0] since this function is only called if height > 0
    float unit_width = levelWidth / static_cast<float>(width), unit_height = levelHeight / height; 
    // initialize level tiles based on tileData		
    for (unsigned int y = 0; y < height; ++y)
    {
        for (unsigned int x = 0; x < width; ++x)
        {
            // check block type from level data (2D level array)
            if (tileData[y][x] == 1) // solid
            {
                glm::vec2 pos(unit_width * x, unit_height * y);
                glm::vec2 size(unit_width, unit_height);
                GameObject obj(pos, size, ResourceManager::GetTexture("block_solid"), glm::vec3(0.8f, 0.8f, 0.7f));
                obj.IsSolid = true;
                this->Bricks.push_back(obj);
            }
            else if (tileData[y][x] > 1)	// non-solid; now determine its color based on level data
            {
                glm::vec3 color = glm::vec3(1.0f); // original: white
                if (tileData[y][x] == 2)
                    color = glm::vec3(0.2f, 0.6f, 1.0f);
                else if (tileData[y][x] == 3)
                    color = glm::vec3(0.0f, 0.7f, 0.0f);
                else if (tileData[y][x] == 4)
                    color = glm::vec3(0.8f, 0.8f, 0.4f);
                else if (tileData[y][x] == 5)
                    color = glm::vec3(1.0f, 0.5f, 0.0f);

                glm::vec2 pos(unit_width * x, unit_height * y);
                glm::vec2 size(unit_width, unit_height);
                this->Bricks.push_back(GameObject(pos, size, ResourceManager::GetTexture("block"), color));
            }
        }
    }
}

这样我们就可以直接调用前面的Draw函数去很容易的绘制砖块了:

void GameLevel::Draw(SpriteRenderer &renderer)
{
	/*
    for (GameObject &tile : this->Bricks)
        if (!tile.Destroyed)
            tile.Draw(renderer);
	*/
	for(int i=0;i<this->Bricks.size();i++){
		GameObject tile=Bricks[i];
		if (!tile.Destroyed)
            tile.Draw(renderer);
	}
}

(3)碰撞检测和处理

GLboolean CheckCollision(GameObject &one, GameObject &two);
GLboolean CheckCollision(BallObject &one, GameObject &two);// AABB - Circle collision
void DoCollisions();

这是极其重要的地方:
CheckCollision碰撞检测有很多种方式,这个游戏中只涉及到砖块、小球和挡板之间的碰撞,相当于只要矩形和矩形之间、矩形和圆形之间的碰撞,其实仔细想想也知道大部分的2d游戏也都只有这两种情况,毕竟我们总会把复杂的物体简单化,让它在数学上更方便:

  • 矩形和矩形:
    通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结_第4张图片
GLboolean CheckCollision(GameObject &one, GameObject &two) // AABB - AABB collision
{
    // x轴方向碰撞?
    bool collisionX = one.Position.x + one.Size.x >= two.Position.x &&
        two.Position.x + two.Size.x >= one.Position.x;
    // y轴方向碰撞?
    bool collisionY = one.Position.y + one.Size.y >= two.Position.y &&
        two.Position.y + two.Size.y >= one.Position.y;
    // 只有两个轴向都有碰撞时才碰撞
    return collisionX && collisionY;
}
  • 矩形和圆形
    通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结_第5张图片

显然P是距离圆最近的点。
这里的关键就在于判断CP和radius的大小关系,只有P是未知的,显然我们只需要通过限制运算把D限制在半边内,并返回限制后的值clamped加上B即为点P。限制运算通常可以表示为:

float clamp(float value, float min, float max) {
    return std::max(min, std::min(max, value));
}

例如,值42.0f被限制到6.0f和3.0f之间会得到6.0f;而4.20f会被限制为4.20f。
限制一个2D的矢量表示将其x和y分量都限制在给定的范围内,只要有一个(x/y)到达范围,另一个也就停在当下了

GLboolean CheckCollision(BallObject &one, GameObject &two) // AABB - Circle collision
{
    // 获取圆的中心 
    glm::vec2 center(one.Position + one.Radius);
    // 计算AABB的信息(中心、半边长)
    glm::vec2 aabb_half_extents(two.Size.x / 2, two.Size.y / 2);
    glm::vec2 aabb_center(
        two.Position.x + aabb_half_extents.x, 
        two.Position.y + aabb_half_extents.y
    );
    // 获取两个中心的差矢量
    glm::vec2 difference = center - aabb_center;
    glm::vec2 clamped = glm::clamp(difference, -aabb_half_extents, aabb_half_extents);
    // AABB_center加上clamped这样就得到了碰撞箱上距离圆最近的点closest
    glm::vec2 closest = aabb_center + clamped;
    // 获得圆心center和最近点closest的矢量并判断是否 length <= radius
    difference = closest - center;
    return glm::length(difference) < one.Radius;
}    

有了这两个函数我们只需要遍历所有的砖块看他们是否与小球相碰即可。

解决了碰撞检测的问题,同样还面临如何处理碰撞的问题:

  • 首先小球碰到砖块后会反弹,如何得知小球碰的哪个面?
    利用向量的点积:上下左右四个方向向量与小球的位置向量的点积的值最大者(夹角余弦值最大)即为小球最接近的方向。在Game类中我们已经声明了一个关于方向的枚举Direction。
Direction VectorDirection(glm::vec2 target)
{
    glm::vec2 compass[] = {
        glm::vec2(0.0f, 1.0f),  // 上
        glm::vec2(1.0f, 0.0f),  // 右
        glm::vec2(0.0f, -1.0f), // 下
        glm::vec2(-1.0f, 0.0f)  // 左
    };
    GLfloat max = 0.0f;
    GLuint best_match = -1;
    for (GLuint i = 0; i < 4; i++)
    {
        GLfloat dot_product = glm::dot(glm::normalize(target), compass[i]);
        if (dot_product > max)
        {
            max = dot_product;
            best_match = i;
        }
    }
    return (Direction)best_match;
}

VectorDirection在 CheckCollision中完成即可。可以设置全局变量,也可以用std::tuple返回一个容器。

  • 得知了碰撞的方向,接下来就是考虑在何时反弹,以及小球的重定位:
    通过消砖块的游戏对OpenGL(glfw)、图形学、游戏编程的一个小的总结_第6张图片
    重定位关键就在求出要偏移出砖块的量R,前面我们已经知道怎么求P了,这里我们用C-P得到即可,同样这个计算可以很方便的在 CheckCollision中完成即可。可以设置全局变量,也可以用std::tuple返回一个容器。

有了上面这些数据,我们可以进行砖和球的碰撞检测和处理了:

void Game::DoCollisions()
{
	for(int i=0;i<this->Levels[this->Level].Bricks.size();i++){//遍历所有砖块
		GameObject box=this->Levels[this->Level].Bricks[i];//当前砖块
		if (!box.Destroyed){
            if (CheckCollision(*Ball, box)){
                if (!box.IsSolid)
					this->Levels[this->Level].Bricks[i].Destroyed = GL_TRUE;
				if (dir == LEFT || dir == RIGHT) // horizontal collision
                {
                    Ball->Velocity.x = -Ball->Velocity.x; // reverse horizontal velocity
                    // relocate
                    float penetration = Ball->Radius - std::abs(R.x);
                    if (dir == LEFT)
                        Ball->Position.x += penetration; // move ball to right
                    else
                        Ball->Position.x -= penetration; // move ball to left;
                }
                else // vertical collision
                {
                    Ball->Velocity.y = -Ball->Velocity.y; // reverse vertical velocity
                    // relocate
                    float penetration = Ball->Radius - std::abs(R.y);
                    if (dir == UP)
                        Ball->Position.y -= penetration; // move ball back up
                    else
                        Ball->Position.y += penetration; // move ball back down
                }
            }
        }
	}
	。。。。。。

之后还有玩家控制的挡板和球的碰撞:

//球和玩家之间的碰撞:撞击点距离挡板的中心点越远,则水平方向的速度就会越大。
	if (CheckCollision(*Ball,*Player)&&!Ball->Stuck){
		// 检查碰到了挡板的哪个位置,并根据碰到哪个位置来改变速度
        GLfloat centerBoard = Player->Position.x + Player->Size.x / 2;
        GLfloat distance = (Ball->Position.x + Ball->Radius) - centerBoard;
        GLfloat percentage = distance / (Player->Size.x / 2);
		 // 依据结果移动
        GLfloat strength = 2.0f;
        glm::vec2 oldVelocity = Ball->Velocity;
        Ball->Velocity.x = INITIAL_BALL_VELOCITY.x * percentage * strength; 
        //Ball->Velocity.y = -Ball->Velocity.y;粘板问题
		//由于我们没有考虑球的中心在AABB内部的情况,游戏会持续试图对所有的碰撞做出响应,
		//当球最终脱离时,已经对y向速度翻转了多次,以至于无法确定球在脱离后是向上还是向下运动。
		Ball->Velocity.y = -1 * abs(Ball->Velocity.y);  
		//要存储旧的速度是因为我们只更新球的速度矢量中水平方向的速度并保持它的y速度不变
        Ball->Velocity = glm::normalize(Ball->Velocity) * glm::length(oldVelocity);
	}
}  

我们可以引入一个小的特殊处理来很容易地修复粘板问题,这个处理之所以成为可能是基于我们可以假设碰撞总是发生在挡板顶部的事实。我们总是简单地返回正的y速度而不是反转y速度,这样当它被卡住时也可以立即脱离。

//Ball->Velocity.y = -Ball->Velocity.y;
Ball->Velocity.y = -1 * abs(Ball->Velocity.y);  

如果足够仔细就会觉得这一影响仍然是可以被注意到的,但是我个人将此方法当作一种可接受的折衷处理。

然而:

在视频游戏的发展过程中,碰撞检测是一个困难的话题甚至可能是最大的挑战。大多数的碰撞检测和处理方案是和物理引擎合并在一起的,正如多数现代的游戏中看到的那样。我们在Breakout游戏中使用的碰撞方案是一个非常简单的方案并且是专门给这类游戏所专用的。

需要强调的是这类碰撞检测和处理方式是不完美的。它只能计算每帧内可能发生的碰撞并且只能计算在该时间步时物体所在的各位置;这意味着如果一个物体拥有一个很大的速度以致于在一帧内穿过了另一个物体,它将看起来像是从来没有与另一个物体碰撞过。因此如果出现掉帧或出现了足够高的速度,这一碰撞检测方案将无法应对。

(我们使用的碰撞方案)仍然会出现这几个问题:

  • 如果球运动得足够快,它可能在一帧内完整地穿过一个物体,而不会检测到碰撞。
  • 如果球在一帧内同时撞击了一个以上的物体,它将会检测到两次碰撞并两次反转速度;这样不改变它的原始速度。
  • 撞击到砖块的角时会在错误的方向反转速度,这是因为它在一帧内穿过的距离会引发VectorDirection返回水平方向还是垂直方向的差别。

最后

这里写的这些也只是我认为让我很有收获的地方,教程中学到的东西也远不止这些,但这些都是最基础的,也为后面我们做一些有趣的项目提供了很好的框架,然而还有一些游戏中更难的东西,比如3维画面、光照计算、精确的物理定位、数据结构的简化。。。并未涉及,这些我们都可以为他们创建单独的类或者增加成员函数来实现。

有了上面的类我们在主程序中的事情就变得十分简单了:

#include "Game.h"
#include 
#include "Resourc_management.h"
#include 

// GLFW function declerations
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

// The Width of the screen
const unsigned int SCREEN_WIDTH = 800;
// The height of the screen
const unsigned int SCREEN_HEIGHT = 600;

Game Breakout(SCREEN_WIDTH, SCREEN_HEIGHT);

int main(int argc, char *argv[])
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif
    glfwWindowHint(GLFW_RESIZABLE, false);

    GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "Breakout", nullptr, nullptr);
    glfwMakeContextCurrent(window);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    glfwSetKeyCallback(window, key_callback);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // OpenGL configuration
    // --------------------
    glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // initialize game
    // ---------------
    Breakout.Init();

    // deltaTime variables
    // -------------------
    float deltaTime = 0.0f;
    float lastFrame = 0.0f;

    // start game within menu state
    // ----------------------------
    Breakout.State = GAME_MENU;

    while (!glfwWindowShouldClose(window))
    {
        // calculate delta time
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;
        glfwPollEvents();

        // manage user input
        // -----------------
        Breakout.ProcessInput(deltaTime);

        // update game state
        // -----------------
        Breakout.Update(deltaTime);

        // render
        // ------
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        Breakout.Render();

        glfwSwapBuffers(window);
    }

    // delete all resources as loaded using the resource manager
    // ---------------------------------------------------------
    ResourceManager::Clear();

    glfwTerminate();
    return 0;
}

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    // when a user presses the escape key, we set the WindowShouldClose property to true, closing the application
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
    if (key >= 0 && key < 1024)
    {
        if (action == GLFW_PRESS)
            Breakout.Keys[key] = true;
        else if (action == GLFW_RELEASE)
            Breakout.Keys[key] = false;
    }
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

优化

这些教程的内容和目前已完成的游戏代码的关注点都在于如何尽可能简单地阐述概念,而没有深入地优化细节。因此,很多性能相关的考虑都被忽略了。为了在游戏的帧率开始下降时可以提高性能,我们将列出一些现代的2D OpenGL游戏中常见的改进方案。

渲染精灵表单/纹理图谱(Sprite sheet / Texture atlas):

代替使用单个渲染精灵渲染单个纹理的渲染方式,我们将所有需要用到的纹理组合到单个大纹理中(如同位图字体),并用纹理坐标来选择合适的精灵与纹理。切换纹理状态是非常昂贵的操作,而使用这种方法让我们几乎可以不用在纹理间进行切换。除此之外,这样做还可以让GPU更有效率地缓存纹理,获得更快的查找速度。(译注:cache的局部性原理)

实例化渲染:

代替一次只渲染一个四边形的渲染方式,我们可以将想要渲染的所有四边形批量化,并使用实例化渲染在一次<>draw call中成批地渲染四边形。这很容易实现,因为每个精灵都由相同的顶点组成,不同之处只有一个模型矩阵(Model Matrix),我们可以很容易地将其包含在一个实例化数组中。这样可以使OpenGL每帧渲染更多的精灵。实例化渲染也可以用来渲染粒子和字符字形。

三角形带(Triangle Strips):

代替每次渲染两个三角形的渲染方式,我们可以用OpenGL的TRIANGLE_STRIP渲染图元渲染它们,只需4个顶点而非6个。这节约了三分之一需要传递给GPU的数据量。
空间划分(Space partition)算法:当检查可能发生的碰撞时,我们将小球与当前关卡中的每一个砖块进行比较,这么做有些浪费CPU资源,因为我们可以很容易得知在这一帧中,大多数砖块都不会与小球很接近。使用BSP,八叉树(Octress)或k-d(imension)树等空间划分算法,我们可以将可见的空间划分成许多较小的区域,并判断小球是否在这个区域中,从而为我们省去大量的碰撞检查。对于Breakout这样的简单游戏来说,这可能是过度的,但对于有着更复杂的碰撞检测算法的复杂游戏,这些算法可以显著地提高性能。

最小化状态间的转换:

状态间的变化(如绑定纹理或切换着色器)在OpenGL中非常昂贵,因此你需要避免大量的状态变化。一种最小化状态间变化的方法是创建自己的状态管理器来存储OpenGL状态的当前值(比如绑定了哪个纹理),并且只在需要改变时进行切换,这可以避免不必要的状态变化。另外一种方式是基于状态切换对所有需要渲染的物体进行排序。首先渲染使用着色器A的所有对象,然后渲染使用着色器B的所有对象,以此类推。当然这可以扩展到着色器、纹理绑定、帧缓冲切换等。

这些应该可以给你一些关于,我们可以用什么样的的高级技巧进一步提高2D游戏性能地提示。这也让你感受到了OpenGL的强大功能。通过亲手完成大部分的渲染,我们对整个渲染过程有了完整的掌握,从而可以实现对过程的优化

你可能感兴趣的:(计算机图形学学习总结)