此次博客更新距离上次的时间间隔变短了好多,由于最近硬是抽出了一大部分时间来进行引擎的开发。并且运气很好的是在写链表这种很“敏感”的的数据结构的时候并无出现那种灾难性的后果(恐怕是前一段时间在leetcode刷数据结构的缘由吧)。因而本人才能在上篇博文发布后不久完成了基本渲染对象,渲染链,场景链这三个系统的实现。能这么顺利,运气其实占了很大的因素(笑)。node
虽然因为此次更新的速度快的离谱,但还请各位放心,至少不会像法国土豆的年货游戏那样遭(育碧:你礼貌么?)。由于本次的内容会触及本引擎渲染系统最核心的一些部分,虽然不能说最复杂,但至少在某些方面也奠基了本引擎的将来开发基调。因此内容可能会比较长,还请各位耐心观看。c++
好的,正文开始,好戏开场!数组
在上一篇博文的末尾,我提到了咱们的引擎目前还存在的一个问题,那就是依旧含有较高的平台依赖,准确的说是对OpenGL的依赖,在咱们的RenderFrame
类的实现里面还存在着大量gl打头的函数调用。以及许多函数的参数列表里还有着OpenGL的上下文类型,这明显是一个比较致命的问题,好比咱们太膨胀想要将本引擎移植到PS5或者是XBOX Series S|X上呢。虽然在VULKAN这种抽象层级低的API大行其道的当代使用一个API就可在几乎全部平台上流畅运行,但因为概念构型与OpenGL这种老一代的图形API不一样以及本人技术力太过生草(如今还不会用VULKAN画三角形),因此目前咱们只能为咱们的引擎作好可能会移植到DX11平台甚至是新的VULKAN的准备(固然也有可能一直赖在OpenGL不走了),为了下降引擎与图形API的耦合度,咱们必须将OpenGL抽象出咱们的引擎。安全
这里普及一下关于OpenGL与VULKAN,虽然二者都是由Khronos Group负责维护的API标准,但二者在基础概念上有很大不一样,OpenGL采用单线程状态机,而VULKAN是彻底支持多线程。举个例子,各位常常会发现同一个游戏在不一样版本的显卡驱动中或者同代不一样品牌显卡中会有不一样的帧率表现,这就是因为OpenGL的抽象层级过高以及只支持单线程管线处理所致使的,因为Khronos Group给OpenGL设置的接口太过“天然”化(能够理解为高级程序语言相对应于汇编语言的语言表达高度天然化),而具体实现方法由各个显卡厂商开发的驱动去完成,因此获得的结果良莠不齐,同一个处理纹理的OpenGL函数可能在ATI的显卡上甚至是某个版本的显卡驱动上运行效率极高,在英伟达的显卡甚至某个版本的驱动上效率次一些。而VULKAN不一样,它与显卡之间只有一层“薄显卡驱动”,VULKAN给的API更加贴合显卡的工做原理,将一切的优化工做交给软件开发者,也便使得它比起老前辈OpenGL更跨平台以及更有效率。数据结构
回到咱们的引擎中。说了那么多,也只是为了让你们意识到引擎与图形API之间抽象的重要性,而并非将OpenGL贬到蛮荒之地去,相反,OpenGL对开发者是最友好的API没有之一。好的,接下来具体到咱们的引擎实现中来。多线程
值得欣慰的是,因为目前咱们很及时地意识到图形API抽象的重要性。因此在状况并未一团糟的状况下,咱们能够很方便的进行图形API抽象。既然要抽象,那就抽象地完全一些,咱们在引擎解决方案里新建一个VS静态库项目,专门存放与OpenGL底层API交互的逻辑实现。到时咱们的引擎只要调用由这个静态库抽象出的方法便可。架构
因为咱们须要在这个抽象模块中实现OpenGL的方法,那么咱们首先就得为项目建立依赖,即GLFW以及GLAD的附加包含目录以及附加依赖项。并且咱们还想将ImGui的初始化与上下文也独立出去,因此也请包含ImGuiSharedLib项目。app
在以上全部工做作好后,咱们开始代码工做,首先新建一个定义用头文件SPOpenGLRenderer.h
,即和VS项目名一致便可。在本文件中键入以下代码:框架
// 包含OpenGL抽象方法类 #include "SPOpenGLRenderAPI.h" // 包含抽象出的上下文类 #include "SPRendererCtx.h" #include <imgui_impl_glfw.h> #include <imgui_impl_opengl3.h> namespace Shadow { // 既然要抽象,那就抽象地完全一些,把名称上的依赖也给抽象掉 typedef SPOpenGLRenderAPI SHADOW_RENDER_API; typedef GLFWwindow* SHADOW_RENDER_API_CTX; typedef ImGuiContext* SHADOW_IMGUI_CTX; }
接下来在此VS项目里新建类,名为SPOpenGLRenderAPI
。从构建日志系统得来的经验告诉咱们:咱们能够将这个类构建成一个静态类,这样能够不创建额外对象占用空间以及不会产生全局变量重定义等问题,那么将以下代码实现键入类中:编辑器
// Declare. class SPOpenGLRenderAPI { public: // RenderFrame. // 这个函数就是将咱们在渲染框架构造函数中执行的相关方法。 static void RendererInitialize(int _iScrWeight, int _iScrHeight, std::string _sWindowTitle, bool& _bIsWithEditor); // 相对应为渲染框架中析构函数中执行的相关方法。 static void RendererTerminator(); // 关于这里我为何会写loopstart以及loopend两个函数 // 这也就是状态机系统的一大弊端,任何流程都是严格线性的,渲染中循环也是同样 // 好比渲染一个三角形的渲染代码必需要在glClear之后并在glSwapBuffers之前同样 static void RendererLoopStart(); static void RendererLoopEnd(); // 因为查询方法内部实现仍是用到了平台相关代码,因此我又将它抽象了一层 static bool WindowStatusQuery() noexcept; // 因为咱们将ImGui初始化以及绘制等相关过程也交给了抽象方法类,因此编辑器的相关开关也要被移到这里 // 其实还有一个解决方案,这也是我在写这篇博文时才想到的,能够将ImGui的初始化独立出另外的方法, // 这样也比较符合单一职责原则一些。你们也能够试一试。 static bool GetEditorSwitch() noexcept; // 这就是咱们将上下文独立后的产物。 static SPRendererCtx* GetContext() noexcept; // 返回出API的上下文 static GLFWwindow* GetAPICtx() noexcept; // 返回出ImGui的上下文 static ImGuiContext* GetImGuiCtx() noexcept; private: static bool b_isWithEditor; static SPRendererCtx* rc_Ctx; };
在编写完之后,咱们就能够将咱们上次编写的上下文抽象也加进来了,这样,一个较为完整的图形API抽象就完成了,其实还有许多方法在咱们开发后期还会加进去,不过目前这些方法足够了。将本抽象静态库编译后接下来将全部引用OpenGL的引擎模块更换OpenGL依赖为咱们写的本抽象静态库。按下F5后咱们会发现运行成功。正如咱们所预期的那样。
接下来开始进行渲染核心的设计,这也是本文此次要着重讲的地方。在当初引擎的应用程序架构刚搭建好时,咱们就发现咱们的应用程序若要想成功在入口点内运行,只能经过C++的运行时动态类型判断以及一大堆的回调函数。咱们的渲染对象也是如此,就好比渲染场景时引擎框架是彻底不知道咱们的场景中有多少个物体,多少个光源等,有多是一个,也有多是114514个(这么臭的场景真是屑),引擎是没法预测的。咱们总不可能将待渲染组件所有写死在渲染框架里,这样就失去游戏引擎的灵活性了。因此在研究了许多现行成熟的引擎,以及结合了本人极度生草的技术力后,本人为此引擎设计了一套链式渲染核心,从小到大分别是基础渲染对象,渲染链,场景链。接下来我会对每个概念进行说明。
在对每一个概念进行说明以前,我会结合一点例子来讲明我这套渲染核心的工做原理,但愿你们在看完本文后会对这款引擎渲染核心的设计思路有所了解,在之后开发本身的引擎中提供思路和帮助。
因为咱们这套引擎在3D和2D场景下皆可适用,因此咱们必需要折中找到3D和2D场景中的共同点,那么,首先让咱们来看看3D场景中的特性。
相信各位之中有许多曾经体验过虚幻引擎或者Unity引擎开发游戏的开发人员。不知在各位的开发过程当中是否发现过咱们使用的Actor或者是某些模型文件真正在3dsmax或者是maya以及blender之中是由多个模型零件组成的模型组?以及在咱们的场景开发中咱们会发现咱们的场景实际上是由一系列的模型对象组成,好比一个库房的场景就由一大堆的货箱以及昏暗的电灯组成。真实世界的组成也是这样由一大堆的元素组成,用哲学中惟物辩证法关于联系的观点的一句话说就是:“事物内部不一样组成部分的联系体现了事物具备内部结构性”。以及在后来咱们引擎中须要使用的assimp模型载入库里,也是将3D模型拆分红多个模型零件导入到内存中的。
接下来我们聊一聊2D场景,以我最喜欢的PSP游戏之一《超级弹丸论破2》来讲,在游戏里面有这么一个系统,以下图示:
当玩家在海岛外景上漫游时,多是因为PSP机能限制,Spike将漫游从一代的3D漫游变成了2D卷轴场景,但相信各位看到后都会说:这多简单,一幅画加一个动图就实现了,是么?真有这么简单那可就省了很多事。其实剔除玩家操纵的创妹以及人物立绘,这样一个2D卷轴场景至少用到了多达六个的图层(尤为是在将来旅馆门口那里用到的图层是最多的),这是因为要体现近大远小以及近快远慢的场景透视特性。一个图层就可看作一个场景组件。固然,更复杂的还在后面,玩家操控的创妹可不只仅是一张简单的动图精灵,因为本人贴的截图是来自于模拟器版,因此画面精细了许多,有些细节不容易看出来,但要是各位有条件的话能够仔细观察PSP版本的画面,创妹的腿部以及手臂的关节处是有细小的缝隙的,也就是说2D卷轴中的创妹是由一堆面片经过2D骨骼拼接出来(听起来虽然有些不寒而栗,但真实状况就是如此),使得2D人物的运动相比gif动图更加真实天然(各位也不用对着2D骨骼技术望洋兴叹,本引擎在后期也会加入2D骨骼系统,这也是本人构建2D系统的终极目标。小高,大家的2D骨骼不错么,拿来吧你!)。
因此在总结了以上两个广泛场景来讲,咱们会发现一个共同点,那就是游戏中的场景是由一大堆的组件所组成,而组件又可分化为子组件,而这些子组件即是不可再分的基础渲染单元(注意,这里的基础渲染单元与OpenGL的基础渲染单元不是一个概念)。因此这也便带出了本引擎的渲染核心:基础渲染对象,渲染链,场景链。
首先放出三者之间的联系:
本引擎中内置两条场景链,一条是专用于编辑器窗口渲染。一条专用于单个场景中全部组建的渲染,场景链的每一个节点内都含有一条渲染链,一条渲染链就表明一个场景组件,也就是一个模型组或者一个创妹,而一条渲染链中能够有多个基础渲染对象结点,而每一个基础渲染对象节点就是引擎最小的渲染单元,也就是一个零件模型或创妹的一个面片,OpenGL的绘制顺序也即是由大到小,即从场景链开始检索每一个场景链结点,而进入了场景链结点的绘制函数后,场景链结点会将OpenGL导引到场景链下每一个基础渲染对象的渲染函数的里面进行相关绘制,渲染完一个节点后跳到下一个继续,直到渲染完全部的结点为止。固然绘制的类型根据传入的上下文自动选择。
因为采用的是链表的数据结构,因此彻底不用担忧一个场景中不能拥有任意数目的组件以及一个组件中不能包含多个元素,只要你的电脑够强劲,组件随便加(笑)。固然,本渲染核心也是有一些缺点的,好比内存分配方面,以及链表遍历消耗的时间和算力都是比较高的,并且在面对开放世界场景须要多个场景块加载的状况时(好比虚幻5的演示Demo,我真的好酸)就会力不从心了。本引擎也没法应对大型游戏开发的性能需求。
不过目前在中小体量游戏开发中,本人仍是颇有自信地认为本人设计的架构能够胜任(欢迎有游戏引擎开发经验的大佬光速来打我脸)。在说明了大体架构设计后,咱们即可以开始进行相关实现了。
这是本引擎最基础的渲染单元,因此其中要实现的功能是最多的,不过目前咱们不用添加太多属性和方法,目前注重于数据结构的实现。因为为了让引擎在不知道的状况下能够运行咱们设定的各个不一样的基础渲染对象(好比光源或者是犹他茶壶等),因此这个基础渲染对象类是做为一个虚基类而存在的,在真正运行的时候,引擎经过C++的RTTI机制来调用真正对象里面的绘制方法。因此接下来让咱们建立基础渲染对象的类声明与类定义。
首先咱们能够知道的是咱们的基础渲染对象须要有的功能是绘制以及判断是否可绘制的方法。可是因为编辑器窗口也是一种基础渲染对象,因此咱们须要建立一种方法的两种不一样重载来应对不一样的绘制上下文。因此咱们能够这样去写:
// 这两个函数都是虚函数,方便派生类能够直接在里面写逻辑 // 其实后期还会在这里加入摄相机变换矩阵的参数 // 不过这都是数学库建好以后的事情了,如今先不着急 void SPRenderObj :: Render(GLFWwindow*) { EngineLog :: ErrorLog(SHADOW_ENGINE_LOG, "Wrong context import in."); // 组件渲染代码(请在派生类里面实现) } void SPRenderObj :: Render(ImGuiContext*) { EngineLog :: ErrorLog(SHADOW_ENGINE_LOG, "Wrong context import in."); // 窗口渲染代码(请在派生类里面实现) }
到时咱们只要写好对应对象的渲染函数便可,当因为某些缘由不当心写错上下文时也不至于程序崩溃,顶多就是不进行绘制并报出错误信息而已。
而后接下来是其中的判断是否可绘制方法,关于这个最直观的体现即是游戏场景模型被破坏后留下的缺口,举一个你们都知道的例子:《侠盗猎车手:圣安地列斯》中剧情最后一幕反派警察驾驶的消防车从葛洛夫大街的桥上冲了出去,在剧情结束后,咱们会发现桥上那个被撞开的缺口会一直存在,其实桥梁在建模的时候自己就是有一个那样的豁口,只是在游戏事件触发前,栏杆以及它所对应的碰撞盒是被容许绘制的,但事件发生后,引擎取消了那一段栏杆模型与碰撞盒的绘制许可,因此在接下来的渲染循环中再也不被绘制。这种断定其实只要一个私有布尔成员以及它的相关Get与Set方法便可解决,这里再也不过多赘述。而后就是注意在派生类的渲染函数的绘制中记得使用相关断定便可。
咱们先从渲染对象代理讲起,因为咱们的基础渲染对象只负责基础渲染功能,它并不知道其余基础渲染对象的存在,但因为咱们最终要把基础渲染对象置入渲染链中,因此咱们必需要让引擎能够找到下一个基础渲染对象,固然咱们也能够给基础渲染对象里面加指向下一个基础渲染对象的指针,从而让它“知道”下一个基础渲染对象的位置,不过这就容易形成必定的耦合性了,即不符合单一职责原则,而且更要命的是指针操做一旦出现问题则容易形成程序的彻底崩盘,咱们更但愿有一个单独的类来帮咱们的基础渲染对象去干这些事情,而不是让咱们的基础渲染对象去当“多面冠军”。
因此此时咱们就须要渲染代理类SPRenderListNode
(我固然知道代理的英文是Surrogate,只是为了让它表达渲染链结点的意思)来负责这一功能,它能够接管原先须要基础渲染对象所作的节点相关操做,并且避免了在往后可能由于更换API致使基础渲染对象类声明重写带来的的麻烦。接下来让咱们进行声明:
class SPRenderListNode { public: // 默认构造函数,许多人会问这里为何会须要默认构造函数 // 不要着急,稍后我会讲到 SPRenderListNode(); // 原则上这个函数是不会调用的,即便调用了,绘制的结果也只是将一个物体 // 在同一个状态和位置下绘制两次罢了。 SPRenderListNode(SPRenderListNode*); // 为结点指定相应的须要被代理的基础渲染对象 SPRenderListNode(SPRenderObj*); // 析构函数 ~SPRenderListNode(); // 咱们将设置结点下一个指针指向的操做独立在结点的类内。 bool SetNextNode(SPRenderListNode*); // 返回指向下一个节点的指针。 SPRenderListNode* ReturnNextNode() const noexcept; // 返回被代理的渲染对象 SPRenderObj* GetObj() const noexcept; private: SPRenderListNode* sprlNode_next; SPRenderObj* sprObj_nodeCtn; };
很简单,一个渲染链结点(基础渲染对象代理)只要作这么多就能够了,它只起到连接一系列渲染对象的做用。因为咱们的渲染对象与代理结点之间使用指针连接,因此咱们必需要考虑到重复赋值所带来的一些问题,如图:
上图表示的是咱们引擎中的其中一条渲染链,在某些特殊状况下这条渲染链中的结点A和结点B均指向了同一个基础渲染对象,这看起来没什么,就像我说的顶可能是绘制两次罢了,但实际上可没有这么简单,假如说此时这条渲染链出于某些缘由被释放出内存,当A先于B释放时,A会直接调用delete关键字释放了本基础渲染对象的内存,而这段逻辑内存映射的真实物理内存中没人知道谁还在里面存储了什么,甚至有多是系统级进程(这个就与操做系统自身内存调度相关了),那么当轮到B的时候,B若是再次调用delete进行释放的话,那便会由于访问未知内存内容形成整个程序的崩溃,最严重的状况甚至有可能致使整个操做系统的崩溃(著名的“《彩虹6号》PS4版死机问题“大部分就是因为糟糕的内存管理的锅)。因此咱们须要有一个组件或者同等类别的机制来确保咱们的渲染链安全释放内存。因此这时咱们就能够为每一个基础渲染对象设置一个计数器,而这个计数器的做用就是为了统计同时链接到本基础渲染单元的代理结点数。当代理结点数大于1时,代理结点释放时就没必要释放掉基础渲染对象,只有当代理结点数等于1时,代理结点才会释放掉连接的基础渲染对象。经过设置这种释放规则来保证内存安全。
因为C++的一个特色即是OOP,也就是说咱们能够将计数器单独抽象出一个类,尽可能下降耦合,确保单一职责。不过这种计数器的结构比较简单,本人不在这里展现它的代码,我会说明其中的逻辑,你们能够尝试着本身实现:既然计数器是单独抽象出来的类,那咱们为了尽可能下降耦合性以及一个基础渲染对象对应一个计数器的状况,咱们能够用前向声明以及指针去让代理结点知道计数器的存在,在复制构造的时候咱们会同时获取另外一个代理所指向的计数器,并实现加1操做。在释放资源的析构函数中,咱们会先让析构函数去到指向的计数器里来判断此时同时指向本渲染对象的代理数目,若惟一,则同时释放掉渲染对象,若不惟一,则将指向计数器以及渲染对象的指针置为空(nullptr)便可。
在完成了渲染代理结点后,咱们即可以开始渲染链的声明,既然咱们要尊重单一职责原则,那么咱们只要在这个类里实现链表相关操做(增,删,查就够了,插入的操做没有任何须要,因为OpenGL是经过深度来肯定绘制的层次关系,而不是Java Awt中的前后顺序)便可。类的声明以下:
class SHADOW_STAGE_API SPRenderList { public: // 这里是默认构造函数 SPRenderList(); // 析构函数,因为有了渲染对象的计数器,咱们须要在析构函数里作的工做会轻松不少 ~SPRenderList(); // 添加渲染代理结点 bool AddNode(); // 为渲染代理结点添加渲染对象 bool AddNode(SPRenderObj*); // 剔除代理结点(头插法逆过程) bool SubNode(); // 剔除符合相关"ID"条件的结点 bool SubNode(SHADOW_RENDER_OBJ_ID); // 渲染结点的两个重载函数 void NodeRender(SHADOW_RENDER_API_CTX); void NodeRender(SHADOW_IMGUI_CTX); // 查找符合相应ID的结点的位置 SPRenderListNode* SPRLSearchNode(SHADOW_RENDER_OBJ_ID); // 设置渲染链的ID void SetId(SHADOW_RENDER_LIST_ID); // 获得渲染链的ID SHADOW_RENDER_LIST_ID GetId(); // 设置以及获取渲染连的渲染许可 void SetDrawSwitch(bool _bIsDraw) noexcept; bool GetDrawSwitch() noexcept; private: bool NodeIsExist(SHADOW_RENDER_OBJ_ID); SHADOW_RENDER_LIST_ID s_Id; // 指向链表的指针,结合上面的默认构造函数以及无参的AddNode方法你们能够看出 // 这里也就是我为何须要在渲染代理结点里设置默认构造函数的缘由: // 即单个指针不可能进行相关设置操做,也就是说单个指针在未指向实际的对象的内存时, // 咱们无权经过指针操做类中的函数,若是非要这么作,没人知道会发生什么事情。 SPRenderListNode* sprl_list; bool b_isListDraw; };
这样,咱们便构建了一条较为完整的渲染链,咱们能够在渲染框架中试验一下:咱们首先在引擎编辑器模块中建立一个渲染对象的派生类AppEditorDemo类,在其窗口的渲染函数中随便写一点窗口内容,咱们能够用这个类建立几个渲染对象(记得把窗口名称名称换一下)。而后在渲染框架中建立一条渲染链,依次将咱们建立的渲染对象加入进去,最后由程序绘制,Application类里构造函数的源代码以下:
// 建立三个基础渲染对象 appDemoAlfa = new AppEditorDemo("LATempleA"); appDemoAlfa->SetDrawSwitch(true); appDemoBeta = new AppEditorDemo("LATempleB"); appDemoBeta->SetDrawSwitch(true); appDemoGamma = new AppEditorDemo("LATempleG"); appDemoGamma->SetDrawSwitch(true); // 建立渲染链(这一段代码是在渲染框架里) SPRenderList* sprlA = new SPRenderList(); sprlA->SetDrawSwitch(true); // 将咱们建立渲染对象加入进渲染链中 sprlA->AddNode(appDemoAlfa); sprlA->AddNode(appDemoBeta); sprlA->AddNode(appDemoGamma);
运行结果以下所示:
看起来很不错,不过若是各位是第一次运行的话会发现貌似只绘制了一个窗口,没有关系,咱们能够试着把第一个窗口移开,就会发现其实三个窗口在同一个地方绘制的,这是ImGui在第一次绘制时并不会产生相关窗口属性的配置文件,不过咱们后期能够在程序中写死窗口的相关属性,毕竟编辑器只有一套。
在成功建立了渲染链后咱们就能够建立场景链了,场景链与渲染链之间只是改了数据类型而已,其内部实现逻辑是一致的,因此具体实现不作过多说明,Application中的检验代码以下:
// 渲染链A中的渲染对象 appDemoAlfa = new AppEditorDemo("LATempleA"); appDemoAlfa->SetDrawSwitch(true); appDemoBeta = new AppEditorDemo("LATempleB"); appDemoBeta->SetDrawSwitch(true); appDemoGamma = new AppEditorDemo("LATempleG"); appDemoGamma->SetDrawSwitch(true); // 渲染链B中的渲染对象 appDemoAlpha = new AppEditorDemo("LBTempleA"); appDemoAlpha->SetDrawSwitch(true); appDemoBravo = new AppEditorDemo("LBTempleB"); appDemoBravo->SetDrawSwitch(true); appDemoCharlie = new AppEditorDemo("LBTempleC"); appDemoCharlie->SetDrawSwitch(true); // 共同建立两条渲染连 SPRenderList* sprlA = new SPRenderList(); sprlA->SetDrawSwitch(true); SPRenderList* sprlB = new SPRenderList(); sprlB->SetDrawSwitch(true); // 为第一条渲染链添加结点 sprlA->AddNode(appDemoAlfa); sprlA->AddNode(appDemoBeta); sprlA->AddNode(appDemoGamma); // 为第二条渲染链添加结点 sprlB->AddNode(appDemoAlpha); sprlB->AddNode(appDemoBravo); sprlB->AddNode(appDemoCharlie); // 将两条渲染链添加入渲染框架内的场景链中 this->ReturnRFInstance()->spsl_demo.AddNode(sprlA); this->ReturnRFInstance()->spsl_demo.AddNode(sprlB);
最后的运行结果以下:
当咱们取消掉渲染链A的绘制许可即设置不可绘制时,结果以下:
成功了,咱们引擎的渲染核心成功运行,在程序结束后,程序也成功释放资源并退出。说明咱们构建的渲染核心的确是按照咱们的构想成功运行。
其实这里还有一个问题,咱们在有玩游戏时会常常发现,有时咱们须要在两个或者多个场景之间来回切换,像上述检验代码中的这种步骤若是在每一次切换场景中都运行一遍那显然很低效,过长的加载时间会消耗玩家的热情,因此咱们还须要在引擎中设置一个场景缓冲区,但固然这个缓冲区是一个定长指针数组,当咱们在游玩这个场景时,引擎会开辟另外一个线程并在这个新建立的线程内自动读取并建立与咱们游玩场景相关联的其余场景并加载进这个缓冲区中,以致于咱们须要在切换场景时不会打断咱们的游戏体验,不过这都是后话,至少是在咱们引擎线程库建立以后的内容了。
在本文中,咱们成功抽象出了图形API以及设计并成功实现了引擎的渲染核心系统。看起来的确是有点游戏引擎(或者说是渲染引擎)的样子了。不过仍是有一些问题存在,不知各位有没有发现,咱们的引擎从开始搭建到如今一直都在进行一个特别危险的行为:直接使用new以及delete关键字去进行相关内存的分配与释放操做,这种操做在小型程序中并不会产生多大的问题,可是会在尤为是游戏引擎这种对于性能要求极高的大型软件项目中会不可避免的会产生野指针,空指针访问等一系列的致命问题。虽然new与delete关键字比起C语言的malloc以及free安全得多,但仅仅是对于小项目来讲。一个好的内存管理是整个引擎良好运行的基础,因此这也便迁出本人下一次将会和各位探讨的内容——内存管理模块,这会是一个较大的系统模块,因此我计划着用一整篇博文去进行讨论,因此,敬请期待。好的,下次见~
本做品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行过许可