如何制作自己的C++游戲引擎
你想了解更多關(guān)于游戲引擎的知識(shí)、并自己來寫一個(gè)嗎?
這可是非常牛皮的一件事。為了幫助你學(xué)習(xí),這里有一些C++庫和依賴項(xiàng)的推薦,可以幫助你快速上手。
游戲開發(fā)一直是我的學(xué)生學(xué)習(xí)更高級(jí)計(jì)算機(jī)科學(xué)主題的好幫手。我的一位導(dǎo)師Sepi博士曾經(jīng)說過:
“有些人認(rèn)為游戲是孩子的東西,但游戲開發(fā)是少數(shù)幾個(gè)幾乎使用了標(biāo)準(zhǔn)CS課程所有內(nèi)容的領(lǐng)域之一?!? Sepideh Chakaveh博士
她說的完全不假!如果將隱藏在現(xiàn)代游戲開發(fā)棧下的內(nèi)容暴露出來,我們會(huì)發(fā)現(xiàn),它觸及了許多計(jì)算機(jī)科學(xué)專業(yè)學(xué)生所熟悉的概念。

算法與數(shù)據(jù)結(jié)構(gòu)
游戲程序員需要在內(nèi)存中表示游戲?qū)ο螅⒃诳紤]性能(數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)局部性)的前提下訪問對(duì)應(yīng)的數(shù)據(jù)。
數(shù)學(xué)
游戲開發(fā)需要不少應(yīng)用數(shù)學(xué)領(lǐng)域的助力。很多游戲都利用了離散數(shù)學(xué)、線性代數(shù)、微積分、概率、數(shù)值分析以及其他數(shù)學(xué)分支。
計(jì)算機(jī)體系結(jié)構(gòu)
引擎開發(fā)人員需要很好地理解目標(biāo)機(jī)器的結(jié)構(gòu),以獲得盡可能快的速度。
圖形
圖形技術(shù)對(duì)游戲開發(fā)至關(guān)重要。著色器、頂點(diǎn)、并行加速、2D&3D渲染和GPU api都是開發(fā)者使用的圖形技術(shù)的典型例子。
編譯器和形式語言
引擎通常需要把腳本語言暴露給關(guān)卡設(shè)計(jì)師,這通常意味著解釋某種高級(jí)定制語法。
人工智能
游戲?qū)ο螅〝橙撕蚽pc)通常需要表現(xiàn)出類似人類的智能。典型例子是尋路算法、追蹤、機(jī)器學(xué)習(xí)和可見性方法。
網(wǎng)絡(luò)
多人游戲和社交整合在現(xiàn)代游戲中很常見。大多數(shù)游戲必須利用好設(shè)備之間的互聯(lián),通過網(wǎng)絡(luò)協(xié)議發(fā)送/接收多個(gè)數(shù)據(jù)包。
UI和UX
UI和UX也是游戲開發(fā)技術(shù)的重要組成部分,這意味著向用戶展示信息以及提供合適的反饋。
根據(jù)你所制作的游戲的本質(zhì),你可能還需要深入到更專業(yè)的領(lǐng)域,例如分布式系統(tǒng)或人機(jī)交互。游戲開發(fā)是一項(xiàng)嚴(yán)肅的業(yè)務(wù),它也完全可以成為學(xué)習(xí)嚴(yán)肅的CS概念的強(qiáng)大工具。
本文將介紹使用C++創(chuàng)建簡單游戲引擎所需的一些基本構(gòu)建模塊。我將解釋游戲引擎中所需要的主要元素,并就如何從頭開始編寫游戲引擎給出一些個(gè)人建議。
話雖如此,這并非一個(gè)編程教程。我不會(huì)介紹太多的技術(shù)細(xì)節(jié),也不會(huì)解釋如何用代碼將所有這些元素粘合在一起。如果你正在尋找一本關(guān)于編寫C++游戲引擎的綜合教程,這是一個(gè)很好的起點(diǎn):

什么是游戲引擎?
如果你正在閱讀這篇文章,那么你可能已經(jīng)很清楚什么是游戲引擎,甚至可能自己也用過。但為了達(dá)成共識(shí),我們來快速回顧一下什么是游戲引擎,以及它們能幫助我們實(shí)現(xiàn)什么。
游戲引擎是一組優(yōu)化游戲開發(fā)的軟件工具。它們可以是小型且極簡的,只提供一個(gè)游戲循環(huán)和一些渲染功能,也可以是大型且綜合性的,類似于IDE類應(yīng)用,在當(dāng)中開發(fā)者可以編寫腳本,調(diào)試,定制關(guān)卡邏輯,AI,設(shè)計(jì),發(fā)布,協(xié)作,并最終從頭到尾構(gòu)建游戲,整個(gè)過程不需要離開引擎。
游戲引擎和游戲框架通常會(huì)將API暴露給用戶。API允許程序員調(diào)用引擎函數(shù)執(zhí)行困難的任務(wù),就像它們是黑盒一樣。
為了真正理解這樣的API是如何工作的,先來點(diǎn)鋪墊。例如,游戲引擎API把名為“IsColliding()”的函數(shù)暴露出來并不罕見,開發(fā)者可以調(diào)用該函數(shù)來檢查兩個(gè)游戲?qū)ο笫欠癜l(fā)生碰撞。程序員不需要知道這個(gè)函數(shù)是如何實(shí)現(xiàn)的,也不需要知道正確確定兩個(gè)形狀是否重疊所需的算法是什么。就我們所關(guān)心的而言,IsColliding函數(shù)就是一個(gè)黑盒,它做了一些神奇的事情,并在這些對(duì)象可能相互碰撞時(shí)正確地返回true或false。這是大多數(shù)游戲引擎都會(huì)向用戶暴露的功能。

除了編程API,游戲引擎的另一個(gè)重要職責(zé)是硬件抽象。例如,3D引擎通常構(gòu)建在專用的圖形API(例如OpenGL、Vulkan或Direct3D)之上。這些API為圖形處理單元(GPU)提供了抽象。
說到硬件抽象,還有一些低級(jí)庫(如DirectX、OpenAL和SDL)提供了對(duì)許多其他硬件元素的抽象和多平臺(tái)訪問。這些庫幫我們?cè)L問和處理鍵盤事件、鼠標(biāo)移動(dòng)、網(wǎng)絡(luò)連接,甚至音頻。
游戲引擎的崛起
在游戲行業(yè)的早期,游戲是用自定義的渲染引擎構(gòu)建的,代碼的開發(fā)是為了從較慢的機(jī)器上榨取盡可能多的性能。每個(gè)CPU時(shí)鐘周期都是至關(guān)重要的,因此代碼重用或適用于多種場景的通用函數(shù)并非開發(fā)者可以負(fù)擔(dān)得起的奢侈品。
而隨著游戲、開發(fā)團(tuán)隊(duì)的規(guī)模和復(fù)雜性的增長,大多數(shù)工作室最終都會(huì)在游戲之間重用功能和子程序。工作室開發(fā)的內(nèi)部引擎基本上是處理低級(jí)任務(wù)的內(nèi)部文件和庫的集合。這些功能讓開發(fā)團(tuán)隊(duì)的其他成員能夠?qū)W⒂谟螒蛲娣?、地圖創(chuàng)建和關(guān)卡定制等高級(jí)細(xì)節(jié)。
一些流行的經(jīng)典引擎包括id Tech、Build和AGI。創(chuàng)建這些引擎是為了幫助特定游戲的開發(fā),它們讓團(tuán)隊(duì)其他成員能快速開發(fā)新關(guān)卡,添加自定義資產(chǎn),以及動(dòng)態(tài)自定義地圖。這些自定義引擎也用于mod或?yàn)樵及姹緞?chuàng)建擴(kuò)展包。
Id Tech由Id Software開發(fā)。Id Tech是不同引擎的集合,每次迭代都與不同的游戲相關(guān)聯(lián)。我們經(jīng)常聽到開發(fā)者將id Tech 0描述為“德軍總部3D引擎”,將id Tech 1描述為“毀滅戰(zhàn)士引擎”,id Tech 2描述為“Quake引擎”。
Build是另一個(gè)塑造90年代游戲歷史的引擎例子。它由Ken Silverman創(chuàng)造,用于幫助定制第一人稱射擊游戲。與id Tech類似,Build也隨著時(shí)間的推移而發(fā)展,其不同版本幫助程序員開發(fā)了《毀滅公爵3D》、《影子武士》和《Blood》等游戲。它們可以說是用Build引擎創(chuàng)建的最受歡迎的游戲,通常被稱為“三巨頭”。

90年代的另一個(gè)游戲引擎例子是“Manic Mansion的腳本創(chuàng)建工具”(SCUMM)。SCUMM是LucasArts開發(fā)的引擎,它是許多經(jīng)典的點(diǎn)擊類游戲(如《猴島小英雄》和《極速天龍》)的基礎(chǔ)。

隨著機(jī)器進(jìn)化得越來越強(qiáng)大,游戲引擎也隨之發(fā)展?,F(xiàn)代引擎裝滿了功能豐富的工具,這些工具需要更快的處理器速度、巨量的內(nèi)存和專用顯卡。
有了多余的動(dòng)力,現(xiàn)代引擎用時(shí)鐘周期換取了更多的抽象。這種權(quán)衡意味著我們可以將現(xiàn)代游戲引擎視為以低成本和短開發(fā)周期創(chuàng)作復(fù)雜游戲的通用工具。
問題來了,我們?yōu)槭裁匆谱饔螒蛞婺兀?/p>
這是一個(gè)非常常見的問題,不同的游戲程序員會(huì)根據(jù)所開發(fā)游戲的性質(zhì)、業(yè)務(wù)需求和其他驅(qū)動(dòng)力,對(duì)這個(gè)議題有著自己的看法。
開發(fā)者可以使用很多免費(fèi)、強(qiáng)大且專業(yè)的商業(yè)引擎來創(chuàng)作和部署自己的游戲。既然有這么多游戲引擎可供選擇,為什么還會(huì)有人不厭其煩地從頭開始制作游戲引擎呢?
我在之前的一篇博文里,解釋了程序員決定從頭開始制作游戲引擎的一些可能原因。在我看來,最主要的原因是:
學(xué)習(xí)機(jī)會(huì):對(duì)游戲引擎工作原理的底層理解可以幫助你成長為一名開發(fā)者。
工作流控制:你可以更好地控制你自己游戲的特殊要素,并根據(jù)自己的工作流需求調(diào)整解決方案。
自定義:你將能夠?yàn)楠?dú)特的游戲需求量身定制解決方案。
極簡化:較小的代碼庫可以減少大型游戲引擎帶來的開銷。
創(chuàng)新:你可能需要實(shí)現(xiàn)一些全新的東西,或者瞄準(zhǔn)其他引擎不支持的非傳統(tǒng)硬件。
在接下來的討論中,我將假設(shè)你對(duì)游戲引擎在教育學(xué)習(xí)層面的吸引力感興趣。從頭開始創(chuàng)造一個(gè)小型游戲引擎是我向所有CS學(xué)生強(qiáng)烈推薦的內(nèi)容。
如何制作游戲引擎
所以,在快速討論了使用和開發(fā)游戲引擎的動(dòng)機(jī)之后,我們繼續(xù)來討論游戲引擎的一些組件,并學(xué)習(xí)如何自己編寫一個(gè)游戲引擎。
1. 選擇編程語言
我們面臨的第一個(gè)抉擇,是挑選用于開發(fā)核心引擎代碼的編程語言。我見過用原始匯編、C、C++以及高級(jí)語言(如c#、Java、Lua,甚至JavaScript)來開發(fā)引擎!
編寫游戲引擎最流行的語言之一是C++。C++編程語言將速度與運(yùn)用OOP及其他編程范式的能力相結(jié)合。這些編程范式能幫助開發(fā)者組織和設(shè)計(jì)大型軟件項(xiàng)目。
當(dāng)我們開發(fā)游戲時(shí),性能通常是非常重要的,C++具有編譯語言的優(yōu)勢。編譯語言意味著最終的可執(zhí)行文件將在目標(biāo)機(jī)器的處理器上原生運(yùn)行。還有許多專門的C++庫和開發(fā)工具包,適用于大多數(shù)現(xiàn)代主機(jī),如PlayStation或Xbox。

說到性能,我個(gè)人不推薦使用虛擬機(jī)、字節(jié)碼或任何其他中間層的語言。除了C++,一些適合編寫核心游戲引擎代碼的現(xiàn)代替代方法是Rust、Odin和Zig。
在本文的剩余部分,我的建議將假設(shè)讀者希望使用C++編程語言構(gòu)建一個(gè)簡單的游戲引擎。
2. 硬件訪問
在老式的操作系統(tǒng)(如MS-DOS)中,我們通??梢灾苯佣ㄎ坏絻?nèi)存地址并訪問映射到不同硬件組件的特殊位置。例如,我要做的就是用表示VGA調(diào)色板正確顏色的數(shù)字來加載一個(gè)特殊的內(nèi)存地址,然后顯示驅(qū)動(dòng)程序?qū)⒏臑槲锢硐袼氐膬?nèi)容轉(zhuǎn)換到CRT監(jiān)視器中。
隨著操作系統(tǒng)的進(jìn)化,它們開始負(fù)責(zé)保護(hù)硬件不受程序員的侵害?,F(xiàn)代操作系統(tǒng)不允許代碼修改操作系統(tǒng)允許給到進(jìn)程的地址之外的內(nèi)存位置。
例如,如果你使用的是Windows、macOS、Linux或BSD,則需要向操作系統(tǒng)請(qǐng)求正確的權(quán)限,以便在屏幕上繪制像素或與任何其他硬件組件對(duì)話。即使是形如“在操作系統(tǒng)桌面上打開一個(gè)窗口”這樣的簡單任務(wù),也必須通過操作系統(tǒng)API來執(zhí)行。
因此,運(yùn)行進(jìn)程、打開窗口、在屏幕上呈現(xiàn)圖形、在窗口內(nèi)繪制像素,甚至從鍵盤讀取輸入事件都是特定于操作系統(tǒng)的任務(wù)。
SDL是一個(gè)非常流行的庫,可以幫忙實(shí)現(xiàn)多平臺(tái)硬件抽象。我個(gè)人喜歡在教授游戲開發(fā)課程時(shí)使用SDL,因?yàn)橛肧DL,我不需要為Windows學(xué)生創(chuàng)建一個(gè)版本的代碼,為macOS學(xué)生創(chuàng)建一個(gè)版本的代碼,又為Linux學(xué)生創(chuàng)建另一個(gè)版本的代碼。SDL不僅是不同操作系統(tǒng)之間的橋梁,也是不同CPU架構(gòu)(Intel、ARM、Apple M1等)之間的橋梁。SDL庫抽象了底層硬件訪問,并“翻譯”了我們的代碼,以在這些不同的平臺(tái)上正確工作。
下面是“用SDL在操作系統(tǒng)上打開一個(gè)窗口”的一小段代碼。下面的代碼對(duì)于Windows、macOS、Linux、BSD甚至RaspberryPi都是一樣的。
但SDL只是我們可以用來實(shí)現(xiàn)這種多平臺(tái)硬件訪問的庫的眾多例子之一。對(duì)于2D游戲和將現(xiàn)有代碼移植到不同平臺(tái),SDL是一個(gè)流行的選擇。另一個(gè)流行的多平臺(tái)庫選項(xiàng)是GLFW,主要用于3D游戲和3D引擎。GLFW庫與加速3D api(如OpenGL和Vulkan)之間的通信非常好。
3.游戲循環(huán)
打開操作系統(tǒng)窗口后,我們需要?jiǎng)?chuàng)建一個(gè)可控制的游戲循環(huán)。
簡而言之,我們通常希望游戲以每秒60幀的速度運(yùn)行。幀速率可能因游戲而異,但從整體來看,電影膠片拍攝的幀速率為24 FPS(每秒鐘閃過24張圖像)。
游戲循環(huán)在gameplay中持續(xù)運(yùn)行,在每次循環(huán)中,我們的引擎都需要跑一些重要的任務(wù)。傳統(tǒng)的游戲循環(huán)必須:
處理輸入事件,不阻塞
更新當(dāng)前幀的所有游戲?qū)ο蠹捌鋵傩?/p>
在屏幕上渲染出所有游戲?qū)ο蠛推渌匾畔?/p>
這是一個(gè)很袖珍的while循環(huán)。完事兒了嗎?明顯沒有。
原始的C++循環(huán)對(duì)我們來說還不夠好。游戲循環(huán)必須與現(xiàn)實(shí)世界的時(shí)間有某種關(guān)系。畢竟游戲中的敵人在任何機(jī)器上都應(yīng)該以相同的速度移動(dòng),不管它們的CPU時(shí)鐘速度怎樣。
控制幀率并將其設(shè)置為固定FPS,實(shí)際上是一個(gè)非常有趣的問題。這通常需要我們跟蹤幀與幀之間的時(shí)間,并進(jìn)行一些合理的計(jì)算,以確保我們的游戲在至少30幀/秒的幀速率下平穩(wěn)運(yùn)行。
4. 輸入
我無法想象一款不用讀取用戶輸入事件的游戲。這些輸入可以來自鍵盤、鼠標(biāo)、手柄或VR設(shè)備。因此,我們必須在游戲循環(huán)中處理不同的輸入事件。
為了處理用戶輸入,我們必須請(qǐng)求訪問硬件事件,而這必須通過操作系統(tǒng)API來執(zhí)行。好消息是,我們可以使用多平臺(tái)硬件抽象庫(SDL、GLFW、SFML等)來為我們處理用戶輸入。
如果我們用了SDL,則可以輪詢事件,并通過幾行代碼進(jìn)行相應(yīng)處理。
同樣,如果使用形如SDL這樣的跨平臺(tái)庫來處理輸入,我們不必太擔(dān)心針對(duì)特定操作系統(tǒng)的實(shí)現(xiàn)。不管目標(biāo)平臺(tái)是什么,我們的C++代碼都應(yīng)該是一樣的。
在擁有一個(gè)有效的游戲循環(huán)和處理用戶輸入的方法后,我們就該開始考慮如何在內(nèi)存中組織游戲?qū)ο罅恕?/p>
5. 在內(nèi)存中表示游戲?qū)ο?/strong>
在設(shè)計(jì)游戲引擎時(shí),需要設(shè)置數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ)和訪問咱們的游戲?qū)ο蟆?/p>
程序員在構(gòu)建游戲引擎時(shí)可以使用多種技術(shù)。一些引擎可能會(huì)使用簡單的面向?qū)ο蟮姆椒?,包括類和繼承,而另一些引擎則可能將其對(duì)象組織為實(shí)體和組件。
如果你的目標(biāo)之一是學(xué)習(xí)更多關(guān)于算法和數(shù)據(jù)結(jié)構(gòu)的知識(shí),我建議你嘗試自己實(shí)現(xiàn)這些數(shù)據(jù)結(jié)構(gòu)。如果你使用C++,一種選擇是使用STL(標(biāo)準(zhǔn)模板庫),充分利用它自帶的許多數(shù)據(jù)結(jié)構(gòu)(向量、列表、隊(duì)列、堆棧、map、set等)。C++ STL在很大程度上依賴于模板,所以這是一個(gè)練習(xí)使用模板并在實(shí)際項(xiàng)目中接觸它們的好機(jī)會(huì)。
而當(dāng)你開始閱讀更多關(guān)于游戲引擎架構(gòu)的內(nèi)容時(shí),你便會(huì)發(fā)現(xiàn),游戲中最受歡迎的設(shè)計(jì)模式是基于實(shí)體和組件。實(shí)體組件設(shè)計(jì)將游戲場景的對(duì)象組織為實(shí)體(Unity稱之為“游戲?qū)ο蟆?,而虛幻稱之為“actor”)和組件(我們能添加或附到實(shí)體上的數(shù)據(jù))。
要理解實(shí)體和組件如何協(xié)同工作,請(qǐng)考慮一個(gè)簡單的游戲場景。實(shí)體將會(huì)是主要玩家、敵人、地板、拋射物,而組件將是我們“附加”到實(shí)體上的重要數(shù)據(jù)塊,如位置、速度、剛體碰撞器等等。

下面列出可以選擇附加到實(shí)體上的組件的部分例子:
位置組件:跟蹤實(shí)體在游戲世界中的x-y位置坐標(biāo)(或3D中的x-y-z)。
速度組件:跟蹤實(shí)體在x-y軸(或3D中的x-y-z軸)中的移動(dòng)速度。
精靈組件:它通常存儲(chǔ)我們應(yīng)該為某個(gè)實(shí)體呈現(xiàn)的PNG圖像。
動(dòng)畫組件:跟蹤實(shí)體的動(dòng)畫速度,以及動(dòng)畫幀如何隨時(shí)間變化。
碰撞器組件:這通常與剛體的物理特性有關(guān),并定義了實(shí)體的碰撞形狀(邊界框、邊界圓、網(wǎng)格碰撞器等)。
健康組件:存儲(chǔ)實(shí)體的當(dāng)前健康值。這通常只是一個(gè)數(shù)字,在某些情況下是一個(gè)百分比值(例如一個(gè)血條)。
腳本組件:有時(shí)我們可以將腳本組件附加到實(shí)體,這可能是引擎必須在幕后解釋和執(zhí)行的外部腳本文件(Lua, Python等)。
這是表示游戲?qū)ο蠛椭匾螒驍?shù)據(jù)的一種非常流行的方式。先是實(shí)體,然后會(huì)將不同的組件“插入”到實(shí)體中。
許多書籍和文章都探討了我們應(yīng)該如何實(shí)現(xiàn)實(shí)體組件設(shè)計(jì),以及在這樣的實(shí)現(xiàn)中應(yīng)該使用什么數(shù)據(jù)結(jié)構(gòu)。我們用到的數(shù)據(jù)結(jié)構(gòu)以及訪問它們的方式直接影響著游戲性能。你可能經(jīng)常聽到開發(fā)者提到諸如“面向數(shù)據(jù)的設(shè)計(jì)”,“實(shí)體-組件-系統(tǒng)”(ECS),“數(shù)據(jù)本地性”和其他許多與游戲數(shù)據(jù)如何存儲(chǔ)在內(nèi)存中、以及如何有效訪問這些數(shù)據(jù)有關(guān)的idea。
表示和訪問內(nèi)存中的游戲?qū)ο罂梢允且粋€(gè)復(fù)雜的主題。在我看來,你可以手動(dòng)編寫一個(gè)簡單的實(shí)體組件實(shí)現(xiàn),也可以簡單使用現(xiàn)有的第三方ECS庫。
我們可以在C++項(xiàng)目中包含一些流行的現(xiàn)成ECS庫,并開始創(chuàng)建實(shí)體和附加組件,而不必?fù)?dān)心它們?cè)诘讓邮侨绾螌?shí)現(xiàn)的。形如EnTT和Flecs都是C++ ECS庫的一些典型例子。
我個(gè)人建議那些認(rèn)真對(duì)待編程的學(xué)生至少嘗試一次手動(dòng)實(shí)現(xiàn)簡易版ECS。即使你的實(shí)現(xiàn)并不完美,從頭開始編寫ECS系統(tǒng)也會(huì)迫使你考慮底層數(shù)據(jù)結(jié)構(gòu)以及其性能。
現(xiàn)在,嚴(yán)肅來講,一旦你完成了自定義ECS實(shí)現(xiàn),我會(huì)鼓勵(lì)你使用一些流行的第三方ECS庫(EnTT、fecs等)。這些是經(jīng)過業(yè)界多年開發(fā)和測試的專業(yè)庫。它們可能比我們自己從零開始做的任何東西都要好得多。
總之,一個(gè)專業(yè)的ECS很難從頭開始實(shí)現(xiàn)。這是一個(gè)有效的學(xué)術(shù)練習(xí),但一旦你完成了你的小型學(xué)習(xí)項(xiàng)目,那么不用多想,選擇一個(gè)經(jīng)過良好測試的第三方ECS庫,并將其添加到你的游戲引擎代碼中。
6. 渲染
好吧,看起來我們的游戲引擎的復(fù)雜性正在緩慢增長。既然已經(jīng)討論了在內(nèi)存中存儲(chǔ)和訪問游戲?qū)ο蟮姆椒?,我們可能還需要討論如何在屏幕上渲染對(duì)象。
第一步是考慮我們將用引擎創(chuàng)造的游戲的性質(zhì)。我們是否創(chuàng)造了一個(gè)只用于開發(fā)2D游戲的游戲引擎?如果是這種情況,我們就需要考慮渲染精靈、紋理、管理圖層,可能還需要利用顯卡加速。好消息是,2D游戲通常比3D游戲簡單,2D數(shù)學(xué)也比3D數(shù)學(xué)簡單得多。

如果你的目標(biāo)是開發(fā)一個(gè)2D引擎,你可以使用SDL來幫助多平臺(tái)渲染。SDL抽象了加速GPU硬件,可以解碼和顯示PNG圖像,繪制精靈,并在游戲窗口內(nèi)渲染紋理。
而如果你的目標(biāo)是開發(fā)一個(gè)3D引擎,那么我們需要定義如何向GPU發(fā)送一些額外的3D信息(頂點(diǎn)、紋理、shader等)。你可能想使用軟件抽象圖形硬件,最流行的選項(xiàng)是OpenGL、Direct3D、Vulkan和Metal。使用哪個(gè)API可能取決于你的目標(biāo)平臺(tái)。例如,Direct3D支持微軟的應(yīng)用程序,而Metal將只與蘋果產(chǎn)品兼容。
3D應(yīng)用程序通過圖形管線處理3D數(shù)據(jù)。該管線將指示你的引擎必須如何向GPU發(fā)送圖形信息(頂點(diǎn)、紋理坐標(biāo)、法線等)。圖形API和管線還將指示我們應(yīng)該如何編寫可編程shader來變換和修改3D場景的頂點(diǎn)和像素。

可編程shader指示GPU應(yīng)該如何處理和顯示3D對(duì)象。我們可以為每個(gè)頂點(diǎn)和每個(gè)像素(片段)運(yùn)用不同的腳本,它們可以控制反射、平滑度、顏色、透明度等等。
說到3D對(duì)象和頂點(diǎn),把讀取和解碼不同網(wǎng)格格式的任務(wù)委托給庫是個(gè)好主意。有許多流行的3D模型格式,大多數(shù)第三方3D引擎應(yīng)該都知道。文件的一些例子是. obj、Collada、FBX和DAE。我建議從. obj文件開始。有一些經(jīng)過良好測試和支持的庫可以用C++處理OBJ加載。TinyOBJLoader和AssImp是很多游戲引擎都用的一個(gè)很棒的選擇。
7. 物理
當(dāng)我們向引擎添加實(shí)體時(shí),我們可能還希望它們?cè)趫鼍爸幸苿?dòng)、旋轉(zhuǎn)和彈跳。這個(gè)游戲引擎的子系統(tǒng)就是物理模擬。它既可以手動(dòng)創(chuàng)建,也可以從現(xiàn)有的現(xiàn)成物理引擎導(dǎo)入。
在這里,我們還需要考慮我們想要模擬的物理類型。2D物理通常比3D簡單,但物理模擬的底層部分2D和3D引擎是非常相似的。
如果你只是想在你的項(xiàng)目中包含一個(gè)物理庫,有幾個(gè)很好的選擇。
對(duì)于2D物理,我建議看看Box2D和Chipmunk2D。對(duì)于專業(yè)和穩(wěn)定的3D物理模擬,則可以瞅瞅像是PhysX和Bullet這樣的庫。如果物理穩(wěn)定性和開發(fā)速度對(duì)你的項(xiàng)目至關(guān)重要,那么使用第三方物理引擎總是不錯(cuò)的選擇。

作為一名教育者,我堅(jiān)信,每個(gè)程序員都應(yīng)該在職業(yè)生涯中至少學(xué)習(xí)一次如何編寫簡單的物理引擎。同樣地,你不需要編寫一個(gè)完美的物理模擬,但要專注于確保物體能夠正確加速,并確保不同類型的力可以應(yīng)用到你的游戲物體上。一旦移動(dòng)搞定了,你還可以考慮實(shí)現(xiàn)一些簡單的碰撞檢測和碰撞解決方案。
如果你想了解更多關(guān)于物理引擎的知識(shí),可以把一些好書和在線資源充分利用起來。對(duì)于2D剛體物理,你可以查看Box2D源代碼和Erin Catto的PPT。但如果你正在尋找一門關(guān)于游戲物理的綜合課程,《2D game physics from Scratch》可能是一個(gè)不錯(cuò)的開始。
如果你想學(xué)習(xí)3D物理以及如何實(shí)現(xiàn)一個(gè)強(qiáng)大的物理模擬,另一個(gè)很好的資源是David Eberly的《Game physics》一書。

8. UI
一提到Unity或虛幻等現(xiàn)代游戲引擎,我們總會(huì)想到帶有各種面板、滑塊、拖放選項(xiàng)及其他幫用戶定制游戲場景的漂亮界面元素的復(fù)雜UI。UI能讓開發(fā)者添加和刪除實(shí)體,動(dòng)態(tài)更改組件值,并輕松修改游戲變量。
需要明確的是,我們談?wù)摰氖怯糜诠ぞ叩挠螒蛞鎁I,而不是向玩家展示的用戶界面(如對(duì)話屏幕和菜單)。
請(qǐng)記住,游戲引擎不一定要嵌入編輯器,但由于游戲引擎通常用于提高工作效率,友好的UI會(huì)幫你和其他團(tuán)隊(duì)成員快速定制關(guān)卡及游戲場景的其他內(nèi)容。
從頭開發(fā)UI框架可能是新手程序員嘗試添加到游戲引擎中的最煩人的任務(wù)之一。你必須創(chuàng)建按鈕、面板、對(duì)話框、滑塊、單選按鈕、管理顏色,還需要正確處理該UI的事件并始終保持其狀態(tài)。一點(diǎn)都不好玩。在引擎中添加UI工具還將會(huì)增加應(yīng)用程序的復(fù)雜性,并為源代碼添加大量的噪聲。
如果你的目標(biāo)是為你的引擎創(chuàng)建UI工具,我的建議是使用現(xiàn)有的第三方UI庫。在Google上搜一把會(huì)立刻出現(xiàn)最受歡迎的一些選擇,例如Dear ImGui, Qt和Nuklear。

Dear ImGui是我最喜歡的工具之一,因?yàn)樗屛覀兡転橐婀ぞ呖焖僭O(shè)置用戶界面。ImGui項(xiàng)目使用一種被稱為“即時(shí)模式UI”的設(shè)計(jì)模式,它被廣泛用于游戲引擎,因?yàn)樗眉铀貵PU渲染能與3D應(yīng)用程序進(jìn)行良好的通信。
總之,如果你想在游戲引擎中添加UI工具,我的建議是使用Dear ImGui。
9. 腳本
隨著游戲引擎的發(fā)展,一個(gè)很流行的選擇是使用簡單的腳本語言進(jìn)行關(guān)卡定制。
這個(gè)想法理念很簡單:我們將腳本語言嵌入到原生C++應(yīng)用中,非專業(yè)程序員可以用這種更簡單的腳本語言編寫實(shí)體行為、AI邏輯、動(dòng)畫和游戲的其他重要元素。
一些流行的游戲腳本語言是Lua, Wren, C#,Python和JavaScript。所有這些語言的操作級(jí)別都比我們的原生C++代碼高得多。無論誰使用腳本語言編寫游戲行為,都不需要擔(dān)心內(nèi)存管理或核心引擎如何工作的其他底層細(xì)節(jié)。他們所需要做的就是編寫關(guān)卡腳本,咱們的引擎知道如何解釋腳本并在幕后執(zhí)行困難的任務(wù)。

我最喜歡的腳本語言是Lua。Lua體積小、速度快,并且非常容易與C和C++原生代碼集成。此外,如果我使用Lua和“現(xiàn)代”C++,我喜歡用一個(gè)名為Sol的wrapper庫。Sol庫幫我地道地運(yùn)用Lua,并提供許多幫助函數(shù)來改進(jìn)傳統(tǒng)的Lua C API。
啟用腳本后,我們就可以開始在游戲引擎中討論更高級(jí)的主題。腳本幫我們定義AI邏輯,定制動(dòng)畫框架和移動(dòng),以及其他不需要存在于原生C++代碼中、可以通過外部腳本輕松管理的游戲行為。
10. 音頻
另一個(gè)可以考慮為游戲引擎添加的支撐元素是音頻。
毫無疑問,若想要插入音頻值并發(fā)出聲音,我們需要通過操作系統(tǒng)訪問音頻設(shè)備。同樣地(二回目),由于我們通常不會(huì)想編寫針對(duì)特定操作系統(tǒng)的代碼,所以我建議用一個(gè)多平臺(tái)庫來抽象音頻硬件訪問。
像SDL這樣的多平臺(tái)庫有各種擴(kuò)展組件,能幫你的引擎處理音樂和音效等內(nèi)容。
但是現(xiàn)在,嚴(yán)肅來講(二回目),我強(qiáng)烈建議在你的引擎的其他部分已經(jīng)協(xié)同工作之后再處理音頻。讓聲音文件響起來可能很容易實(shí)現(xiàn),但一旦我們開始處理音頻同步,將音頻與動(dòng)畫、事件和其他游戲元素鏈接起來,事情就會(huì)變得混亂。
如果你非常務(wù)實(shí)地全手動(dòng)處理,由于多線程管理的緣故,音頻部分可能會(huì)很棘手。它并非不能實(shí)現(xiàn),但如果你的目標(biāo)是編寫一個(gè)簡單的游戲引擎,那么這部分內(nèi)容是最值得委托給專業(yè)庫的。
你可以考慮將SDL_Mixer、SoLoud和FMOD等優(yōu)秀的音頻庫和工具整合到游戲引擎中。

11. 人工智能
我要討論的最后一個(gè)子系統(tǒng)是AI??梢酝ㄟ^腳本實(shí)現(xiàn)AI,這意味著我們能將AI邏輯委托給關(guān)卡設(shè)計(jì)師編寫腳本。另一種選擇便是將適當(dāng)?shù)腁I系統(tǒng)嵌入到我們的游戲引擎核心原生代碼中。
在游戲中,AI是用來產(chǎn)生對(duì)游戲?qū)ο蟮捻憫?yīng)性、適應(yīng)性或智能行為。大多數(shù)AI邏輯被添加到非玩家角色(npc,敵人)中,以模擬類似人類的智能。
敵人是AI在游戲中應(yīng)用的一個(gè)常見例子。當(dāng)敵人在地圖上追逐目標(biāo)時(shí),游戲引擎可以通過尋路算法或有趣的類人行為來創(chuàng)建抽象。
Ian Millington的《AI for games》是一本關(guān)于游戲人工智能理論和執(zhí)行的綜合性書籍。

不要試圖同時(shí)做所有的事情
好了,我們前開你討論了一些重要的概念,你可以考慮將它們添加到你的簡單C++游戲引擎中。但在開始把這些東西粘在一起之前,我想提一點(diǎn)非常重要的事情。
開發(fā)游戲引擎最困難的部分之一是,大多數(shù)開發(fā)者不會(huì)設(shè)定明確的邊界,也沒有“終點(diǎn)線”的概念。換句話說,程序員會(huì)開始一個(gè)游戲引擎項(xiàng)目,渲染對(duì)象,添加實(shí)體,添加組件,然后便走入不歸路了。如果沒有定義邊界,很容易添加越來越多的功能,而失去對(duì)大局的掌控。如果出現(xiàn)這種情況,游戲引擎很有可能永遠(yuǎn)都見不到天日。
除了缺乏邊界之外,當(dāng)我們看到代碼以迅雷不及掩耳盜鈴之勢在眼前增長時(shí),我們很容易不知所措。游戲引擎項(xiàng)目的復(fù)雜性可能會(huì)在幾周內(nèi)迅速增長,你的C++項(xiàng)目可能會(huì)有多個(gè)依賴項(xiàng),需要一個(gè)復(fù)雜的構(gòu)建系統(tǒng),隨著引擎添加更多功能,你的代碼的整體可讀性也會(huì)下降。
我的最重要建議之一是,在編寫一款實(shí)際游戲的同時(shí)編寫游戲引擎。在開始和完成引擎的第一次迭代時(shí),腦中要有一款真正的游戲。這將幫助你設(shè)定限制,為你需要完成的事情定義一條清晰的道路。盡你最大的努力堅(jiān)持下去,不要在中途改變需求。
慢慢來,專注于基礎(chǔ)
如果你正在創(chuàng)建自己的游戲引擎作為學(xué)習(xí)練習(xí),那就盡情享受這樣的小小勝利吧!
大多數(shù)學(xué)生在項(xiàng)目開始時(shí)都非常興奮,隨著時(shí)間的推移,焦慮開始浮現(xiàn)。如果我們從頭開始創(chuàng)造游戲引擎,特別是使用像C++這樣復(fù)雜的語言時(shí),我們很容易就會(huì)不知所措并失去動(dòng)力。
我想鼓勵(lì)你戰(zhàn)勝那種“與時(shí)間賽跑”的感覺。深呼吸,享受每一個(gè)微小的勝利。例如,當(dāng)你學(xué)會(huì)如何成功在屏幕上顯示PNG紋理時(shí),享受這一刻,并確保你明白你做了什么。如果你成功地檢測到了兩個(gè)物體之間的碰撞,那就再次享受這一刻,并反思你剛剛獲得的知識(shí)。
專注于基礎(chǔ)并確保切實(shí)擁有這些知識(shí)。不管一個(gè)概念多小、多簡單,都要切實(shí)擁有它??!其他一切都是浮云。
(本文由皮皮關(guān)翻譯編輯,原文鏈接:https://www.gamedeveloper.com/blogs/how-to-make-your-own-c-game-engine )
歡迎加入游戲開發(fā)群歡樂攪基:1082025059
對(duì)學(xué)習(xí)游戲開發(fā)、游戲制作感興趣的童鞋,歡迎訪問咱們的主頁:http://levelpp.com/