1. 程式人生 > >從新建資料夾開始構建UtopiaEngine(1)

從新建資料夾開始構建UtopiaEngine(1)

## 序言 在苦等了半年多之後,我終於開始了嚮往已久的實時NPR遊戲引擎專案——**Utopia Engine**,這半年多一直為了構建這個引擎在做很多準備:多執行緒、動態連結庫、指令碼引擎、立即渲染GUI……統統吃了一遍(就差彙編沒學了,話說這學期要開這門課來著,結果老師都已經翹課四周了(╯‵□′)╯︵┻━┻)。於是,等不及的我開始了Utopia Engine的構建專案(彙編的知識就一邊做一邊學吧)要趕在畢設前做完這個引擎工作量還是有些大的。當然,畢竟遊戲引擎脫離不了計算機圖形,所以,本部落格裡的計算機圖形學相關內容也不會停止更新,敬請期待! 編寫此文的目的也很簡單:就是為了做一個記錄。在以後的構建過程中防止出現錯誤而無法找到曾經埋下的隱患,並且同時也是學習相關技術的一個筆記,當然,我會盡量把這個系列寫的通俗易懂,可以讓看到這個部落格同時也想要構建自己的遊戲引擎的各位遵循著本系列做出自己的遊戲引擎,是個有點偏向於教程的開發日誌。但是並不是所謂的“0基礎速成班”,至少各位是要有較為足夠的C++語言開發經驗以及計算機圖形學的基礎知識。如果兩者都沒有,那麼各位在啃本人寫的這些文章時恐怕就要有些費勁了。 ## 1.專案設計 雖然說我對這個引擎渲染目標的定位是實時NPR(也就是實時非真實渲染),但其實效果更偏向於NPR裡的“Toon Shading”,即卡通渲染。畢竟也是被新海誠導演製作的電影系列所驚豔到,所以希望通過實時渲染將這“雖然很假,但假的漂亮”的畫面表現出來。不過目前由於本人的技術能力太過於生草以至於如果直接進行NPR的渲染實現不知道會做出什麼屑作來,並且目前現存的成功NPR技術例項也並不是完全在引擎層面上去實現的,因為美術風格可是破壞渲染統一性的最大原因,著名的如萬代南夢宮的《究極風暴》以及《罪惡裝備X》幾乎都是從貼圖層面去實現真實的“假”細節(這可累壞了不少美工和TA),所以本人目前並不打算在Utopia Engine上實現實時NPR渲染,著重實現目前較為流行的PBR(基於物理渲染)技術,也就是真實渲染領域,要求不高,只要能通過本引擎做出一個可以跑的中小體量的3D遊戲即可,這也就是目前本階段的目標。接下來的事情就交給以後的我吧(拜託了,另一個我!) ## 2.初始化工作 ### 2.1 專案構建 首先,宣告一下,我這裡的工作環境是微軟的Visual Studio 2017 Community(以後均簡稱VS2017),使用的圖形api如果不出意外的話應該就是OpenGL,WinSDK是 10.0.17763.0,不過,由於WinSDK所造成的編譯失敗或者是其他除錯中的問題並不常見,所以這裡可以忽略。 那麼接下來開始構建專案,如何在VS2017裡建立一個完整的C++空專案與解決方案並且為它建立本地與遠端git程式碼庫就不用我過多贅述了,相信看到這裡的大家都明白。不過我們接下來並不會在專案裡建立我們的第一個程式碼檔案並且開始莽程式碼。因為VS2017自動建立的生成目錄、中間目錄、專案目錄檔案結構並不怎麼符合我們的需求,只要記住專案名即可。 接下來要介紹一位熟悉的陌生人:premake工具,說它熟悉是因為想必各位都應該聽說過CMake,這個premake和CMake的作用一樣,都是用來做跨平臺專案構建的工具。說它陌生是因為這個名字是真的陌生(至少我周圍圈子的同學都沒聽過),使用這個工具的一個最主要原因是它不用在外網下載MinGW( ̄へ ̄),而且體量極小,一個Lua指令碼就可以完成所有配置並且生成你想要的平臺版本,缺點就是沒有GUI,你得面對CMD無盡的黑暗(這裡強力推薦微軟的Windows Terminal,有了它你甚至可以將你的CMD裝扮成初號機的顯示屏)。不過操作很簡單,而且官方超詳細的Wiki也足以彌補這些缺陷。 如果你沒有premake工具的話,請前往premake的[GitHub專案主頁](https://github.com/premake/premake-core)下載對應作業系統的release壓縮包。解壓後你會得到一個premake5.exe可執行檔案,將這個檔案放入你的解決方案根目錄裡,接下來再在你的根目錄裡新建一個Lua指令碼檔案(檔案字尾名是 **“.lua”**,檔名必須為premake5),在裡面敲入如下Lua指令碼程式碼: ```lua workspace "UtopiaEngine" -- 解決方案名稱(填入你自己的引擎解決方案名稱) architecture "x64" -- 對應執行平臺,如果你想相容更老的32位系統,請再加x86選項 configurations { -- 配置型別:Debug,Release,Dist "Debug", "Release", "Dist" } -- 全域性變數:描述輸出檔案路徑,因為無論是中間目錄還是輸出目錄都有一部分相同的,所以將它們提取出來 -- 和VS2017一樣,路徑定義都可以用特殊格式轉義符來表示,具體轉義符表示意義請參閱premake工具的GitHub wiki,裡面有詳細解釋,這裡不過多說明 outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}" project "UtopiaEngine" -- 引擎專案名,不要說你連專案和解決方案的區別都不知道哦 location "UtopiaEngine" -- 原始檔所在目錄名,這裡建議和專案名保持一致 kind "SharedLib" -- 專案生成型別,這裡選擇SharedLib,在Windows平臺上就是dll檔案 language "C++" -- 專案使用語言型別 targetdir ("bin/" .. outputdir .. "/%{prj.name}") -- 專案成品輸出目錄,Lua中使用“..”作為字串與變數之間的連線符 objdir ("bin-int/" .. outputdir .. "/%{prj.name}") -- 專案中間目錄 files { -- 專案包含的檔案型別,假如說你希望使用VS自帶的.def檔案代替__declspec(dllexport),那麼請加上.def的宣告 "%{prj.name}/src/**.h", "%{prj.name}/src/**.cpp" } includedirs { -- 專案使用的第三方庫的include目錄,spdlog會在後面說明 "%{prj.name}/vendor/spdlog/include" } filter "system:windows" -- Lua裡的條件判斷語句,開始和中止邊界以縮排為準,這裡的意思是若目標系統為Windows則執行下列操作 cppdialect "C++17" -- 專案所遵循的語言標準 staticruntime "On" -- 靜態執行時 systemversion "10.0.17763.0" -- 系統版本號,也就是WinSDK的版本號,如果不知道你自己的,可以填latest defines { -- 專案的全域性巨集定義,後面會解釋兩個巨集定義的意義 "UTOPIA_PLATFORM_WINDOWS", "UTOPIA_BUILD_DLL", } postbuildcommands { -- 需要premake在生成專案時執行的命令 ("{COPY} %{cfg.buildtarget.relpath} ../bin/" .. outputdir .. "/Sandbox") } filter "configurations:Debug" -- debug配置相關設定,下同 defines "UTOPIA_DEBUG" symbols "On" filter "configurations:Release" defines "UTOPIA_RELEASE" optimize "On" filter "configurations:Dist" defines "UTOPIA_DIST" optimize "On" project "Sandbox" -- 接下來是引擎編輯器的專案配置,名字什麼都可以,大致與Utopia Engine專案配置一致 location "Sandbox" kind "ConsoleApp" language "C++" targetdir ("bin/" .. outputdir .. "/%{prj.name}") objdir ("bin-int/" .. outputdir .. "/%{prj.name}") files { "%{prj.name}/src/**.h", "%{prj.name}/src/**.cpp" } includedirs { "UtopiaEngine/vendor/spdlog/include", "UtopiaEngine/src/UtopiaEngine" } links { "UtopiaEngine" } filter "system:windows" cppdialect "C++17" staticruntime "On" systemversion "latest" defines { "UTOPIA_PLATFORM_WINDOWS" } filter "configurations:Debug" defines "UTOPIA_DEBUG" symbols "On" filter "configurations:Release" defines "UTOPIA_RELEASE" optimize "On" filter "configurations:Dist" defines "UTOPIA_DIST" optimize "On" ``` 接下來呼叫cmd命令列鍵入“premake5.exe vs2017”命令,其中如果你使用的IDE是VS2019,那麼就填“vs2019”即可,回車後執行,執行結果如下: (圖1) ![執行結果](https://ae01.alicdn.com/kf/H7b527e527ba44dfe8c5cea822a1c34f8L.jpg) 在繼續之前先解釋幾個事情: 1. 關於為什麼將引擎編輯器和引擎核心分離出來,這一點相信大家如果使用虛幻4引擎寫過專案的話應該是深有體會的:自己專案的解決方案裡通常在自己的專案之外也會包含一個名為UE4的專案(話說才發現Utopia Engine縮寫竟然和虛幻一樣),而這個專案就是引擎核心,是獨立掛載於你的專案上的(ps:貌似UE4編輯器的程式碼就在核心裡面,是要通過手動呼叫方法來略過生成編輯器的),再直白一點說就是你肯定不希望你的遊戲裡還塞著一個編輯器,佔用儲存空間不說,也會消耗一定的計算資源。所以分離開是很有必要的。 2. Lua指令碼中的postbuildcommands選項的解釋: {COPY} %{cfg.buildtarget.relpath}:copy命令,如果是使用VS2017預設的生成目錄,那麼引擎核心dll檔案是與編輯器可執行程式分開儲存在兩個不同的目錄裡,如果要是進行debug(尤其是針對引擎核心的debug),那麼必須在每次debug之前先編譯,然後手動將dll檔案放入編輯器的生成目錄裡,麻煩。所以使用copy命令指示系統在每次生成完dll檔案後會自動複製一份放在編輯器的生成目錄裡。 3. 目前我只寫了關於Windows平臺的命令指令碼,為的是讓專案的目錄結構符合開發習慣並且減少無謂的工作量,所以這個premake指令碼檔案我並沒有寫MacOS以及Linux的生成命令,而且目前完全沒有必要去在這兩個平臺上去除錯程式碼(人窮,買不起Mac)。如果各位有需要,可以去premake的專案主頁看看官方文件。 ### 2.2 隱藏程式入口點 在引擎專案構建之前,我並沒有想好究竟該為這個引擎配置一個怎樣的指令碼引擎。於是,我目前的想法是使用純C++作為引擎的遊戲開發指令碼語言(與其說指令碼倒不如說直接上原始碼,誒嘿!),就和UE4一樣。不過,既然如此,就會很考驗設計模式的基礎了,即面向介面,因為遊戲開發者可不想因為閱讀你的原始碼而浪費大量的時間成本,而我們能做的就是儘量為他們提供通俗易懂並且功能實在的介面讓他們去呼叫。 既然各位可以看到這裡並且信得過我,那麼我也相信各位已經有了一些圖形API的呼叫基礎(再不濟你也應該拿Java的awt做過小遊戲吧)。我們知道,一般圖形程式無非就是由三個部分組成:即初始化、渲染迴圈以及釋放資源。這三個部分我們可以將它封裝在一個名為Application的類裡(這裡只是打個比方),然後用三個方法將這三個部分依次描述,再然後在main裡面進行呼叫,這是目前最基礎的做法。或許有人會說:“啊呀,你這不符合單一職責原則”。那你也可以封裝成三個類,每個類只負責一個部分。但是遊戲開發者(尤其是個人開發者)才不管你什麼亂七八糟的“七大原則、二十多種模式”的條條框框,他們只想專注於遊戲的實現,這時候如果讓他們去自己實現main函式,恐怕就沒人在再用你的引擎了(我就是這麼過來的)。所以我們得隱藏程式入口點。 廢話說了那麼多,開始幹正事。在你引擎的編輯器專案裡新建一個cpp檔案,在引擎核心裡新建兩個h檔案,為了名稱好記,暫且就叫做Engine.h(引擎標頭檔案)、EntryPoint.h(入口點標頭檔案)以及Application.cpp(編輯器原始檔)。當然,名稱只是打個比方,你可以按照你自己喜歡的名稱來,但是你得記住相應的名稱的檔案作用是什麼,畢竟我們還會在以後再用到它們,我們會在這三個檔案中分別寫入如下內容: EnntryPoint.h ``` C++ int main(int argc, char** argv) { // 這裡是引擎初始化程式碼 // 這裡是引擎渲染迴圈程式碼 // 這裡是引擎執行結束前釋放資源並且terminate的程式碼 std::cout<<"Utah Teapot"; return 0; } ``` Engine.h ```C++ #include #include "EntryPoint.h" ``` Application.cpp ```C++ #include ``` 執行結果如下:(其實只要看第一行即可,因為下面是我後來加的log系統,如果是你的專案,應該會成功輸出第一行的文字,log系統在之後的文章中我會講到) (圖2) ![執行結果](https://ae01.alicdn.com/kf/Had6c363e87594fe7946665a3a194faadz.png) 看起來貌似很簡單,對沒錯,就這幾行程式碼,我們完成了最重要的一步:入口隱藏。我們將入口隱藏在了引擎核心dll檔案裡,使得遊戲開發人員不必將過多的精力放在main的實現上。這樣看起來便有點樣子了,不是麼? ### 2.3 確定引擎核心規則 其實我也不知道該怎麼稱呼,總之就是想要將我們以後在引擎核心定義的物件、靜態方法、函式等一系列的東西呼叫在遊戲邏輯中的時候,可不止是一句簡簡單單的#include就可以解決問題,畢竟引擎核心是一串二進位制程式碼構成的dll檔案,即動態連結庫。微軟專門為動態連結庫提供了兩個語句:dllimport以及dllexport,呼叫方法如下: ```C++ // 假如說我在dll裡的某個標頭檔案裡定義了這麼一個函式,並且是對外提供呼叫的 __declspec(dllexport) void DllFunc() { // Blablablablablablabla... } // 如果我要在另一個依賴此dll的專案裡使用它,那麼我必須在這個專案裡宣告 __declspec(dllexport) void DllFunc(); ``` 但是它太麻煩了,試想,如果每一個對外開放的函式都這麼寫,冗餘的程式碼量增加不說,關鍵是在面對跨平臺構建專案時,XCode可不認你的dllimport和dllexport,畢竟蘋果的MacOS有著自己的一套SharedLib體系,這時候再去一個個改又會增加無謂的工作量。所以我們要通過某些手段來降低移植難度。 還記得在命令腳本里出現的兩句全域性巨集定義嗎:**UTOPIA_PLATFORM_WINDOWS** 以及 **UTOPIA_BUILD_DLL** 。接下來就是使用這些巨集定義的時候了,我們可以為引擎核心創造一個專門用來進行SharedLib生成指令的標頭檔案(檔名“Core.h”),程式碼如下: ```C++ #pragma once #ifdef UTOPIA_PLATFORM_WINDOWS // 平臺識別巨集 #ifdef UTOPIA_BUILD_DLL // 判斷是否是Dll內定義 #define ENGINE_API __declspec(dllexport) // 如果是,則dllexport #else #define ENGINE_API __declspec(dllimport) // 否則,dllimport #endif #else #error engine is only build on windows at now! //錯誤資訊 #endif ``` 先解釋幾個問題: 1. 我只是定義了適用於Windows平臺的巨集定義,其他平臺大家可以根據編譯器的要求來設定不同的分支。 2. 全域性巨集定義以及其他的巨集定義的名稱任君所愛,但是在定義完後請一定要記牢,以後還有用。 接下來讓它被包含在Engine.h中。那麼,在你的遊戲專案中只要引入Engine.h並且在我們的以後書寫中為類和函式定義前加上你所定義的 `ENGINE_API`,那麼在遊戲專案載入編譯時,編譯器會自動根據巨集定義去判斷究竟是使用import還是export而不用你在原始檔中進行宣告。 ## 3.結束語 其實本引擎的結構在很大程度上與油管大佬Cherno的榛子引擎相像,因為我就是看著Cherno大佬的課程一步步來的,但是YouTube的聽譯字幕質量著實不咋樣,導致我看的時候甚至一天只能看兩個教學視訊,B站也有好心的UP主搬運過來但由於是全英文的所以也勸退了不少人。我記得在虛幻引擎的官方Q&A裡面有這樣一句話:“程式碼是有版權的,但知識是無價的”。所以,如果說你的閱讀能力不錯,那可以跟著我一塊來學習,我替你把該踩的雷先踩過,這樣你也就輕鬆多了,對我來說也是在積攢開發知識的過程,何樂而不為?但是也不要抱太大希望哦,說不定哪天就斷更了,誒嘿!o( ̄▽ ̄)ブ 當然了,因為完成時間倉促以及本人技術力太過生草,肯定文中還有許許多多的問題,歡迎各位大佬們指正(如果只是想無腦噴的話,請出門左拐WB),