1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

translate comment and fix hyperlinks

This commit is contained in:
aillieo
2017-05-18 14:11:43 +08:00
parent 0637ea54cc
commit bc6febc4b5
5 changed files with 62 additions and 85 deletions

View File

@@ -1,25 +1,22 @@
# 渲染文字
原文 | [Particles](https://learnopengl.com/#!In-Practice/2D-Game/Render-text)
----- | ----
作者 | JoeydeVries
翻译 | [aillieo](https://github.com/aillieo)
校对 | 暂无
In this tutorial we're adding the final enhancements to the game by adding a life system, a win condition and feedback in the form of rendered text. This tutorial heavily builds upon the earlier introduced [Text Rendering](https://learnopengl.com/#!In-Practice/Text-Rendering) tutorial so it is highly advised to first work your way through that tutorial if you haven't already.
本教程中将通过增加生命值系统、获胜条件和渲染文字形式的反馈来对游戏做最后的改进。本教程很大程度上是建立在之前的教程xx基础之上因此如果没有看过的话强烈建议您先一步一步学习之前的教程。
| 原文 | [Particles](https://learnopengl.com/#!In-Practice/2D-Game/Render-text) |
| ---- | ---------------------------------------- |
| 作者 | JoeydeVries |
| 翻译 | [aillieo](https://github.com/aillieo) |
| 校对 | 暂无 |
本教程中将通过增加生命值系统、获胜条件和渲染文字形式的反馈来对游戏做最后的完善。本教程很大程度上是建立在之前的教程[文本渲染](../02 Text Rendering.md)基础之上,因此如果没有看过的话,强烈建议您先一步一步学习之前的教程。
In Breakout all text rendering code is encapsulated within a class called TextRenderer that features the initialization of the FreeType library, render configuration and the actual render code. You can find the code of the TextRenderer class below:
在Breakout中所有的文字渲染代码都封装在一个名为TextRenderer的类中TextRenderer类中有FreeType库的初始化、渲染配置和实际渲染代码等重要组成部分。以下是TextRenderer类的代码
Breakout中,所有的文字渲染代码都封装在一个名为<fun>TextRenderer</fun>的类中其中包含FreeType库的初始化、渲染配置和实际渲染代码等重要组成部分。以下是<fun>TextRenderer</fun>类的代码:
- **TextRenderer**: [header](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/text_renderer.h), [code](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/text_renderer).
- **Text shaders**: [vertex](https://learnopengl.com/code_viewer.php?code=in-practice/text_rendering&type=vertex), [fragment](https://learnopengl.com/code_viewer.php?code=in-practice/text_rendering&type=fragment).
The content of the text renderer's functions is almost exactly the same as the code from the text rendering tutorial. However, the code for rendering glyphs onto the screen is slightly different:
文字渲染器中函数的内容几乎与文字渲染教程中的代码完全一样。但用于向屏幕渲染字形glyphs的代码稍有不同:
文字渲染器中函数的内容几乎与文字渲染教程中的代码完全一样。但用于向屏幕渲染字形的代码稍有不同:
```
void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat scale,glm::vec3 color)
@@ -32,7 +29,7 @@ void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat sc
GLfloat w = ch.Size.x * scale;
GLfloat h = ch.Size.y * scale;
// Update VBO for each character
// 为每个字符更新VBO
GLfloat vertices[6][4] = {
{ xpos, ypos + h, 0.0, 1.0 },
{ xpos + w, ypos, 1.0, 0.0 },
@@ -45,21 +42,20 @@ void TextRenderer::RenderText(std::string text, GLfloat x, GLfloat y, GLfloat sc
[...]
}
}
```
The reason for it being slightly different is that we use a different orthographic projection matrix than the one we've used in the text rendering tutorial. In the text rendering tutorial, all `y` values ranged from bottom to top while in the Breakout game all `y` values range from top to bottom with a `y` coordinate of `0.0` corresponding to the top edge of the screen. This means we have to slightly modify how we calculate the vertical offset.
之所以会稍有不同,是因为相比于文字渲染教程我们此处使用了一个不同的正交投影矩阵。在文字渲染教程中,所有的y值取值从底部向顶部递增但在Breakout游戏中y值取值从顶部到底部递增值为0.0的y值对应屏幕顶端。这意味着我们需要稍微改变计算垂直方向偏移的方法。
之所以会稍有不同,是因为相比于文字渲染教程我们此处使用了一个不同的正交投影矩阵。在文字渲染教程中,所有的`y`值取值从底部向顶部递增,但在游戏Breakout中,`y`值取值从顶部到底部递增,值为`0.0``y`值对应屏幕顶端。这意味着我们需要稍微改变计算垂直方向偏移的方法。
Since we now render downwards from RenderText's y parameter we calculate the vertical offset as the distance a glyph is pushed downwards from the top of the glyph space. Looking back at the glyph metrics image from FreeType this is indicated by the red arrow:
由于现在我们将渲染文字y坐标参数从上向下渲染我们将垂直偏移计算为一个字形距离字形空间顶部的距离(下推)。回顾FreeType的字形矩阵图片此垂直偏移用红色箭头标记。
由于现在我们将渲染文字y坐标参数从上向下渲染我们将垂直偏移计算为一个字形字形空间顶部向下推进的距离。回顾FreeType的字形矩阵图片此垂直偏移用红色箭头标记。
![img](https://learnopengl.com/img/in-practice/breakout/glyph_offset.png)
![](../../img/06/Breakout/10/glyph_offset.png)
To calculate this vertical offset we need to get the top of the glyph space (basically the length of the black vertical arrow from the origin). Unfortunately, FreeType has no such metric for us. What we do know is that that some glyphs always touch this top edge; characters like 'H', 'T' or 'X'. So what if we calculate the length of this red vector by subtracting `bearingY` from any of these *top-reaching* glyphs by `bearingY` of the glyph in question. This way, we push the glyph down based on how far its top point differs from the top edge.
为计算垂直偏移我们需要获取字形空间的顶部基本上是原点发出的黑色垂直箭头的长度。不幸的是FreeType并不向我们提供这样的metric。我们已知的是有一些字形直接与顶部接触,如字符'H'、'T'或'X'。那么我们通过到达顶部top-reaching的字形的bearingY减去顶部不确定字形的bearingY来计算红色矢量的长度。使用这种方法我们依据字形顶部的点与顶部边差异的距离来下推字形。
为计算垂直偏移我们需要获取字形空间的顶部基本上是原点发出的黑色垂直箭头的长度。不幸的是FreeType并不向我们提供这样的机制。我们已知的是有一些字形直接与顶部接触,如字符'H'、'T'或'X'。那么我们通过与顶部接触的字形的`bearingY`减去顶部不确定字形的`bearingY`来计算红色矢量的长度。使用这种方法,我们依据字形顶部的点与顶部边差异的距离来下推字形。
```
@@ -67,11 +63,9 @@ GLfloat ypos = y + (this->Characters['H'].Bearing.y - ch.Bearing.y) * scale;
```
Aside from updating the `ypos` calculation we also switched the order of the vertices a bit around to make sure all the vertices are still front facing when multiplied with the current orthographic projection matrix (as discussed in the [face culling](https://learnopengl.com/#!Advanced-OpenGL/Face-culling)tutorial).
除了更新`ypos`的计算之外,我们还调换了一些顶点的顺序,用以确保所有的顶点在与现在的正交投影矩阵相乘后仍为正方向(详见教程面剔除)。
除了更新`ypos`的计算之外,我们还调换了一些顶点的顺序,用以确保所有的顶点在与现在的正交投影矩阵相乘后仍为正方向(详见教程[面剔除](../../04 Advanced OpenGL/04 Face culling.md))。
Adding the TextRenderer to the game is easy:
向游戏中加入文字渲染器并不难:
```
@@ -86,21 +80,19 @@ void Game::Init()
```
The text renderer is initialized with a font called OCR A Extended that you can download from [here](http://fontzone.net/font-details/ocr-a-extended). If the font is not to your liking feel free to use a different font.
使用一个名为OCR A Extended的字体来初始化文字渲染器改字体可以从[这里](http://fontzone.net/font-details/ocr-a-extended)下载。您可以使用任意不同的字体,如果不很喜欢这个字体。
Now that we have a text renderer, let's finish the gameplay mechanics.
使用一个名为OCR A Extended的字体来初始化文字渲染器该字体可以从[这里](http://fontzone.net/font-details/ocr-a-extended)下载。您可以使用任意不同的字体,如果不很喜欢这个字体。
现在我们已经有了一个文字渲染器,接下来我们来完成游戏机制。
## Player lives
Instead of immediately resetting the game as soon as the ball reaches the bottom edge, we'd like to give the player a few extra chances. We do this in the form of player lives, where the player begins with an initial number of lives (say `3`) and each time the ball touches the bottom edge the player's life total is decreased by 1. Only when the player's life total becomes `0` we reset the game. This makes it easier for the player to finish a level while also building tension.
当球碰到底部边界时我们会给玩家额外的几次机会而不是立即重置游戏。我们使用玩家生命值的形式来实现玩家开始时会有初始数量的生命值比如3每当球碰到底部边界玩家的生命值总数会减1。只有当玩家生命值变为`0`时才会重置游戏。这样对玩家来说完成关卡会稍容易一点,同时也会感受到难度。
## 玩家生命值
We keep count of the lives of a player by adding it to the game class (initialized within the constructor to a value of `3`):
我们向game类中增加玩家的生命值以记录它在构造函数中将其初始化为3
当球碰到底部边界时,我们会给玩家额外的几次机会,而不是立即重置游戏。我们使用玩家生命值的形式来实现,玩家开始时会有初始数量的生命值(比如`3`每当球碰到底部边界玩家的生命值总数会减1。只有当玩家生命值变为`0`时才会重置游戏。这样对玩家来说完成关卡会稍容易一点,同时也会感受到难度
我们向game类中增加玩家的生命值以记录它在构造函数中将其初始化为`3`)。
```
@@ -110,21 +102,19 @@ class Game
public:
GLuint Lives;
}
```
We then modify the game's Update function to, instead of resetting the game, decrease the player's life total and only reset the game once the life total reached `0`:
接下来我们修改game类的update函数不再重置游戏而是减少玩家生命值只有当生命值为0时重置游戏。
接下来我们修改game类的update函数不再重置游戏而是减少玩家生命值只有当生命值为`0`时重置游戏。
```
void Game::Update(GLfloat dt)
{
[...]
if (Ball->Position.y >= this->Height) // Did ball reach bottom edge?
if (Ball->Position.y >= this->Height) // 球是否接触到底部边界?
{
--this->Lives;
// Did the player lose all his lives? : Game over
// 玩家是否已失去所有生命值? : 游戏结束
if (this->Lives == 0)
{
this->ResetLevel();
@@ -133,13 +123,11 @@ void Game::Update(GLfloat dt)
this->ResetPlayer();
}
}
```
As soon as the player is game over (lives equal to `0`) we reset the level and change the game state to GAME_MENU which we'll get to later.
一旦玩家游戏结束生命值等于0我们会重置关卡并将游戏状态game state改变为GAME_MENU稍后会详细讲。
Don't forget to reset the player's life total as soon as we reset the game/level:
一旦玩家游戏结束(生命值等于`0`我们会重置关卡并将游戏状态game state改变为GAME_MENU稍后会详细讲。
注意不要忘了在重置游戏/关卡时重置玩家生命值:
```
@@ -148,10 +136,9 @@ void Game::ResetLevel()
[...]
this->Lives = 3;
}
```
The player now has a working life total, but has no way of seeing how many lives he currently has while playing the game. That's where the text renderer comes in.
此时玩家生命值已可以运作,但玩家在游戏时却无法看到自己当前有多少生命值。这时就需要加入文字渲染器。
@@ -168,22 +155,22 @@ void Game::Render()
```
Here we convert the number of lives to a string and display it at the top-left of the screen. It'll now look a bit like this:
这里我们将生命值数量转化为一个字符串,并将其显示在屏幕左上角。看起来将会是像这样:
![img](https://learnopengl.com/img/in-practice/breakout/render_text_lives.png)
![](../../img/06/Breakout/10/render_text_lives.png)
As soon as the ball touches the bottom edge, the player's life total is decreased which is instantly visible at the top-left of the screen.
一旦球接触到底部边界,玩家的生命值会减少,这会在屏幕左上角直接可见。
## Level selection
## 关卡选择
Whenever the user is in the game state GAME_MENU we'd like to give the player the control to select the level he'd like to play in. With either the 'w' or 's' key the player should be able to scroll through any of the levels we loaded. Whenever the player feels like the chosen level is indeed the level he'd like to play in he can press the enter key to switch from the game's GAME_MENU state to the GAME_ACTIVE state.
当玩家所处游戏状态为GAME_MENU时我们希望玩家可以控制选择他想玩的关卡。玩家应该可以使用'w'或's'键在我们加载的所有关卡中滚动选择。当玩家感觉选中的是他想玩的关卡时他可以按回车键将游戏状态从GAME_MENU切换到GAME_ACTIVE。
Allowing the player to choose a level is not too difficult. All we have to do is increase or decrease the game class's Levelvariable based on whether he pressed 'w' or 's' respectively:
允许玩家选择关卡并不难。我们要做的就是当玩家按下'w'或's'键时分别增加或减小game类中的Levelvariable值
@@ -205,8 +192,8 @@ if (this->State == GAME_MENU)
```
We use the modulus operator (`%`) to make sure the Level variable remains within the acceptable level range (between `0` and `3`). Aside from switching levels we also want to define what we want to render when we're in the menu state. We'd like to give the player some instructions in the form of text and also display the selected level in the background.
我们使用取模运算以保证变量Level在可接受的关卡值范围内0和3之间)。除了变换关卡之外,在菜单状态时我们还需要将其渲染出来。我们要给玩家一些文字形式的指示,并在背景中展示出选中的关卡。
我们使用取模运算(`%`)以保证变量Level在可接受的关卡值范围内`0``3`之间)。除了变换关卡之外,在菜单状态时我们还需要将其渲染出来。我们要给玩家一些文字形式的指示,并在背景中展示出选中的关卡。
@@ -215,7 +202,7 @@ void Game::Render()
{
if (this->State == GAME_ACTIVE || this->State == GAME_MENU)
{
[...] // Game state's rendering code
[...] // 游戏状态渲染代码
}
if (this->State == GAME_MENU)
{
@@ -223,21 +210,20 @@ void Game::Render()
Text->RenderText("Press W or S to select level", 245.0f, Height / 2 + 20.0f, 0.75f);
}
}
```
Here we render the game whenever we're in either the GAME_ACTIVE state or the GAME_MENU state and whenever we're in the GAME_MENU state we also render two lines of text to inform the player to select a level and/or accept his choice. Note that for this to work when launching the game you do have to set the game's state as GAME_MENU by default.
这里无论游戏处在GAME_ACTIVE状态还是GAME_MENU状态都会渲染游戏当游戏处在GAME_MENU状态需要渲染两行文字用于告知玩家选择一个关卡或确认确认选择。注意为此必须在启动游戏时将游戏状态默认设置为GAME_MENU。
![img](https://learnopengl.com/img/in-practice/breakout/render_text_select.png)
![](../../img/06/Breakout/10/render_text_select.png)
It looks great, but once you try to run the code you'll probably notice that as soon as you press either the 'w' or the 's' key the game rapidly scrolls through the levels making it difficult to select the level you want to play in. This happens because the game records the key press for multiple frames until we release the key. This causes the ProcessInput function to process the pressed key more than once.
看起来很棒,但当你试图运行代码你很可能会注意到,当按下'w'或's'键时游戏会在关卡之前快速滚动很难选中你想玩的关卡。这是因为game会在多帧记录按键直到按键松开。这将导致ProcessInput函数处理按下的键不止一次。
We can solve this issue with a little trick commonly found within GUI systems. The trick is to not only record the keys currently pressed, but also storing the keys that have been processed once, until released again. We then check (before processing) whether the key has not yet been processed, and if so, process this key after which we store this key as being processed. Once we want to process the same key again without the key having been released, we do not process the key. This probably sounds slightly confusing, but as soon as you see it in practice it (probably) starts to make sense.
我们可以使用GUI系统中常见的一个小技巧来解决这一问题。这一小技巧就是不仅记录当前按下的键并且存储已经被按下的键直到再次松开。然后我们会检查在处理之前是否还没有被处理如果没有被处理的话处理该按键并将其存储为正在被处理。一旦我们要在未松开时再次处理相同的按键我们将不会处理该按键。这听起来让人稍微迷惑但当你在实际应用中见到它很可能就会明白它的意义。
First we have to create another array of bool values to indicate which keys have been processed. We define this within the game class:
首先我们需要创建另一个布尔数组用来表示处理过的按键。我们在game类定义如下
@@ -251,8 +237,8 @@ class Game
```
We then set the relevant key(s) to `true` as soon as they're processed and make sure to only process the key if it wasn't processed before (until released):
当相对应的按键被处理时我们将其设置为true以确保按键只在之前没有被处理过直到松开时将其处理。
当相对应的按键被处理时,我们将其设置为`true` ,以确保按键只在之前没有被处理过(直到松开)时将其处理。
```
void Game::ProcessInput(GLfloat dt)
@@ -283,10 +269,10 @@ void Game::ProcessInput(GLfloat dt)
```
Now as soon as the key's value in the KeysProcessed array has not yet been set, we process the key and set its value to `true`. Next time we reach the `if` condition of the same key, it will have been processed so we'll pretend we never pressed the button until it's released again.
现在当KeysProcessed数组中按键的值未被设置时我们会处理按键并将其值设为true。下一次当到达同一按键的if条件时它已经被处理过了所以我们会假装并没有按下此键直到它被松开。
Within GLFW's key callback function we then need to reset the key's processed value as soon as its released so we can process it again the next time it's pressed:
现在当KeysProcessed数组中按键的值未被设置时我们会处理按键并将其值设为`true`。下一次当到达同一按键的`if`条件时,它已经被处理过了所以我们会假装并没有按下此键,直到它被松开。
之后我们需要在松开按键时通过GLFW的按键回调函数重置按键处理后的值以便于下次再处理
```
@@ -307,18 +293,18 @@ void key_callback(GLFWwindow* window, int key, int scancode, int action, int mod
```
Launching the game gives us a neat level select screen that now precisely selects a single level per key press no matter how long we press he key.
启动游戏会展示一个整洁的关卡选择界面,现在可以每次按键清晰地选择一个关卡,无论按键被按下多久。
## Winning
## 获胜
Currently the player is able to select levels, play the game and fail in doing so to lose. It is kind of unfortunate if the player finds out after destroying all the bricks he cannot in any way win the game. So let's fix that.
现在玩家可以选择关卡、玩游戏和游戏失败。有些不幸的是玩家在消除了所有的砖块之后会发现无法获得游戏胜利。现在来修复此问题。
The player wins when all of the non-solid blocks have been destroyed. We already created a function to check for this condition with the GameLevel class:
在所有实体砖块被消除之后玩家会取得胜利。我们已经在GameLevel类创建了一个函数用于检查这一条件
@@ -333,8 +319,8 @@ GLboolean GameLevel::IsCompleted()
```
We check all bricks in the game level and if a single non-solid brick is not destroyed we return `false`. All we have to do is check for this condition in the game's Update function and as soon as it returns `true` we change the game state to GAME_WIN:
我们会检查游戏关卡所有的砖块如果有一个非实体砖块未被消除则返回false。我们所要做的就是在Update函数中检查此条件一旦返回true我们就将游戏状态改变为GAME_WIN
我们会检查游戏关卡所有的砖块,如果有一个非实体砖块未被消除则返回`false`。我们所要做的就是在Update函数中检查此条件一旦返回`true`我们就将游戏状态改变为GAME_WIN
```
@@ -352,7 +338,7 @@ void Game::Update(GLfloat dt)
```
Whenever the level is completed while the game is active we reset the game and display a small victory message in the GAME_WIN state. For fun we'll enable the chaos effect while in the GAME_WIN screen. In the Render function we'll congratulate the player and ask him to either restart or quit the game:
在游戏激活状态当关卡完成时我们会重置游戏并且在GAME_WIN状态展示一条小的胜利的消息。为了增加趣味性我们会在GAME_WIN界面启用混沌效果chaos effect。在渲染函数中我们会祝贺玩家并询问其重新开始还是退出游戏。
@@ -375,7 +361,7 @@ void Game::Render()
```
Then we of course have to actually catch the mentioned keys:
之后我们当然也要处理之前提到的按键:
```
@@ -395,26 +381,17 @@ void Game::ProcessInput(GLfloat dt)
```
If you can then actually manage to win the game you'd get the following image:
之后如果你真的可以赢得游戏,你将会看到以下图片:
![img](https://learnopengl.com/img/in-practice/breakout/render_text_win.png)
![](../../img/06/Breakout/10/render_text_win.png)
And that is it! The final piece of the puzzle of the Breakout game we've been working on. Try it out, customize it to your liking and show it to all your family and friends!
就是这样这是游戏Breakout我们要处理的最后一块拼图。尝试按照自己的意愿自定义并把它展示给你的家人和朋友
You can find the final version of the game's code below:
以下是最终版的游戏代码:
- **Game**: [header](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game.h), [code](https://learnopengl.com/code_viewer.php?code=in-practice/breakout/game).
## Want to help?
Enjoyed the tutorial? Consider becoming a [patron](https://www.patreon.com/learnopengl) of LearnOpenGL.
喜欢这个教程吗?来[赞助](https://www.patreon.com/learnopengl)LearnOpenGL吧。
Your support helps me create new and better tutorials, improve existing content and enables me to improve the website wherever I can.
您的支持会帮助我创作新的更好的教程,改进现有内容,帮助我改进网站的任何地方。

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB