此次博客更新距離上次的時間間隔變短了好多,由於最近硬是抽出了一大部分時間來進行引擎的開發。並且運氣很好的是在寫鏈表這種很「敏感」的的數據結構的時候並無出現那種災難性的後果(恐怕是前一段時間在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 國際許可協議進行過許可