(译)LearnOpenGL实际案例Breakout(十二):渲染文本

英文原文
在这篇教程的最后我们通过添加一个生命值系统来作为最后一个系统,一个胜利条件和来自文本渲染的反馈。这个教程严重依赖于之前介绍的文本渲染教程,因此如果你没准备好,非常建议你从那篇教程开始。
在Breakout中所有文本渲染脚本被封装到TextRenderer类中,它的特性基于FreeType库的渲染特性和真实的渲染代码。你能在这里找到TextRenderer类:
TextRenderer: header, code.
Text shaders: vertex, fragment.
文本渲染方法的内容几乎和文本渲染教程中完全相同。然而,渲染字形到屏幕稍微有一些不同:

void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)
{
    [...]
    for (c = text.begin(); c != text.end(); c++)
    {
        GLfloat xpos = x + ch.Bearing.x * scale;
        GLfloat ypos = y + (this->Characters['H'].Bearing.y - ch.Bearing.y) * scale;

        GLfloat w = ch.Size.x * scale;
        GLfloat h = ch.Size.y * scale;
        // Update VBO for each character
        GLfloat vertices[6][4] = {
            { xpos,     ypos + h,   0.0, 1.0 },
            { xpos + w, ypos,       1.0, 0.0 },
            { xpos,     ypos,       0.0, 0.0 },

            { xpos,     ypos + h,   0.0, 1.0 },
            { xpos + w, ypos + h,   1.0, 1.0 },
            { xpos + w, ypos,       1.0, 0.0 }
        };
        [...]
    }
}

稍微有一些不同是因为我们使用了一个和我们在文本渲染教程中不同的正交投影矩阵。在文本渲染教程中,所有的y值从底部到顶部,在Breakout游戏中所有y值得范围伴随着一个以屏幕上边为0.0的y坐标从顶部到底部。这意味着我们我们需要稍微修改垂直偏移。
由于我们现在从RenderText的y参数向下渲染,我们以字形从字形空间顶部向下挤压的距离来计算垂直偏移。回顾FreeType的字形材质图,这被红色部分标识出来:
这里写图片描述
为了计算垂直偏移我们需要获得字形空间(基本上是从原点开始的黑色垂直列的长度)的顶部。不幸的是,FreeType对我们来说没有这样的标准。我们知道的是一些字形触碰到顶边;例如‘H’,‘T’或者‘X’字符。因此我们通过从字符的y轴减去任何一个这些触顶字符的y轴来计算红色容器的长度。这样,我们基于它从顶点到顶边的距离下压字符。
GLfloat ypos = y + (this->Characters[‘H’].Bearing.y - ch.Bearing.y) * scale;
除了更新y坐标计算,我们还切换了顶点的顺序来确认所有的顶点在乘以当前正交投影矩阵后仍然面向前面。
添加TextRenderer到游戏中很简单:

TextRenderer  *Text;
void Game::Init()
{
    [...]
    Text = new TextRenderer(this->Width, this->Height);
    Text->Load("fonts/ocraext.TTF", 24);
}

文本渲染被一个叫做OCR A Extended的字体初始化,你可以在这里下载它。如果这个字体不是你喜欢的,换一个别的字体。
现在我们有一个文本渲染了,让我们完成游戏机制。

玩家生命

小球触碰底边的时候我们给玩家一个额外的机会来取代重置游戏。我们在玩家生命显示上实现它,它在玩家开始的时候有一个初始的生命数(这里是3)并且每当小球触碰底边玩家的生命总值将会减1.仅仅当玩家的生命总值为0的时候我们重置游戏。这让玩家完成关卡更容易还会增加压力。
我们将玩家生命自添加到游戏类(在构造函数中将值初始化为3):

class Game
{
    [...]
public:
    GLuint Lives;
}

然后我们用减少玩家生命总值来代替重置游戏并且仅仅在玩家生命总值减少为0的时候重置游戏:

void Game::Update(GLfloat dt)
{
    [...]
    if (Ball->Position.y >= this->Height) // Did ball reach bottom edge?
    {
        --this->Lives;
        // Did the player lose all his lives? : Game over
        if (this->Lives == 0)
        {
            this->ResetLevel();
            this->State = GAME_MENU;
        }
        this->ResetPlayer();
    }
}

一旦玩家游戏结束(生命值为0),我们重置关卡并且改变游戏状态为GAME_MENU并在稍后完成它。
不要忘记在重置游戏/关卡的手重置玩家的生命:

void Game::ResetLevel()
{
    [...]
    this->Lives = 3;
}

玩家现在有一个生命总值在运作,但是在游戏中没有位置来显示当前生命值。这就需要用到文本渲染了。

void Game::Render()
{
    if (this->State == GAME_ACTIVE)
    {
        [...]
        std::stringstream ss; ss << this->Lives;
        Text->RenderText("Lives:" + ss.str(), 5.0f, 5.0f, 1.0f);
    }
}

我们将生命总值转换为string并且将它放置到屏幕的左上角,他看起来像这样:
这里写图片描述
一旦小球触碰底边,玩家的生命总值就递减并在屏幕左上角显示出来。

关卡选择

无论何时用户的游戏状态为GAME_MENU,我们将会给予玩家选项来选择关卡。使用‘w’或‘s’键玩家需要能够在任何一个我们加载的关卡中切换。无论何时玩家选中符合他水平的关卡,他可以通过按下enter键来选中关卡并重游戏的GAME_MENU状态切换到GAME_ACTIVE状态。
允许玩家选中关卡并不难,所有我们需要做的只是在他按下‘w’或‘s’时增加或者减少game类的Level参数:

if (this->State == GAME_MENU)
{
    if (this->Keys[GLFW_KEY_ENTER])
        this->State = GAME_ACTIVE;
    if (this->Keys[GLFW_KEY_W])
        this->Level = (this->Level + 1) % 4;
    if (this->Keys[GLFW_KEY_S])
    {
        if (this->Level > 0)
            --this->Level;
        else
            this->Level = 3;
    }
}

我们使用取模运算符(%)来确保Level参数在可接受的关卡范围内(在0到3之间)。除了转换关卡我们还想要定义哪些使我们想要渲染的当我们在菜单状态。我们将会在文本框给玩家一些提示信息并且在背景中显示所选关卡。

void Game::Render()
{
    if (this->State == GAME_ACTIVE || this->State == GAME_MENU)
    {
        [...] // Game state's rendering code
    }
    if (this->State == GAME_MENU)
    {
        Text->RenderText("Press ENTER to start", 250.0f, Height / 2, 1.0f);
        Text->RenderText("Press W or S to select level", 245.0f, Height / 2 + 20.0f, 0.75f);
    }
}

无论我们在GAME_ACTIVE状态还是GAME_MENU状态我们都渲染游戏并且无论何时我们在GAME_MENU状态我们还会渲染两行文字来提示玩家选择关卡和/或接受他的选择。注意启动游戏的时候你需要将game的状态默认设置为GAME_MENU。
这里写图片描述
看起来不错,但是一旦你尝试运行代码你讲坑你注意到点你按下‘w’或‘s’键游戏将会非常迅速的滑动关卡,这让选中你想要玩的关卡变难。出现这个问题是因为游戏在每一帧记录下了按键直到我们释放按键。这在按键的时候出发了ProcessInput方法不止一次。
通常我们能够通过一个建立在GUI系统中的小花招解决这个问题。这个花招是不仅返回当前按键,还存储曾经处理过一次的按键,直到释放它。然后我们检查(在处理前)是否这个按键还没被处理,如果是,在我们存储这个按键为正在处理的按键时处理这个按键。一旦我们想要在不释放按键时再次处理同一个按键,我们不会处理这个按键。这听起来可能有些乱,但是一旦你在实践中看到它,它(可能)开始有感觉。
首先我们必须创建另一个bool数组值来指出哪个按键被处理。我们在game类中定义它:

class Game
{
    [...]
public:
    GLboolean KeysProcessed[1024];
}

然后一旦他们被处理我们设置相关按键(s)为true来保证如果它没被处理(直到释放)我们仅仅处理它:

void Game::ProcessInput(GLfloat dt)
{
    if (this->State == GAME_MENU)
    {
        if (this->Keys[GLFW_KEY_ENTER] && !this->KeysProcessed[GLFW_KEY_ENTER])
        {
            this->State = GAME_ACTIVE;
            this->KeysProcessed[GLFW_KEY_ENTER] = GL_TRUE;
        }
        if (this->Keys[GLFW_KEY_W] && !this->KeysProcessed[GLFW_KEY_W])
        {
            this->Level = (this->Level + 1) % 4;
            this->KeysProcessed[GLFW_KEY_W] = GL_TRUE;
        }
        if (this->Keys[GLFW_KEY_S] && !this->KeysProcessed[GLFW_KEY_S])
        {
            if (this->Level > 0)
                --this->Level;
            else
                this->Level = 3;
            this->KeysProcessed[GLFW_KEY_S] = GL_TRUE;
        }
    }
    [...]
}

现在一旦KeysProcessed数组中的按键值没被处理,我们处理这个按键并且设置它的值为true。下一刻我们眼神同一个按键的if条件,它将被处理因此我们将会假装我们从未处理这个按钮知道他再次被释放。
在GLFW的按键回调方法中一旦按键被释放我们需要充值它的处理值因此我们能够在下次被按下时再次处理它:

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    [...]
    if (key >= 0 && key < 1024)
    {
        if (action == GLFW_PRESS)
            Breakout.Keys[key] = GL_TRUE;
        else if (action == GLFW_RELEASE)
        {
            Breakout.Keys[key] = GL_FALSE;
            Breakout.KeysProcessed[key] = GL_FALSE;
        }
    }
}

启动游戏将会给我们一个蒸汽的关卡选择屏幕,现在无论我们按下按键多久它正好选中一关。

胜利

当前玩家能够选择关卡,玩游戏以及失败。当玩家摧毁了所有砖块后他无法获胜是很不幸的。因此让我们来解决它。
玩家会在所有非固体砖块被销毁时获胜。我们在GameLevel类中创建一个方法来检查这个条件:

GLboolean GameLevel::IsCompleted()
{
    for (GameObject &tile : this->Bricks)
        if (!tile.IsSolid && !tile.Destroyed)
            return GL_FALSE;
    return GL_TRUE;
}

我们在游戏关卡检查所有的砖块并且如果有一个单独的非固体砖块还没被破坏我们返回false。所有我们需要做的是在game类的Update方法中检查这个条件并且一旦他返回true我们改变游戏状态为GAME_WIN:

void Game::Update(GLfloat dt)
{
    [...]
    if (this->State == GAME_ACTIVE && this->Levels[this->Level].IsCompleted())
    {
        this->ResetLevel();
        this->ResetPlayer();
        Effects->Chaos = GL_TRUE;
        this->State = GAME_WIN;
    }
}

无论何时关卡未完成,在游戏激活状态下我们重置游戏并且在GAME_WIN状态显示一个小的胜利消息。为了有趣我们在GAME_WIN屏幕启用混乱效果。在Render方法中我们将会配置玩家并且问他是重置还是离开游戏:

void Game::Render()
{
    [...]
    if (this->State == GAME_WIN)
    {
        Text->RenderText(
            "You WON!!!", 320.0, Height / 2 - 20.0, 1.0, glm::vec3(0.0, 1.0, 0.0)
        );
        Text->RenderText(
            "Press ENTER to retry or ESC to quit", 130.0, Height / 2, 1.0, glm::vec3(1.0, 1.0, 0.0)
        );
    }
}

然后我们当然需要实际捕捉提到的按键:

void Game::ProcessInput(GLfloat dt)
{
    [...]
    if (this->State == GAME_WIN)
    {
        if (this->Keys[GLFW_KEY_ENTER])
        {
            this->KeysProcessed[GLFW_KEY_ENTER] = GL_TRUE;
            Effects->Chaos = GL_FALSE;
            this->State = GAME_MENU;
        }
    }
}

转载请出名出处:http://blog.csdn.net/ylbs110/article/details/53243598
如果你能够实际管理游戏胜利,你将得到如下画面:
这里写图片描述
这就是它!我们一直在做的Breakout游戏的最后一块拼图。试试看,按你的喜好来定制它并且向你的所有家人和朋友展示他!
你能够在这里找到game类脚本的最终版本:
Game: header, code.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值