摸了兩個月的魚,又一次拾起了本身引擎的框架,開始完善引擎系統,若是非要用現實中的什麼東西比喻的話,那麼咱們目前實現的框架連個腳手架都不是。把這項目這樣晾着顯然不符合本人的風格,並且要做爲畢業設計的東西可不能矇混過關。因此如今成了既要準備研究生考試又要忙於設計框架並編碼的狀況,生活已經充實到必須得抽空來寫blog了。ios
還有一件事,就是咱們的引擎如今的構建步驟可能要與我曾經參考的Cherno大佬的不一樣了,其中一個緣由是由於他的game engine系列還在更新,對代碼的修改也相比剛開始有很大區別,目前榛子引擎架構只有大致上與曾經視頻中講述的一致,具體代碼實現有許多部分已經不一樣了。這也給在下徒增了很多麻煩。固然,本人的引擎在後期也會變成這樣,可能你會在很長一段時間後纔看到這個系列博文,而此時我發佈在GitHub和碼雲上的引擎源碼或許已經徹底不一樣(本系列博文在連載時並不會放出源碼,因此若是你看的時候本系列還未更新結束,或許不用太擔憂),這是不可避免的,不過文檔的歸檔性至少比視頻要好。還有一個緣由就是本人的英語聽力能力實在太過生草以及Cherno本人後期的語速實在太快,表示已經看不下去。若是對各位形成不便還請理解。c++
這段時間發現了一本比較好的書,是關於遊戲引擎的,是由企鵝的程東哲大佬寫的《遊戲引擎架構與實踐》(暫時忽略企鵝家那些遊戲爛到家的口碑,那些都是策劃的鍋,至少企鵝的技術人員仍是很強的),本引擎在後期的內存分配以及數據結構容器等部分可能會參考本書上的實現,各位也能夠買來看看。git
好了,正文開始,精彩繼續。數據結構
咱們剛開始在引擎核心那裏架設了入口點,但當咱們在應用程序(遊戲或編輯器)項目中寫入任何處理流程時咱們會發現引擎核心是並不會執行的。這很好解釋,咱們的引擎核心並不知道咱們應用程序項目的存在,應用程序項目只是單向依賴引擎核心,而且更明顯的緣由是咱們沒法將應用程序項目中的處理步驟寫入引擎核心的入口點的main函數裏。強制性經過include來引入沒人會知道發生什麼事,恐怕只有編譯器本身知道。架構
接下來就是解決方案,咱們如今來建立一個應用程序接口,其實接口這個說法並不怎麼嚴謹,按照嚴格OOP規則,接口內是不容許有方法實現的,但C++在這方面並不怎麼「守規矩」以及咱們的引擎核心有時也要實現其相關方法,但實在找不到個什麼別的說法,因此就先勉強湊合一下。那麼咱們就先在引擎核心類內部聲明並定義一個應用程序接口BaseApplication
類,聲明與定義以下:框架
// BaseApplication.h(聲明) #pragma once #include "Core.h" namespace Utopia { // 還記得我在上一篇文章中說過的內核規則麼? // 這裏爲了將咱們這個應用程序接口暴露在dll外面,咱們能夠對類聲明也這樣作 // 在類名前加上已經定義好的ENGINE_API便可,條件編譯會保證調用正確,你能夠用本身上次定義的宏 class ENGINE_API BaseApplication { public: BaseApplication(); virtual ~BaseApplication(); void ExcuteLoop(); virtual void ExcuteCallback(); private: }; } // BaseApplication.cpp(定義) #include "BaseApplication.h" #include <iostream> namespace Utopia { BaseApplication :: BaseApplication() { // 構造函數定義,用來在這裏進行引擎核心相關的初始化步驟 // 好比渲染框架的初始化,log系統的初始化等 std::cout << "BaseApplication default constructor.\n"; } BaseApplication :: ~BaseApplication() { // 析構函數的定義,用來釋放已經被引擎核心調用的相關資源 std::cout << "BaseApplication default destructor.\n"; } void BaseApplication :: ExcuteLoop() { while(true) { // 把渲染以及每幀消息處理相關代碼放在這裏 // 鑑於目前並無開始渲染框架的構建,循環條件暫時以true代替,各位也能夠隨便編寫一些條件測試一下 // 但後續請記得刪掉 this->ExcuteCallback(); } } void BaseApplication :: ExcuteCallback(){} }
固然,老規矩,類名和命名空間名任君喜歡,但在後續調用中請記住它們的名字,以便調用。編輯器
這個時候呢,咱們已經建立了引擎的應用程序接口類,接下來就是要在應用程序內建立應用程序接口類實現了,在咱們的應用程序項目下新建一個.cpp文件便可,由於應用程序接口實現類是沒有別的類會調用它的。聲明與定義以下:函數
// Application.cpp(聲明與定義) #include "Engine.h" #include <iostream> class Application : public BaseApplication { public: Application(); ~Application(); void ExcuteCallback(); private: }; Application :: Application() { // 構造函數,用來初始化應用程序內的一些成員 // 好比編輯器的UI框架,又或者是別的一些東西 // 這裏UI框架有些特殊,這裏稍微劇透一下,本引擎打算使用的編輯器UI是著名的DearImGui // 但它的初始化過程必須在OpenGL相關API初始化併成功建立上下文以後,但這裏不用擔憂, // 因爲程序在運行時會首先運行接口類的初始化過程,完成後才運行本實現類的初始化過程。 std::cout << "Application default constructor.\n"; } Appication :: ~Application() { // 析構函數,用來釋放資源 std::cout << "Application default destructior.\n"; } void Application :: ExcuteCallback() { // 用來將應用程序中須要在渲染與消息處理循環中處理的東西放在這裏 // 想必各位應該已經發現了這個函數實際上是接口類BaseApplicaiton的一個虛函數, // 由於只有這樣纔可讓接口類運行應用程序中的處理流程(虛函數可真是個好東西) std::cout << "Application ExcuteCallback() has called\n"; }
細心的同窗此時應該發現問題了,你的下一句即是:永樂,這裏有點不對勁,即便已經聲明瞭應用程序接口,但引擎核心仍是不知道應用程序中實現類的存在,那麼咱們仍是沒法在入口點運行,以下:工具
// EntryPoint.h int main(int argc, char** argv) { BaseApplication* ba = new Application(); // 這裏即便支持里氏替換原則,但編譯器並不知道這個Application是誰 ba->ExcuteLoop(); delete ba; std :: cin.get(); return 0; }
這裏不用着急,咱們能夠利用一個特性(Mojang:方塊懸空不是bug,是特性!!!):即聲明與定義能夠在不一樣的文件裏面。咱們能夠在BaseApplication
的聲明文件裏面添加這樣一個函數的聲明,也就是這樣:oop
namespace Utopia { class ENGINE_API BaseApplication{ ··· }; // 咱們在這裏寫上聲明 BaseApplication* ReturnAppInstance(); }
而咱們會在Application.cpp
裏面這樣去實現:
Utopia :: BaseApplication* Utopia :: ReturnAppInstance() { return new Application(); }
這下咱們就完成了一次「偷天換日」,咱們將尋找實現的工做交給編譯器,接下來要作的就是接一杯摩卡坐在躺椅上慢慢享受緩慢的MSVC編譯過程……固然不是,距離成功運行咱們還有些工做沒作,那麼接下來讓咱們一塊兒來看看。
首先,就是Engine.h
中的問題,咱們雖然成功建立了應用程序接口,但咱們並無在Engine.h
中包含應用程序接口的聲明文件,以及咱們並未包含引擎規則。因此咱們會這樣作:
#pragma once #include "Engine.h" #include "Core.h" #include "BaseApplication.h"
以上就是目前Engine.h
的徹底體。
接下來是處理入口點中的一些問題:既然咱們的入口點纔是真正的執行體,那麼咱們便要定義以下執行體:
#include "Core.h" #include "BaseApplication.h" #include <iostream> // 關於這裏爲何要使用extern關鍵字: // 編譯器可沒有IDE那麼聰明直接進行跳轉,因爲編譯器並未在同名.cpp文件內查找到相關函數聲明 // 若是咱們不作些什麼的話,那麼編譯器就將錯就錯認爲咱們並未建立定義了,因此這時使用extern關鍵字 // 用來告訴編譯器這個函數在別的地方已經定義過,讓它擴大搜尋範圍。 extern Utopia :: BaseApplication* Utopia :: ReturnAppInstance(); int main() { BaseApplication* uBA = ReturnAppInstance(); uBA->ExcuteLoop(); delete uBA; std::cin.get(); return 0; }
這樣便萬無一失了,來按下f5鍵開始編譯。最後運行結果應該是以下幾句(前兩句打印完後實際上是會再也不打印的,緣由是我爲循環設的條件爲true,這時爲了顯示下面兩句(運行析構,強制性關閉並不會運行析構),能夠考慮加入某些循環成立條件):
BaseApplication default constructor. Application default constructor. Application default destructor. BaseApplication default destructor.
不知你們發現沒有,BaseApplication
的構造和析構流程將Application
的執行流程「包裹」起來。這樣也便成功達到咱們的目的:即先進行基礎框架的初始化,再完成更高級模塊的初始化,釋放資源時正好相反。這樣就能防止像Imgui初始化和釋放資源時特殊狀況了。
還記得我在上一篇文章說的日誌系統麼?此次就來填掉這個坑。這個部分是幾乎全部應用程序都會有的一個子模塊,好比CAD,模擬器(RPCS3,PPSSPP和PCX2等),以及你如今正在用的VS,各式各樣的控制檯程序等等……咱們的引擎固然也不能少,至少在編輯器中咱們是很是須要這個系統的,以及在遊戲製做中的調試裏咱們也有很大的須要。因此,接下來開始構建日誌系統,不過別擔憂,這個系統很簡單,稍微一點點步驟就會完成。
咱們如今先在解決方案文件夾裏新建一個文件夾Vendor(小攤販?不過也差很少,後續咱們引用的第三方庫多起來的時候是否是就應該叫作Supermarket了?),專門在這個文件夾裏放置各類第三方工具或代碼。
咱們的並不會本身從頭去寫一個日誌系統,咱們將採用一個第三方代碼庫:spdlog,這是一個調用很是簡單,使用容易上手而且極其強大的專門的日誌代碼庫,它默認有三種提示類型:error,warning,information,分別對應不一樣的提示顏色,你能夠增長類型並自定義顏色,並且你甚至能夠不只讓日誌輸出在控制檯上,你也可讓它輸出在任何你想要的界面上,不過鑑於本人技術力太過生草以及本引擎的體量,使用默認的設置就足以完成咱們的需求。
前往GitHub去下載spdlog的源碼(連接我就不放了,在GitHub搜索很容易就找到),記住,是下載源碼,若是你的引擎項目添加了Git跟蹤,你能夠直接用git module
命令扒取下來,這裏不對這個命令作過多解釋。下好源碼後就能夠將源碼文件一股腦地全扔進Vendor文件夾裏面。接下來請打開你的VS,咱們要對咱們引擎項目作些設置:
注意,這裏的「項目」並非指在引擎以外新建一個項目,而是VS解決方案中的「項目」,藉此機會說明一下對應關係,其實咱們的引擎項目對應的是VS中的解決方案,而VS中的項目的概念對應的是咱們引擎項目中引擎模塊的概念。正好就在這裏進行嚴格規定,之後我會將VS的解決方案稱爲解決方案或者引擎項目,VS的項目咱們會稱爲引擎模塊,以此來避免概念混淆。
在本系列的第一篇文章發出後,有同窗提出了反饋,說是新建項目用premake步驟仍是比較麻煩,但願仍是可使用VS圖形化界面來建立,本人想了一下以爲也是比較可行的,一個緣由即是屢次引擎項目從新載入花的時間太長,尤爲是在後期引擎模塊增多了之後那更是緩慢,並且使用腳本並不必定每次都會考慮周到將項目所有設置完畢,模塊的依賴項太多時此缺點極其明顯,相似於「熱編譯」這種的仍是有些吃不消。因此接下來全部的項目構建過程本人都會採用VS自帶的圖形化界面建立,除了特殊之處須要說明外,其餘步驟不放圖。
首先在解決方案下新建一個新模塊(VS選擇「增長新建項目」),因爲這個模塊是專門爲日誌系統準備的,因此就起名叫作EngineLog
便可,接下來在模塊屬性中添加附加目錄,咱們能夠用VS提供的宏定義來編寫附加目錄項。若是此時個人spdlog的路徑是:
D:/Project/UtopiaEngine/Vendor/spdlog
那麼咱們能夠來這麼寫:
$(SolutionDir)Vendor\spdlog\include
這裏$(SolutionDir)
就是D:/Project/UtopiaEngine/路徑的宏定義,這樣就會在因爲由於某些緣由更改引擎項目目錄的狀況時不用擔憂得一條條更改依賴路徑了。如下提供幾個經常使用宏定義:
$(SolutionDir) // 解決方案路徑 $(ProjectName) // 項目(模塊)名稱 $(Platform) // CPU平臺名稱,有x86,x64和arm三種 $(Configuration) // 項目屬性,即Debug,Release,Dist等
接下來設置模塊生成的二進制文件爲「動態連接庫(.dll)」,生成二進制文件的目錄以及obj文件的目錄和引擎核心與應用程序同步便可。(切記必定要將各個模塊最終生成的二進制文件(.lib .dll .exe)均放在同一個文件夾內,premake5中的複製命令也能夠完成,具體作法請參考上一篇)
在繼續以前請爲應用程序和引擎核心模塊添加依賴項,即將咱們的EngineLog做爲它們的依賴項(即項目資源管理器中的依賴項以及模塊屬性中的附加包含目錄均要添加),再而後爲本模塊新建一個文件夾src,代碼文件均放在這裏。完成此步驟以後,讓咱們開始編寫相關代碼。首先呢,咱們須要和引擎核心同樣規定內核規則,新建一個頭文件LogLibDefine.h
用來規定條件編譯(固然不要忘記在模塊屬性的預處理器定義裏面加上UTOPIA_LOG_DLLEXPORT哦):
#pragma once #ifdef UTOPIA_LOG_DLLEXPORT #define LOG_API _declspec(dllexport) #else #define LOG_API _declspec(dllimport) #endif
接下來就是建立相關類的聲明與定義了:
// EngineLog.h #pragma once #include <string> #include <memory> #include <spdlog\spdlog.h> #include "LogLibDefine.h" // 設置兩個宏定義來指定我要使用的日誌輸出類型,分爲引擎日誌和應用程序日誌兩部分 // 引擎日誌主要用在編輯器以及其餘的開發環境中,應用程序日誌主要用在遊戲程序調試或編輯器的相關信息中。 #define UTOPIA_ENGINE_LOG 1 #define UTOPIA_APP_LOG 2 namespace Utopia { class LOG_API EngineLog { public: // 關於這裏我爲何所有使用靜態成員: // 因爲日誌系統的代碼能夠說幾乎在引擎中的全部地方都會調用,若是使用非靜態成員,那每次調用都要在相應類中 // 設定一個日誌類的成員對象,浪費了內存資源不說,可能還會形成不可必要的麻煩。 // 其實關於這個還有一個更好的方法:將本模塊轉爲靜態庫(.lib),這樣便減小了模塊調用之間的麻煩關係與限制。 // 並且本模塊並複雜,因此以靜態庫的形式在程序運行時就裝載進內存對效率的影響影響不算大 // 具體方法具體選擇,你們能夠嘗試用靜態庫包裝本模塊。我目前在這裏先使用動態庫包裝。 static void LogInit(); // 對參數解釋一下: // 1. 類型是整型,用來存放我在上面的宏定義的,程序會根據宏定義的指定來選擇日誌輸出方,便是引擎仍是應用程序 // 2. 類型是字符串,這很好懂啊,你想讓輸出什麼信息,那就把它傳進這個字符串裏就好 static void ErrorLog(int _iLogType, string _sLogInfo); static void WarningLog(int _iLogType, string _sLogInfo); static void InfoLog(int _iLogType, string _sLogInfo); private: // 關於這裏我爲何使用智能指針:官方給的建議是這樣,誒嘿 // 但其實真實緣由也是由於智能指針真的太香了,尤爲是對於這種靜態成員來講,我能夠徹底不用關心什麼時候進行釋放。 static std::shared_ptr<spdlog::logger> s_CoreLogger; static std::shared_ptr<spdlog::logger> s_ClientLogger; }; } // EngineLog.cpp #include "EngineLog.h" #include <spdlog\sinks\stdout_color_sinks.h> namespace Utopia { // 因爲是靜態成員,因此須要在這裏實現一下 std::shared_ptr<spdlog::logger> EngineLog::s_CoreLogger; std::shared_ptr<spdlog::logger> EngineLog::s_ClientLogger; // spdlog初始化步驟 void EngineLog::LogInit() { // 這裏是對Log的格式進行設置,最終輸出結果是: // [xx:xx:xx]Utopia/APP:日誌消息 // 其餘格式你們能夠參考spdlog的官方文檔本身去編寫一個格式 spdlog::set_pattern("%^[%T] %n: %v%$"); s_CoreLogger = spdlog::stdout_color_mt("Utopia"); s_CoreLogger->set_level(spdlog::level::trace); s_ClientLogger = spdlog::stdout_color_mt("APP"); s_ClientLogger->set_level(spdlog::level::trace); } void EngineLog::ErrorLog(int _iLogType, string _sLogInfo) { string s_logErrInfo = "Cannot find log type, please check your code. Origin information: "; switch (_iLogType) { case UTOPIA_ENGINE_LOG: s_CoreLogger.get()->error(_sLogInfo); break; case UTOPIA_APP_LOG: s_ClientLogger.get()->error(_sLogInfo); break; default: s_CoreLogger.get()->warn(s_logErrInfo + _sLogInfo); break; } } void EngineLog::WarningLog(int _iLogType, string _sLogInfo) { string s_logErrInfo = "Cannot find log type, please check your code. Origin information: "; switch (_iLogType) { case UTOPIA_ENGINE_LOG: s_CoreLogger.get()->warn(_sLogInfo); break; case UTOPIA_APP_LOG: s_ClientLogger.get()->warn(_sLogInfo); break; default: s_CoreLogger.get()->warn(s_logErrInfo + _sLogInfo); break; } } void EngineLog::InfoLog(int _iLogType, string _sLogInfo) { string s_logErrInfo = "Cannot find log type, please check your code. Origin information: "; switch (_iLogType) { case UTOPIA_ENGINE_LOG: s_CoreLogger.get()->info(_sLogInfo); break; case UTOPIA_APP_LOG: s_ClientLogger.get()->info(_sLogInfo); break; default: s_CoreLogger.get()->warn(s_logErrInfo + _sLogInfo); break; } } }
完成了以上工做後,咱們即可以開始下面的一步。
首先咱們並不但願日誌系統相關初始化步驟在每一個調用它的模塊裏都執行一遍,那豈不是太麻煩了,瀕危內存保護協會會提出抗議的,因此咱們會讓它在引擎核心老老實實地初始化後就不用再管其餘的事情了。因爲日誌系統並非狀態機系統,因此也便不須要上下文的獲取與釋放,這樣就讓咱們的行動更加靈活了。
老規矩,先爲引擎核心建立相關模塊依賴,兩個依賴建立完成後,咱們還要爲引擎核心也包含spdlog的路徑,在這些前置工做都作完後,咱們即可以肆無忌憚地在引擎核心中調用其相關初始化方法,好比這樣:
BaseApplication :: BaseApplication() { std::cout << "BaseApplication default constructor.\n"; EngineLog :: LogInit(); }
當咱們想要調用的時候就不須要再次初始化即可直接在想要調用其方法的函數體裏調用。固然,別忘了爲調用日誌系統的模塊建立依賴以及附加包含目錄。運行效果的話你們能夠參考上一篇那裏的截圖,那個就是我用了spdlog所建立的日誌系統
你看,多簡單,就只有簡簡單單的兩步,咱們就建立了一個引擎的框架,其實目前看來這纔算是一個應用程序框架,固然,距離遊戲引擎框架還有必定的路要走,不過也不遠了。再更上個三四回吧,咱們大概就能夠出搭建一個既具備底層渲染框架,事件系統以及音效系統的較爲完善的遊戲引擎框架。哦,作一個預告,下次更新我會開始搭建底層渲染框架以及部署咱們引擎編輯器的UI底層。還請各位敬請期待。
本做品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行過許可