From 18dc11fb82089c7b81699220df38d73324427638 Mon Sep 17 00:00:00 2001 From: Meow J Date: Mon, 29 Aug 2016 01:43:33 +0800 Subject: [PATCH] 06-03-02 --- docs/06 In Practice/2D-Game/02 Setting up.md | 83 +++++++++--------- .../2D-Game/03 Rendering Sprites.md | 2 +- docs/img/06/Breakout/02/setting-up.png | Bin 0 -> 22671 bytes glossary.md | 9 +- 4 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 docs/img/06/Breakout/02/setting-up.png diff --git a/docs/06 In Practice/2D-Game/02 Setting up.md b/docs/06 In Practice/2D-Game/02 Setting up.md index ae46b6f..ef8afec 100644 --- a/docs/06 In Practice/2D-Game/02 Setting up.md +++ b/docs/06 In Practice/2D-Game/02 Setting up.md @@ -6,101 +6,100 @@ 翻译 | [ZMANT](https://github.com/Itanq) 校对 | 暂无 -## 设置 +在开始真正写游戏机制之前,我们首先需要配置一个简单的框架,用来存放这个游戏,这个游戏将会用到几个第三方库,它们的大多数都已经在前面的教程中介绍过了。在需要用到新的库的时候,我会作出适当的介绍。 -在我们开始实际构造这个游戏之前,我们首先需要设置一些简单的框架来处理这个游戏,这个游戏将会用到几个第三方库,它们大多数都已经在前面的教程中介绍过了。不管在那个地方需要用到新的库的时候,我们都会作出适当的介绍。 +首先,我们定义一个所谓的超级(Uber)游戏类,它会包含所有相关的渲染和游戏代码。这个游戏类的主要作用是(简单)管理你的游戏代码,并与此同时将所有的窗口代码从游戏中解耦。这样子的话,你就可以把相同的类迁移到完全不同的窗口库(比如SDL或SFML)而不需要做太多的工作。 -首先,我们定义一个叫做`Game`的类来包含所有有关的渲染和游戏设置代码。有了这个类,我们就可以用这个类把游戏代码(稍稍的)有组织的嵌入到游戏窗口代码中。用这种方式,你就可以把相同的代码迁移到完全不同的窗口库(比如 SDL或SFML)而不需要做太多的工作。 +!!! important -> 存在有成千上万的方式来抽象并概括游戏/图形代码然后封装到类和对象中。在这个教程中,你刚好会看到有一个方法来解决这个问题。如果你感到有一个更好的方法,尽量实现你的改进。 + 抽象并归纳游戏或图形代码至类与对象中有成千上万种方式。在这个系列教程中你所看到的仅是其中的一种。如果你觉得能有更好的方式进行实现,你可以尝试改进我的这个实现。 -这个`Game`类封装了一个初始化函数,一个更新函数,一个处理输入函数以及一个渲染函数: +这个游戏类封装了一个初始化函数、一个更新函数、一个处理输入函数以及一个渲染函数: -```C++ +```c++ class Game { public: - // Game state + // 游戏状态 GameState State; GLboolean Keys[1024]; GLuint Width, Height; - // Constructor/Destructor + // 构造函数/析构函数 Game(GLuint width, GLuint height); ~Game(); - // Initialize game state (load all shaders/textures/levels) + // 初始化游戏状态(加载所有的着色器/纹理/关卡) void Init(); - // GameLoop + // 游戏循环 void ProcessInput(GLfloat dt); void Update(GLfloat dt); void Render(); }; ``` -这个类可能就是你期望中的游戏类。我们通过一个`width`和`height`(对应于你玩游戏时的设备分辨率)来初始化一个游戏实例并且使用`Init`函数来加载着色器、纹理以及初始化游戏状态。我们可以通过调用`ProcessInput`函数来使用存储在`Keys`数组里的数据来处理输入并且在`Update`函数里面更新游戏设置状态(比如玩家/球的移动)。最后,我们还可以在`Render`函数里面渲染游戏。注意,我们在渲染逻辑里面分离出了运动逻辑。 +这个类应该包含了所有在一个游戏类中会出现的东西。我们通过给定一个宽度和高度(对应于你玩游戏时的分辨率)来初始化这个游戏,并且使用Init函数来加载着色器、纹理并且初始化所有的游戏状态。我们可以通过调用ProcessInput函数,并使用存储在Keys数组里的数据来处理输入。并且在Update函数里面我们可以更新游戏设置状态(比如玩家/球的移动)。最后,我们还可以调用Render函数来对游戏进行渲染。注意,我们将运动逻辑与渲染逻辑分开了。 -这个`Game`类同样了封装了一个叫做`State`的变量,它的类型`GameState`如下定义: +这个Game类同样了封装了一个叫做State的变量,它的类型是GameState,定义如下: -```C++ -// Represents the current state of the game +```c++ +// 代表了游戏的当前状态 enum GameState { GAME_ACTIVE, GAME_MENU, GAME_WIN -}; +}; ``` -这可以使我们跟踪游戏当前的状态。这样我们就可以根据当前游戏的状态来决定渲染或者处理不同的元素(比如当我们在游戏菜单界面的时候就可能需要渲染和处理不同的元素)。 +这个类可以帮助我们跟踪游戏的当前状态。这样的话我们就可以根据当前游戏的状态来决定渲染和/或者处理不同的元素(Item)了(比如当我们在游戏菜单界面的时候就可能需要渲染和处理不同的元素了)。 -目前为止,这个`Game`类的函数还完全是空的,因为我们还没有写实际的实现代码,但这里就是`Game`类的[header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up.h)和[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up)文件。 +目前为止,这个游戏类的函数还完全是空的,因为我们还没有写游戏的实际代码,但这里是`Game`类的[头文件](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up.h)和[代码文件](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/game_setting-up)。 -## 通用 +## 工具类 -因为我们正在开发一个大型应用,所以我们将不得不频繁的重用一些OpenGL的概念,比如纹理和着色器等。因此,为这两个元素(items)创建一个更加易用的接口就是情理之中的事了,就像在我们前面一个教程中创建的那个`shader`类一样。 +因为我们正在开发一个大型应用,所以我们将不得不频繁地重用一些OpenGL的概念,比如纹理和着色器等。因此,为这两个元素创建一个更加易用的接口也是情理之中的事了,就像在我们前面教程中创建的那个着色器类一样。 -定义一个着色器类,它接收两个或三个字符串(如果有几何着色器)然后生成一个编译好的着色器(或者如果失败的话就生成一条错误信息)。这个着色器类同样也包含大量实用的函数来快速的设置`uniform`变量。同样也定义一个纹理类,它从给定的字节数组中生成一个2D纹理图像(基于它的内容)。并且这个纹理类同样也封装了许多实用的函数。 +着色器类会接受两个或三个(如果有几何着色器)字符串,并生成一个编译好的着色器(如果失败的话则生成错误信息)。这个着色器类也包含很多工具(Utility)函数来帮助快速设置uniform值。纹理类会接受一个字节(Byte)数组以及宽度和高度,并(根据设定的属性)生成一个2D纹理图像。同样,这个纹理类也会封装一些工具函数。 -我们并不打算钻研这些类实现的细节,因为学到这里你应该可以很容易的理解它们是如何工作的了。出于这个原因你可以找到它们的头文件和实现的代码文件,有详细的注释,如下: +我们并不会深入讨论这些类的实现细节,因为学到这里你应该可以很容易地理解它们是如何工作的了。出于这个原因,你可以在下面找到它们的头文件和代码文件,都有详细的注释: ->* Shader : [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader) ->* Texture: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture.h),[code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture) +- **着色器**:[头文件](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader.h),[代码](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/shader) +- **纹理**:[头文件](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture.h),[代码](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/texture) -注意到当前的这个纹理类仅仅设置成单一的`2D`纹理,但你可以很容易的通过可选的纹理类型来扩展。 +注意当前的纹理类仅是为2D纹理设计的,但你很容易就可以将其扩张至更多的纹理类型。 -## 资源管理器 +## 资源管理 -当着色器和纹理类的函数被他们自己使用的时候,他们确实需要一个字节数组或是几个字符串来初始化他们。我们可以很容易的在他们自己的类里面嵌入文件的加载代码,但这就稍微有点违反`单一职责原则`,(也就是说)在这个类里我们应该仅仅关注纹理或着色器本身而不需要关注他们的文件加载结构。 +尽管着色器与纹理类的函数本身就很棒了,它们仍需要有一个字节数组或一些字符串来调用它们。我们可以很容易将文件加载代码嵌入到它们自己的类中,但这稍微有点违反了单一功能原则(Single Responsibility Principle),即这两个类应当分别仅仅关注纹理或者着色器本身,而不是它们的文件加载机制。 -出于这个原因,通常用一个更加有组织的方法(来实现文件的加载),就是创建一个叫做`resource manager`的单独实体类来加载游戏相关的资源。这里有好几个方法来实现`resouce manager`;在这个教程里我们选择使用一个单独的静态`resouce manager`(就是给它静态属性),也就是说可以在整个工程中使用它来加载资源以及使用和她相关加载功能。 +出于这个原因,我们通常会用一个更加有组织的方法(译注:来实现文件的加载),就是创建一个所谓资源管理器的实体,专门加载游戏相关的资源。创建一个资源管理器有多种方法。在这个教程中我们选择使用一个单一实例(Singleton)的静态资源管理器,(由于它静态的本质)它在整个工程中都可以使用,它会封装所有的已加载资源以及一些相关的加载功能。 -使用具有静态属性的单独的类有很多好处,但它主要的坏处就是会失去`OOP`特性以及控制结构/破坏。不过,这些对于小项目来说是很容易处理的。 +使用一个具有静态属性的单一实例类有很多优点也有很多缺点。它主要的缺点就是这样会损失OOP属性,并且丧失构造与析构的控制。不过,对于我们这种小项目来说是这些问题也是很容易处理的。 -就像其他的类文件一样,这个资源管理器的列表如下: +和其它类的文件一样,这个资源管理器的代码如下: ->* Resource Manager: [header](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager.h), [code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager) +- **资源管理器**:[头文件](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager.h),[代码](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/resource_manager) -使用资源管理我们可以很容易的把着色器加载到程序里面,比如: +通过使用资源管理器,我们可以很容易地把着色器加载到程序里面: -```C++ +```c++ Shader shader = ResourceManager::LoadShader("vertex.vs", "fragment.vs", nullptr, "test"); -// then use it +// 接下来使用它 shader.Use(); -// or +// 或者 ResourceManager::GetShader("test").Use(); ``` -通过定义的`Game`类和`resouce manager`类一起就可以很很容易管理`Shader`和`Texture2D`,基于这个基础,在以后的教程里我们将会广泛的使用这些类来实现这个`Breakout`游戏。 +Game类、资源管理器类,以及很容易管理的ShaderTexture2D类一起组成了之后教程的基础,我们之后会广泛使用这些类来实现我们的Breakout游戏。 ## 程序 -对这个游戏,我们仍然需要创建一个窗口并且设置OpenGL的初识状态。我们确保使用OpenGL的[面剔除](http://learnopengl.com/#!Advanced-OpenGL/Face-culling)功能和它的[混合](http://learnopengl.com/#!Advanced-OpenGL/Blending)功能。我们不需要使用深度测试;因为这个游戏完全是一个二维的,所有顶点的`z`坐标都具有相同的值。因此开启深度测试并没有什么用还有可能造成`z-fighting`现象。 +我们仍然需要为这个游戏创建一个窗口并且设置一些OpenGL的初始状态。我们确保使用OpenGL的[面剔除](../../04 Advanced OpenGL/04 Face culling.md)功能和[混合](../../04 Advanced OpenGL/03 Blending.md)功能。我们不需要使用深度测试,因为这个游戏完全是2D的,所有顶点都有相同的z值,所以开启深度测试并没有什么用,反而可能造成深度冲突(Z-fighting)。 -这个`Breakout`游戏开始时候的代码相当的简单:我们用`GLFW`创建一个窗口,注册了一些回调函数,创建一个`Game`实例并且调用了`Game`类所有相关的函数。这个代码如下: +这个Breakout游戏的起始代码非常简单:我们用GLFW创建一个窗口,注册一些回调函数,创建一个Game对象,并将所有相关的信息都传到游戏类中。代码如下: ->* Program: [code](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/program) +- **程序**:[代码](http://learnopengl.com/code_viewer.php?code=in-practice/breakout/program) -运行这个代码,你可能得到下面的输出: +运行这个代码,你应该能得到下面的输出: -![ans](http://learnopengl.com/img/in-practice/breakout/setting-up.png) - -目前为止我们已经有了一个后面的教程需要的固定框架;我们将会持续的扩展这个`Game`类来封装一些新的功能。如果你准备好了就点击[下一个](http://learnopengl.com/#!In-Practice/2D-Game/Rendering-Sprites)教程。 +![](../../img/06/Breakout/02/setting-up.png) +现在我们已经为之后的教程构建了一个坚实的框架,我们将不断地拓展这个游戏类,封装新的功能。如果你准备好了,就可以开始[下一节](03 Rendering Sprites.md)的学习了。 \ No newline at end of file diff --git a/docs/06 In Practice/2D-Game/03 Rendering Sprites.md b/docs/06 In Practice/2D-Game/03 Rendering Sprites.md index 2221b2e..f8ca749 100644 --- a/docs/06 In Practice/2D-Game/03 Rendering Sprites.md +++ b/docs/06 In Practice/2D-Game/03 Rendering Sprites.md @@ -167,7 +167,7 @@ void SpriteRenderer::DrawSprite(Texture2D &texture, glm::vec2 position, 当试图在一个场景中用旋转矩阵和伸缩矩阵放置一个对象的时候,建议首先做伸缩变换,然后旋转最后是移动变换。因为矩阵乘法是从右向左执行的,所以我们变换的矩阵顺序是相反的:移动,旋转,缩放。 -旋转变换可能看起来稍微有点让人望而却步。我们从[变换](../../01 Getting started/07 Transformations/)教程里面知道旋转总是围绕原点`(0,0)`转动的。因为我们指定了四边形的左上角为`(0,0)`,所有的旋转都是围绕`(0,0)`的。主要是这个旋转的原点是四边形的左上角,这样就会造成不太友好的旋转效果。我们想要做的就是把旋转的点移到四边形的中心,这样旋转就是围绕四边形中心而不是左上角了。我们通过在旋转之前把旋转点移动到四边形中心解决了这个问题。 +旋转变换可能看起来稍微有点让人望而却步。我们从[变换](../../01 Getting started/07 Transformations.md)教程里面知道旋转总是围绕原点`(0,0)`转动的。因为我们指定了四边形的左上角为`(0,0)`,所有的旋转都是围绕`(0,0)`的。主要是这个旋转的原点是四边形的左上角,这样就会造成不太友好的旋转效果。我们想要做的就是把旋转的点移到四边形的中心,这样旋转就是围绕四边形中心而不是左上角了。我们通过在旋转之前把旋转点移动到四边形中心解决了这个问题。 ![](../../img/06/Breakout/03/rotation-origin.png) diff --git a/docs/img/06/Breakout/02/setting-up.png b/docs/img/06/Breakout/02/setting-up.png new file mode 100644 index 0000000000000000000000000000000000000000..12b1de82935eae1be771cd2b23422f76bcc767e1 GIT binary patch literal 22671 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