Proofread chapter 2

This commit is contained in:
Mouse
2019-09-05 20:06:11 +08:00
parent ba48a0d8f8
commit 1d35076ab6
2 changed files with 55 additions and 86 deletions

View File

@@ -1,8 +1,8 @@
# 游戏循环The Game Loop
在本章中,我们将通过创建我们的游戏循环来开始开发游戏引擎。游戏循环是每个游戏的核心部分它基本上是一个无休止的循环,负责周期地处理用户的输入、更新游戏状态和渲染图到屏幕上。
在本章中,我们将通过创建游戏循环来开始开发游戏引擎。游戏循环是每个游戏的核心部分它基本上是一个无休止的循环,负责周期地处理用户的输入、更新游戏状态和渲染图到屏幕上。
面的代码片段展示了游戏循环的结构:
代码片段展示了游戏循环的结构:
```java
while (keepOnRunning) {
@@ -12,9 +12,9 @@ while (keepOnRunning) {
}
```
那么,就这样了吗?我们已经完成游戏循环了吗?显然还没有。上面的代码有很多缺陷。首先,游戏循环运行的速度将取决于运行它的计算机。如果计算机足够快,用户甚至看不到游戏中发生了什么。此外,这个游戏循环将消耗所有的计算机资源。
那么,这就完了吗?我们已经完成游戏循环了吗?显然还没有,上述代码有很多缺陷。首先,游戏循环运行的速度将取决于运行它的计算机。如果计算机足够快,用户甚至看不到游戏中发生了什么。此外,这个游戏循环将消耗所有的计算机资源。
因此,我们需要游戏循环独立于运行的计算机,尝试以恒定速率运行。让我们假设我们希望游戏以每秒50帧50 Frames Per Second50 FPS的恒定速率运行那么我们的游戏循环代码可能是这样的:
因此我们需要游戏循环独立于运行的计算机尝试以恒定速率运行。假设我们希望游戏以每秒50帧50 Frames Per Second50 FPS的恒定速率运行那么游戏循环代码可能是这样的:
```java
double secsPerFrame = 1.0d / 50.0d;
@@ -28,17 +28,17 @@ while (keepOnRunning) {
}
```
这个游戏循环很简单可以用于一些游戏但是它也存在一些缺陷。首先它假定我们的更新和渲染方法适合以50FPS`secsPerFrame`等于20毫秒的速更新。
这个游戏循环很简单可以用于一些游戏但是它也存在一些缺陷。首先它假定我们的更新和渲染方法适合以50FPS`secsPerFrame`等于20毫秒的速更新。
此外,我们的计算机可能会优先考虑暂停游戏循环运行一段时间,运行其他的任务。因此,我们可能会在非常不稳定的时间周期更新游戏状态,这是不符合游戏物理的要求的。
此外,我们的计算机可能会优先考虑暂停游戏循环运行一段时间,运行其他的任务。因此,我们可能会在非常不稳定的时间周期更新游戏状态,这是不符合游戏物理的要求的。
最后线程休眠的时间精度仅仅只有0.1秒,所以即使我们的更新和渲染方法没有消耗时间,我们也不会以恒定的速率更新。所以,如你看到的,问题没那么简单。
最后线程休眠的时间精度仅仅只有0.1秒,所以即使我们的更新和渲染方法没有消耗时间,也不会以恒定的速率更新。所以,如你所见,问题没那么简单。
在网上你可以找到大量的游戏循环的变种。在本书中,我们将用一个不太复杂的,在大多数情况下都能正常工作的方法。我们将用的方法通常被称为固定步长游戏循环Fixed Step Game Loop
在网上你可以找到大量的游戏循环的变种。在本书中,我们将用一个不太复杂的,在大多数情况下都能正常工作的方法。我们将用的方法通常被称为**定长游戏循环**Fixed Step Game Loop
首先,我们可能想要单独控制游戏状态被更新的周期和游戏被渲染到屏幕的周期。为什么我们要这么做?因为以恒定的速率更新游戏状态更为重要,特别是如果我们使用物理引擎。相反,如果我们的渲染没有及时完成,在运行我们的游戏循环时渲染旧帧是没有意义的我们可以灵活地跳过某些帧。
首先,我们可能想要单独控制游戏状态被更新的周期和游戏被渲染到屏幕的周期。为什么要这么做?因为以恒定的速率更新游戏状态更为重要,特别是如果使用物理引擎。相反,如果渲染没有及时完成,在运行游戏循环时渲染旧帧是没有意义的我们可以灵活地跳过某些帧。
让我们看看现在我们的游戏循环是什么样的:
让我们看看现在的游戏循环是什么样的:
```java
double secsPerUpdate = 1.0d / 30.0d;
@@ -47,7 +47,7 @@ double steps = 0.0;
while (true) {
double loopStartTime = getTime();
double elapsed = loopStartTime - previous;
previous = current;
previous = loopStartTime;
steps += elapsed;
handleInput();
@@ -58,11 +58,11 @@ while (true) {
}
render();
sync(current);
sync(loopStartTime);
}
```
通过这个游戏循环,我们可以在固定的周期更新我们的游戏状态。但是我们如何避免耗尽计算机资源,使它不连续渲染呢?这在`sync`方法中实现:
使用这个游戏循环,我们可以在固定的周期更新游戏状态。但是如何避免耗尽计算机资源,使它不连续渲染呢?这在`sync`方法中实现:
```java
private void sync(double loopStartTime) {
@@ -76,28 +76,27 @@ private void sync(double loopStartTime) {
}
```
那么我们在上述方法做了什么呢?而言之,我们计算我们的游戏循环迭代应该持续多长时间(它被储存在`loopSlot`变量中),我们休眠的时间取决于我们在循环中花费的时间。但我们不做一整段时间的休眠,而是进行一些小的休眠。这允许其他任务运行,并避免我们之前提到的休眠准确性问题。然后,我们要做的是:
1. 计算我们应该退出这个方法的时间(这个变量名为`endTime`),并开始我们的游戏循环的另一迭代。
2. 比较当前时间和结束时间,如果我们没有到达结束时间就休眠1毫秒。
那么上述方法做了什么呢?而言之,我们计算游戏循环迭代应该持续多长时间(它被储存在`loopSlot`变量中),休眠的时间取决于在循环中花费的时间。但我们不做一整段时间的休眠,而是进行一些小的休眠。这允许其他任务运行,并避免前提到的休眠准确性问题。接下来我们要做的是:
1. 计算应该退出这个方法的时间(这个变量名为`endTime`),并开始游戏循环的另一迭代。
2. 比较当前时间和结束时间如果没有到达结束时间就休眠1毫秒。
现在是时候构造我们的代码以便开始编写游戏引擎的第一个版本了。但在此之前,我们来讨论一下控制渲染速率的另一种方法。在上面的代码中,我们做微休眠,以控制我们需要等待的时间。但我们可以选择另一种方法来限制帧率。我们可以使用**垂直同步**Vertical Synchronization垂直同步的主要目的是避免画面撕裂。什么是画面撕裂?这是一种显示现象,当我们更新图像储存区时,它正被渲染。结果是一部分图像显示先前的图像,而另一部分图像显示正在渲染的图像。如果我们启用垂直同步当GPU中的内容正被渲染到屏幕上时我们不会向GPU发送数据。
> Now it is time to structure our code base in order to start writing our first version of our Game Engine. But before doing that we will talk about another way of controlling the rendering rate. In the code presented above, we are doing micro-sleeps in order to control how much time we need to wait. But we can choose another approach in order to limit the frame rate. We can use v-sync \(vertical synchronization\). The main purpose of v-sync is to avoid screen tearing. What is screen tearing? Its a visual effect that is produced when we update the video memory while its being rendered. The result will be that part of the image will represent the previous image and the other part will represent the updated one. If we enable v-sync we wont send an image to the GPU while it is being rendered onto the screen.
现在是构建代码以便开始编写游戏引擎的第一个版本的时候了。但在此之前,我们来讨论一下控制渲染速率的另一种方法。在上代码中,我们做微休眠是为了控制需要等待的时间。但我们可以选择另一种方法来限制帧率。我们可以使用**垂直同步**Vertical Synchronization垂直同步的主要目的是避免画面撕裂。什么是画面撕裂?这是一种显示现象,当正在渲染时,我们更新图像储存区,导致屏幕的一部分显示先前的图像,而屏幕的另一部分显示正在渲染的图像。如果启用垂直同步当GPU中的数据正被渲染到屏幕上时我们不会向GPU发送数据。
我们开启垂直同步时,我们将与显卡的刷新率同步,显卡将以恒定的帧率渲染。用下一行代码启用它:
当开启垂直同步时,我们将与显卡的刷新率同步,显卡将以恒定的帧率渲染。用下一行代码启用它:
```java
glfwSwapInterval(1);
```
有了上面的代码,就意味着我们,至少在一个屏幕更新被绘制到屏幕之前,必须等待。事实上我们不是直接绘制到屏幕上。相反,我们将数据储存在缓冲区中,然后用下面的方法交换它:
有了上代码,就意味着至少在一个屏幕更新被绘制到屏幕之前,我们必须等待。事实上我们不是直接绘制到屏幕上。相反,我们将数据储存在缓冲区中,然后用下面的方法交换它:
```java
glfwSwapBuffers(windowHandle);
```
因此,如果我们启用垂直同步,我们就可以实现稳定的帧率,而不需要进行微休眠来检查更新时间。此外,帧率将与我们的显卡刷新率相匹配也就是说如果它设定为60Hz60FPS那么我们就有60FPS。我们可以通过在`glfwSwapInterval`方法中设置高于1的数字来降低这个速率如果我们设置为2我们将得到30FPS
因此,如果启用垂直同步,我们就可以实现稳定的帧率,而不需要进行微休眠来检查更新时间。此外,帧率将与设备的显卡刷新率相匹配也就是说如果它设定为60Hz60FPS那么我们就有60FPS。我们可以通过在`glfwSwapInterval`方法中设置高于1的数字来降低这个速率如果设置为2将得到30FPS
让我们整理一下源代码。首先我们将把所有的GLFW窗口初始化代码封装在一个名为`Window`的类中,提供一些基本的参数(如标题和大小)。`Window`类还提供一个方法以便在游戏循环中检测按下的按键:
让我们整理一下源代码。首先我们将把所有的GLFW窗口初始化代码封装在一个名为`Window`的类中,传递一些基本的参数(如标题和大小)。`Window`类还提供一个方法以便在游戏循环中检测按下的按键:
```java
public boolean isKeyPressed(int keyCode) {
@@ -105,10 +104,10 @@ public boolean isKeyPressed(int keyCode) {
}
```
除了有初始化代码以外,`Window`类还需要知道调整大小。因此需要设置一个回调方法,在窗口大小被调整时调用它。回调方法将接收帧缓冲区的以像素为单位的宽度和高度(绘制区域,简单来说就是显示区域)。如果希望得到帧缓冲区的宽度高度,你可以使用`glfwSetWindowSizeCallback`方法。屏幕坐标不一定对应像素例如具有视网膜显示屏Retina Display的Mac设备。因为我们将在进行OpenGL调用时使用这些信息所以我们要注意像素不在屏幕坐标中。您可以通过GLFW的文档了解更多信息。
除了有初始化代码以外,`Window`类还需要知道窗口大小被调整。因此需要设置一个回调方法,在窗口大小被调整时调用它。回调方法将接收帧缓冲区(渲染区域,简单来说就是显示区域)的以像素为单位的宽度和高度。如果希望得到帧缓冲区的宽度高度,你可以使用`glfwSetWindowSizeCallback`方法。屏幕坐标不一定对应像素例如具有视网膜显示屏Retina Display的Mac设备。因为我们将在进行OpenGL调用时使用这些信息所以要注意像素不在屏幕坐标中,你可以通过GLFW的文档了解更多信息。
```java
// Setup resize callback
// 设置调整大小回调
glfwSetFramebufferSizeCallback(windowHandle, (window, width, height) -> {
Window.this.width = width;
Window.this.height = height;
@@ -116,7 +115,7 @@ glfwSetFramebufferSizeCallback(windowHandle, (window, width, height) -> {
});
```
我们还将创建一个`Renderer`类,它将处理我们游戏的渲染。现在,它仅会有一个空的`init`方法,和另一个用预设颜色清空屏幕的方法:
我们还将创建一个`Renderer`类,它将处理我们游戏的渲染。现在,它仅会有一个空的`init`方法,和另一个用预设颜色清空屏幕的方法:
```java
public void init() throws Exception {
@@ -142,30 +141,23 @@ public interface IGameLogic {
}
```
然后我们将创建一个名为`GameEngine`的类,它将包含我们游戏循环的代码。这个类实现了`Runnable`接口,因为游戏循环将要在单独的线程中运行。
然后我们将创建一个名为`GameEngine`的类,它将包含我们游戏循环的代码,该类将实现储存游戏循环:
```java
public class GameEngine implements Runnable {
//..[Removed code]..
//...
private final Thread gameLoopThread;
public GameEngine(String windowTitle, int width, int height, boolean vsSync, IGameLogic gameLogic) throws Exception {
gameLoopThread = new Thread(this, "GAME_LOOP_THREAD");
window = new Window(windowTitle, width, height, vsSync);
public GameEngine(String windowTitle, int width, int height, boolean vSync, IGameLogic gameLogic) throws Exception {
window = new Window(windowTitle, width, height, vSync);
this.gameLogic = gameLogic;
//..[Removed code]..
//...
}
```
`vSync`参数允许我们选择是否使用垂直同步。你可以看到我们创建了一个新线程,它将执行我们的`GameEngine`类的`run`方法,该类包含着我们的游戏循环:
`vSync`参数允许我们选择是否用垂直同步。你可以看到我们实现了`GameEngine`类的`run`方法,其中包括游戏循环:
```java
public void start() {
gameLoopThread.start();
}
@Override
public void run() {
try {
@@ -177,11 +169,11 @@ public void run() {
}
```
我们的`GameEngine`类提供了一个`start`方法,它仅会启动我们的线程,因此`run`方法将异步执行。`run`方法将执行初始化,并运行游戏循环,直到我们关闭窗口。在线程中初始化GLFW是非常重要的在之后我们才更新它。因此`init`方法中,我们的窗口和`Renderer`实例被初始化
`GameEngine`类提供了一个`run`方法,该方法将执行初始化任务,并运行游戏循环,直到我们关闭窗口。关于线程需要注意的一点是GLFW需要从主线程初始化事件的轮询也应该在该线程中完成。因此我们将在主线程中执行所有内容而不是为游戏循环创建单独的线程
在源代码中,你将看到我们创建了其他辅助类,例如`Timer`(它将提供用于计算已经过的时间的实用方法),并在我们的游戏循环逻辑中使用它们。
在源代码中,你将看到我们创建了其他辅助类,例如`Timer`(它将提供用于计算已经过的时间的实用方法),并在游戏循环逻辑中使用它们。
我们的`GameEngine`类只是将`input``update`方法委托给`IGameLogic`实例。在`render`方法中,它也委托给`IGameLogic`实例并更新窗口。
`GameEngine`类只是将`input``update`方法委托给`IGameLogic`实例。在`render`方法中,它也委托给`IGameLogic`实例并更新窗口。
```java
protected void input() {
@@ -198,7 +190,7 @@ protected void render() {
}
```
在程序的入口,`main`方法的类只会创建一个`GameEngine`实例并启动它。
在程序的入口,含`main`方法的类只会创建一个`GameEngine`实例并运行它。
```java
public class Main {
@@ -209,7 +201,7 @@ public class Main {
IGameLogic gameLogic = new DummyGame();
GameEngine gameEng = new GameEngine("GAME",
600, 480, vSync, gameLogic);
gameEng.start();
gameEng.run();
} catch (Exception excp) {
excp.printStackTrace();
System.exit(-1);
@@ -219,7 +211,7 @@ public class Main {
}
```
最后,我们只需要创建游戏逻辑类,本章中我们实现一个简单的游戏逻辑。它只会在按下上或下键时,增加或降低窗口的颜色缓冲区的清空颜色`render`方法将会用这个颜色清空窗口的颜色缓冲区。
最后,在本章中我们只需要创建一个简单的游戏逻辑。它只会在按下上或下键时,变亮或变暗窗口的颜色缓冲区的清空颜色`render`方法将会用这个颜色清空窗口的颜色缓冲区。
```java
public class DummyGame implements IGameLogic {
@@ -241,9 +233,9 @@ public class DummyGame implements IGameLogic {
@Override
public void input(Window window) {
if ( window.isKeyPressed(GLFW_KEY_UP) ) {
if (window.isKeyPressed(GLFW_KEY_UP)) {
direction = 1;
} else if ( window.isKeyPressed(GLFW_KEY_DOWN) ) {
} else if (window.isKeyPressed(GLFW_KEY_DOWN)) {
direction = -1;
} else {
direction = 0;
@@ -262,7 +254,7 @@ public class DummyGame implements IGameLogic {
@Override
public void render(Window window) {
if ( window.isResized() ) {
if (window.isResized()) {
glViewport(0, 0, window.getWidth(), window.getHeight());
window.setResized(false);
}
@@ -274,45 +266,15 @@ public class DummyGame implements IGameLogic {
`render`方法中,当窗口大小被调整时,我们接收通知,以便更新视口将坐标中心定位到窗口的中心。
我们创建的类层次结构将帮助我们将游戏引擎代码与具体的游戏代码分开。虽然现在可能看起来没有必要,但我们将每个游戏的通用代码从具体的游戏的逻辑、美术作品和资源中分离出来,以便重用我们的游戏引擎。在后的章节中,我们需要重构这个类层次结构,因为我们的游戏引擎变得更加复杂。
创建的类层次结构将帮助我们将游戏引擎代码与具体的游戏代码分开。虽然现在可能看起来没有必要,但我们将每个游戏的通用代码从具体的游戏的逻辑、美术作品和资源中分离出来,以便重用游戏引擎。在后的章节中,我们需要重构这个类层次结构,因为我们的游戏引擎变得更加复杂。
## 线程问题
## 平台差异OSX
如果你试图在OSX系统运行前面提供的源代码你将得到这样的错误
你可以运行上面的代码在Windows或Linux上但我们仍需要为OS X平台做一些修改。正如GLFW文档中所描述的
```
Exception in thread "GAME_LOOP_THREAD" java.lang.ExceptionInInitializerError
```
> 目前OS X仅支持的OpenGL 3.x和4.x版本的环境是向上兼容的。OS X 10.7 Lion支持OpenGL 3.2版本和OS X 10.9 Mavericks支持OpenGL 3.3和4.1版本。在任何情况下你的GPU需要支持指定版本的OpenGL以成功创建环境。
这是什么意思这是因为GLFW库的某些功能不能在`Thread`中调用,因为这不是主`Thread`。我们在初始化时,包括在`GameEngine`类的`init`方法中的窗口创建。这些方法都是由同一类的`run`方法调用,而`run`方法由一个新的`Thread`调用,而不是用来启动程序的主线程调用。
这是GLFW库的一个限制它基本上意味着我们应该避免为游戏循环创建新线程。我们可以尝试在主线程中创建所有与窗口相关的东西但是我们将无法渲染任何东西。问题是OpenGL的调用需要在创建其上下文Context的同一个`Thread`中运行。
在Windows和Linux平台上即使我们不能使用主线程初始化GLFW示例代码也能运行。问题就是在OS X平台上所以我们需要更改我们的`GameEngine`类的`run`方法的代码使它支持这个平台,就像这样:
```java
public void start() {
String osName = System.getProperty("os.name");
if ( osName.contains("Mac") ) {
gameLoopThread.run();
} else {
gameLoopThread.start();
}
}
```
我们正做的是当我们在OS X平台上时忽略游戏循环线程并直接在主线程运行游戏循环代码。这不是一个完美的解决方案但它将允许你在Mac上运行示例。在论坛上找到的其他解决方案例如启动JVM时添加`-XstartOnFirstThread`参数)似乎不起作用。
在将来如果LWJGL提供其他的GUI库来检查此限制是否适用于它们这可能是值得探讨的。非常感谢Timo Bühlmann指出这个问题。
> In the future it may be interesting to explore if LWJGL provides other GUI libraries to check if this restriction applies to them. (Many thanks to Timo Bühlmann for pointing out this issue).
## 平台差异OS X
你可以运行上面的代码在Windows或Linux上但我们仍需要为OSX平台做一些修改。正如GLFW文档中所描述的
> 目前OS X仅支持的OpenGL 3.x和4.x版本的上下文是向上兼容的。OS X 10.7 Lion支持OpenGL 3.2版本和OS X 10.9 Mavericks支持OpenGL 3.3和4.1版本。在任何情况下你的GPU需要支持指定版本的OpenGL以便上下文创建成功。
因此,为了支持在之后的章节中介绍的特性,我们需要将这些代码添加到`Window`类创建窗口代码之前:
因此,为了支持在此后章节中介绍的特性,我们需要将下述代码添加到`Window`类创建窗口代码之前:
```java
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
@@ -321,4 +283,4 @@ public void start() {
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
```
这将使程序使用OpenGL 3.2到4.1之间的最高版本。如果没有这些代码就会使用旧版本的OpenGL。
这将使程序使用OpenGL 3.2到4.1之间的最高版本。如果没有上述代码就会使用旧版本的OpenGL。

View File

@@ -1,7 +1,14 @@
# 术语表
> 本术语表按术语首次出现章节排序。
> 术语表按术语首次出现章节排序。
**LWJGLLightweight Java Game Library** Java轻量级游戏库OpenGL、OpenCL、OpenAL和Vulkan对Java平台的原生绑定。
**Java轻量级游戏库Lightweight Java Game LibraryLWJGL** OpenGL、OpenCL、OpenAL和Vulkan对Java平台的原生绑定,常用于开发游戏
**GLFW** 为OpenGL、OpenGL ES和Vulkan提供的跨平台窗口与输入库。
**帧率Frames Per SecondFPS** 以帧称为单位的位图图像连续出现在显示器上的频率速率单位为Hz或FPS通俗来讲就是每秒出现在屏幕上的画面数。
**定长游戏循环Fixed Step Game Loop** 以固定时间周期更新的游戏循环。
**垂直同步Vertical Synchronization** 避免因为游戏运行速度过快导致的画面撕裂现象。