本篇序言
這次博客更新距離上次的時間間隔變短了好多,因為最近硬是抽出了一大部分時間來進行引擎的開發,而且運氣很好的是在寫鏈表這種很“敏感”的的資料結構的時候并沒有出現那種災難性的后果(恐怕是前一段時間在leetcode刷資料結構的原因吧),于是本人才能在上篇博文發布后不久完成了基本渲染物件,渲染鏈,場景鏈這三個系統的實作,能這么順利,運氣其實占了很大的因素(笑),
雖然由于這次更新的速度快的離譜,但還請各位放心,至少不會像法國土豆的年貨游戲那樣遭(育碧:你禮貌么?),因為本次的內容會觸及本引擎渲染系統最核心的一些部分,雖然不能說最復雜,但至少在某些方面也奠定了本引擎的未來開發基調,所以內容可能會比較長,還請各位耐心觀看,
好的,正文開始,好戲開場!
1. 渲染框架(第二部分):OpenGL抽象
在上一篇博文的末尾,我提到了我們的引擎目前還存在的一個問題,那就是依舊含有較高的平臺依賴,準確的說是對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專案,
在以上所有作業做好后,我們開始代碼作業,首先新建一個定義用頭檔案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后我們會發現運行成功,正如我們所預期的那樣,
2. 渲染框架(第三部分):渲染核心的設計
接下來開始進行渲染核心的設計,這也是本文這次要著重講的地方,在當初引擎的應用程式架構剛搭建好時,我們就發現我們的應用程式若要想成功在入口點內運行,只能通過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,我真的好酸)就會力不從心了,本引擎也無法應對大型游戲開發的性能需求,
不過目前在中小體量游戲開發中,本人還是很有自信地認為本人設計的架構可以勝任(歡迎有游戲引擎開發經驗的大佬光速來打我臉),在說明了大致架構設計后,我們便可以開始進行相關實作了,
2.1 基礎渲染物件SPRenderObj
這是本引擎最基礎的渲染單元,所以其中要實作的功能是最多的,不過目前我們不用添加太多屬性和方法,目前注重于資料結構的實作,由于為了讓引擎在不知道的情況下可以運行我們設定的各個不同的基礎渲染物件(比如光源或者是猶他茶壺等),所以這個基礎渲染物件類是作為一個虛基類而存在的,在真正運行的時候,引擎通過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方法即可解決,這里不再過多贅述,然后就是注意在派生類的渲染函式的繪制中記得使用相關判定即可,
2.2 渲染鏈SPRenderList以及渲染物件代理SPRenderListNode
我們先從渲染物件代理講起,由于我們的基礎渲染物件只負責基礎渲染功能,它并不知道其他基礎渲染物件的存在,但由于我們最終要把基礎渲染物件置入渲染鏈中,所以我們必須要讓引擎可以找到下一個基礎渲染物件,當然我們也可以給基礎渲染物件里面加指向下一個基礎渲染物件的指標,從而讓它“知道”下一個基礎渲染物件的位置,不過這就容易造成一定的耦合性了,即不符合單一職責原則,并且更要命的是指標操作一旦出現問題則容易造成程式的完全崩盤,我們更希望有一個單獨的類來幫我們的基礎渲染物件去干這些事情,而不是讓我們的基礎渲染物件去當“多面冠軍”,
所以此時我們就需要渲染代理類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 國際許可協議進行過許可
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296246.html
標籤:其他
