[UnrealEngine] Plugin製作相關注意事項與心得分享
前言 寫出一套好用好維護的程式碼相信會是所有程式設計師無意識中追求的目標,而把自己的功能包裝成Plugin是一個非常直接達成這個目的的手段,因為設計的過程會強迫把自己置更高、更抽象的位置往下俯瞰整個遊戲系統並定位出自己的功能應該在哪個位置。 只是,並不是任何的程式碼都適合放進Plugin中。因此接下來我的目的主要是想根據自己過去的經驗,分享一些自己在寫各種Plugin時的大原則。雖然這些想法不一定適用於所有的情境,應該也會有許多我沒考慮到的狀況,但基本上也就只是目前在我有限的經驗中現階段的想法。如果有人有不一樣的觀點也歡迎一起討論,或許可以碰撞出一些東西也說不定。 順帶一提,這邊的Plugin討論的主要是CodePlugin,其他類型的Plugin,例如Asset Only或BP Only,並不在接下來的討論範圍內,除非我有另外指明。 目前我在Unreal Marketplace已經有上架六項產品,內容包括Quest、Interact、Dialogue以及Tween,有興趣的可以參考: https://www.unrealengine.com/marketplace/en-US/profile/horizon-studio 另外我也會不定期的在我的FB頁面上分享一些開發日誌,通常是跟遊戲技術相關的話題,有興趣的歡迎追蹤:https://www.facebook.com/dorgonresearches 本文開始 Unreal中的程式碼我們可以非常粗爆的大致分層如下:EngineModule->EnginePlugin->GamePlugin->GameModule->GameFeaturePlugin。其中EngineModule、EnginePlugin我們頂多修修bug不可能做太大的修改;GameModule則是看專案團隊的風格會有不同的構成方式,我們等一下再回來聊這部份;而GamePlugin跟GameFeaturePlugin則是我們接下來要討論的重點。 EnginePlugin 、GamePlugin相對直觀,大家應該不會有什麼疑惑,但GameFeaturePlugin這個概念或許就有人沒聽過了。這個功能是5.0所引入的最大變革之一,對程式相關職能的人而言,特別是寫Gameplay相關的,我覺得其重要性甚至更勝於Nanite跟Lumen,因為相關機制會從根本性上的改變了我們程式碼的架構方式以及思考模式。 就設計理念看來,他是屬於DLC概念的實現,跟Patch不同,Patch需要蓋掉原本的內容,而DLC必須要做到「熱插拔」的機制。 就傳統UE4 project,我們要達到DLC的這項要求非常麻煩,因為我們只能對「Content-Only」plugin包出dlc包,而哪些dlc需要開啟或關閉則需要你在BaseGame中做出一些可以管理的設計。在GameFeature Plugin 機制出現後,我們總算不需要做這些勞力活了。注:這邊的BaseGame指的是 ${PROJECT_ROOT}/Source 下面所有的Runtime GameModule以及${PROJECT_ROOT}/Content下面所有會打包進去的Asset。 有些人會困惑於我們建出的 GameFeature Plugin 沒辦法跨專案共用,但這個理解並沒有錯,基本上GameFeature Plugin需要依賴於BaseGame而存在,因此我們也可以理解成他是專屬於project的Plugin。若你的功能想要跨專案共用,你要寫的是一般的GamePlugin而不是GameFeature。 在理解了GamePlugin跟GameFeaturePlugin的不同點之後,讓我們先來思考一下,Plugin到底是什麼?就字面上來看的話,形象一點解釋,他就像是一個插頭,把這個插頭插進我們的專案之後就能夠提供各種我們遊戲專案會需要的功能。雖然Plugin不像DLC需要做到「熱插拔」,但大家期望的是功能的插入跟移除是相對簡單的。因此每個GamePlugin在設計上要能做到獨立運作,也就是說在設計上只能依賴於Engine或EnginePlugin,依賴於其他同層級的GamePlugin會明顯造成遊戲專案的使用障礙。移除簡不簡單這件事取決定Plugin的功能與細部設計這邊先不展開討論,但對於功能的插入而言,應該沒有人會希望開啟某個Plugin前還需要去閱讀文件看清楚有哪些相依Plugin需要下載吧?若我們設計的Plugin有依賴於其他同層級的GamePlugin的情形發生,可能要思考看看哪些功能要下放到GameModule給專案自己實作,哪些功能需要整合進同一個GamePlugin中。 由上面的討論我們知道,Plugin在本質上就是對於能獨立運作功能的封裝,而這些功能來自於各種需求各異的不同專案,因此Plugin在設計上必須要放在一個更抽象的位置以滿足不同專案的需求。因此我不建議在Plugin中實現任何的框架機制,因為那個是Engine該做的事;若只是薄薄一層的實作,主要目的只是更改某些Engine的預設設定的話,那個則是專案GameModule該做的事,我們不該假設我們的設定會比Engine的預設更通用;若是覺得Engine的某些Function實作有bug或不夠完美,所以想繼承下來做override來做workaround,那個也是專案該做的事,我們可以寫一些文檔分享給其他團隊說明為什麼我們想做這些修改,而不是寫成某個Plugin要求大家先繼承某個class再做某些事,因為那不一定適用於所有的專案,再次強調,我們不該假設我們的實作會比Engine的實作更具有通用性,若你覺得你的目的是修掉Engine Bug,你該做的是上Github送PullRequest給Engine。 那麼到底什麼東西適合寫成一個Plugin,最好的判斷方法是:先假設把這個Plugin拿到UnrealMarketplace上賣時,我們是否能夠明確的指出這個Plugin想解決的是什麼問題?受眾是誰、有多少人可能會需要這個功能……等等類似的問題,若無法回答這些問題,代表相關功能的想法還不夠成熟,建議先拉回GameModule中等待各種需求的磨練。 那麼在把功能寫在GameModule時我們該注意什麼呢?就我所知,根據專案團隊的風格會有不同的構成方式,有人喜歡使用一個大module放入所有的功能、也有人喜歡依功能的性質導入一些模組化的設計,但不可否認的是,由於這邊的程式碼非常接近Gameplay、更迭會非常快速,有可能一個遊戲企畫的改變導致某個系統在瞬間變成一個沒用的東西。基於這種變動頻繁的特性,因此有些人會覺得不需要在GameModule的層級上浪費時間進行遊戲邏輯的模組化設計,反正就是想辦法用最快的方法將功能做出來,不要因為模組化而導入的各種限制而拖慢了組裝系統的時間。不過我自己倒是有不同的觀點,在GameModule這個層級做模組化反而是我們用來對抗變化的手段,良好的模組化設計應當要考慮到當你想把某個功能移除時需要耗費多少的力氣,理想狀況是直接把該Module移除就行,不過這部份就屬於不同系統的實作細節了,就不再往下討論。除此之外,我認為模組化的練習可以幫助我們慢慢的把相對不成熟的想法粹練出來。我們不需要一開始就用Plugin的高度在思考問題,在個人經驗不足或需求還不明確的情況下很容易陷入寸入難行的地步;相對的在GameModule層級進行設計的話,我們可以先以更貼近專案的需求的方式來思考怎麼進行抽象化的設計。 結論 以上寫了這麼多,大致下可以簡單用以下幾點描述: 1. 每個GamePlugin在設計上要能做到獨立運作,也就是說在設計上只能依賴於Engine或EnginePlugin。 2. 不建議在Plugin中實現任何的框架機制,因為那個是Engine該做的事,不該假設我們的設定會比Engine的預設更通用。 … Continued
[UnrealEngine] 淺談GameFeature Plugin
GameFeaturePlugin,這個功能是5.0最大的變革之一,對程式相關職能的人而言,我覺得重要性甚至更勝於nanite跟lumen,因為相關機制會從根本性上的改變了我們程式碼的架構方式以及思考模式。 不知道這東西的人可以先看一下官方介紹:https://docs.unrealengine.com/……/game-features……/ 以下就先來粗略的分享一些我目前對這功能的理解吧: 就他的設計理念看來,他是屬於DLC概念的實現,跟Patch不同,Patch需要蓋掉原本的內容,而DLC必須要做到「熱插拔」的機制。 就傳統UE4 project,我們要達到DLC的這項要求非常麻煩,因為我們只能對「Content-Only」plugin包出dlc包,而哪些dlc需要開啟或關閉則需要你在BaseGame中做出一些可以管理的設計。 在GameFeature Plugin機制出現後,我們總算不需要做這些勞力活了。 注:所謂的BaseGame,指的是${PROJECT_ROOT}/Source下面所有的module以及${PROJECT_ROOT}/Content所有的Asset。 有些人會困惑於我們建出的GameFeature Plugin沒辦法跨專案共用,但這個理解並沒有錯,基本上GameFeature Plugin需要依賴於BaseGame而存在,因此我們也可以理解成他是專屬於project的Plugin。若你的功能想要跨專案共用,你要寫的是一般的Plugin而不是GameFeature。 GameFeaturePlugin之間可以有相依關係,但一個大原則是不能做出循環參照(circular reference)的設計。 就實現面來看,我們做出來的GameFeature Plugin依賴的是BaseGame,而BaseGame則是依賴於Plugins\Experimental\ModularGameplay。 它本身是利用訂閱者模式,讓發佈者(GameFeature)主動的將設計的內容推送到目標Actor身上,並動態掛上該Actor需要的Component。 Lyra Game 中有一個ModularGameplayActors Plugin,裡面就是幫我們做出了各種跟 GameFrameworkComponentManager 訂閱的Actor。 BaseGame本身是不認得我們做的那堆GameFeature Plugin的,他只負責做訂閱的動作。因此當我們在設計GameFeature Plugin的時候,要特別注意這些Plugin要能夠自給自足。可以想像一下,我們總不能做出一個dlc然後把dlc拔掉後原本的遊戲就不能動吧?
[UnrealEngine][開發日誌] 5.1升級到5.2時遇到的有關於AutomationTest的坑
最近5.2 preview 1發布了,想說可以開始來升級我的plugin。由於我的plugin有寫一些automation test來幫忙做unit test跟functional test,這樣我就可以省下一些QA的時間。只是在把Plugin升上5.2之後,突然發現之前寫的Automation Test都不會跑了。 在花了一些時間除錯之後,總算發現原因在哪了。結論是:Unreal在5.2之後有做以下幾個更改會影響到這個功能。 第一點:用來過濾來跑哪些Test的FilterPrefix原本是「Filter:」,在5.2的時候改成了「StartsWith:」,因此我們的的AutomationTest要調整成如下的型式: 第二點:有用ScreenShot功能的人會發現不會去比較之前留下來的ground truth image而是要求重新加入,以為整組功能壞光光,這是因為從5.2開始Default RHI變成了D3D12,我們可以在Log中發現下面的訊息: 由於系統會根據目前的FunctionalTest找下面的路徑出來比較: 因此我們只要把所有的Functional Test中的D3D11_SM5改成D3D12_SM5就能抓到需要比較的ground truth image,如下圖: 不過可以想見的是,如果我們把SM5調成SM6的話,那麼相對路徑也會變化,這邊在使用上就要特別注意。 升級完Plugin後,看到Automation Test全通過,嗯,感覺真好。
[開發日誌][UnrealEngine] PainCausingVolume如何結合GAS中的GameplayEffect使用
最近在思考怎麼做毒沼澤的功能,發現引擎中內建PainCausingVolume還蠻符合自己需求的,我們可以在這個Volume上設定傷害數值,並且設定每隔幾秒才觸發事件。 不過由於我的測試專案高度集成GameplayAbilitySystem(GAS),因此會希望所有的角色的數值變化操作都透過系統中的GameplayEffect來執行。由於PainCausingVolume跟GAS系統並沒有相依關係,為了這個機制要另外刻一個Volume又感覺有點多餘。不過好加在GAS系統本身製作的彈性還蠻大的,因此是有方法可以把這種外部的傷害數值注入,後續的傷害計算跟數值的加減就可以延著原本系統設計的邏輯進行,不需要再製作另一套系統維護。 其實方法還蠻簡單的,主要就是要在我們的給PainCausingVolume用的GE中的Magnitude Calculation Type使用Set by Caller,並設定一個GamplayTag讓系統知道之後要拿傷害的時候就去找這個Tag拿就行了。下圖我的這個Tag是設定成:GameEffect.SetByCaller.IncomingDirectDamage。 然後在角色的AnyDamage Callback中,我們就可以做出這個GameplayEffect,使用AssignTagSetByCallerMagnitude把實際的數值指定給這個Tag,然後就可以把這個效果套用給角色產生傷害,如下圖:
[開發日誌][UnrealEngine] 如何將Mixamo的動畫Retarget給Unreal Character
這篇文章主要是要記錄自己怎麼把Mixamo中的動畫導出來給Unreal中的Mannequin用,以及除何修正中途遇到的一些坑。 使用版本:5.1 使用的是Third Person Template 第一步當然是先從Mixamo下載看中想使用的動畫。 由於Mixamo的骨架跟Unreal不相容,主要原因Mixamo是少了一個root,這會造成root motion沒辦法正常運作。我們可以下載 mixamo_convert這個Blender Addons來幫我們加上一個root bone。 如下圖,右邊加上root的是我們希望的狀態: 這邊要注意的是,Blender Addons的安裝要使用zip才認的到。 https://github.com/enziop/mixamo_converter 下載完zip之後,從preference選Add-ons安裝,然後把功能打開 接著從右上方打開選單 可以看到Mixamo Rootbaker 可以選擇把bone轉成UnrealEngine公版的bone Name,但我這邊不勾,之後自己再使用UE內建的IK Rig以及IK Retargeter來做map,詳細步驟後面會記錄。另外他的OutputPath目錄不會自己建,所以要自己先開: 之後我們就可以先匯入轉換過後的Mixamo的模型(X Bot.fbx) 接著匯入其他的動畫,記得Import Mesh勾掉,Skeleton選擇剛剛匯入的。 然後就可以開始建立IK Rig,這個檔案的目的是建出組Bone Chain,讓我們可以可以在IK Retargeter中指定Source IK Rig跟Target IK Rig做map。 Source就是剛剛匯進來的Mixamo,我們必須要手動建一個出來(右鍵Animation -> IK Rig 下可以找到),Target則是Unreal內建的Mannequin,路徑是/Game/Characters/Mannequins/Rigs/IK_Mannequin。 … Continued
[開發日誌][UnrealEngine] 如何自動排序原始碼中的include
之前在寫code的時候,發現某個cpp檔中的include了一大堆其他module中的檔案,而且其中一大堆重覆include,越看越覺得亂,然後心中的潔癖突然發作想要好好的整頓一番。第一個想法是,如果我們的include能夠自動依字母排序,這樣就能夠很清楚的看出有哪些include重覆了,若我們能好好的依不同的module進行分類,還能夠清楚的知道我們用到了各別module中的哪些功能。 查詢了一下,其實VisualStudio中還真的有相關的功能,他的快速鍵是Shift+Alt+L, Shift+Alt+S: 由於我有買Visual Assist X,因此另外也試了一下它的版本,並把快速鍵設成了F9。最後發現這個版本比內建的順手多了: https://docs.wholetomato.com/default.asp?W175 另外也發現了Visual Studio Code也有SortLine的功能: 做完快速鍵的設置後,我們就可以使用該功能來幫我們做排序,我自己是喜歡依據module的不同來區分,如下圖: 除了include之外,我也喜歡對build.cs中的DependencyModuleNames做排序: 嗯,看起來清爽多了。
關於數學的本質的理解
最近無意間看到台大數學系教授關於數學的本質這堂課,讓我開始思考到底什麼是數學。以前大學的時候上課就是不斷的在背公式、解題以及背下證明題的步驟,等考完試之後所有的東西就全部還給老師了,從來不記得自己到底在算了什麼跟證明了什麼。因此,這堂課對我的衝擊其實蠻大的,從來沒有人從公理化數學的角度切入並介紹相關的概念給自己。 只是關於何謂公理系統,雖然在ChatGPT的補助之下自己有一些結論,但這些結論是否正確自己也不是很有把握,有任何問題也歡迎一起討論。 以下是我對數學本質的理解: 數學的本質就是遊戲設計,遊戲設計的本質就是在創造世界。 公理等同於遊戲規則,一套規則可以組成一個遊戲(公理系統)。你可以覺得這個遊戲是糞Game,但沒辦法去質疑規則是否正確,不能接受某個公理系統的規則的話,就是必須要換個遊戲玩或自己做一套。而定理,就是基於這套基本遊戲規則所製定出來的組合技,我們可以直接引用合適的定理來證明某個命題,而不用從公理開始進行推論,這樣就可省下不少推導的時間,就像是在遊戲中可以把某個必殺技的複雜按法設為快捷鍵直接快速放招。 由於科學的定義必須要可以證偽,也就是說理論的前提必須要讓其他人可以驗證是否正確,否則根據真值表,前提是False,結論隨便你怎麼說都會是True。而數學的公理是不能證偽的大前提,所以數學不是科學。 那麼公理是發現還是發明出來的呢? 大部份公理都是發現出來的,因為他要解決的是我們現實世界的問題,但如果要解決的不是現實世界問題的時候,就可以自己發明。數學最神奇的地方在於他本應是只存在於人類理性思維下的產物,但卻可以用來預測現實世界的物理規律,因此古今不少物理學家都認為上帝是一位數學家。 另外也有看到了axiom(公理)、postulate(公設)這二個詞常常被交替使用,網路上查了很久都沒有得到滿意的答案,最後問了一下AI後,就突然懂了。 以遊戲來比喻的話,我可以將axiom想成是硬體要求而postulate想成是遊戲中的規則,因為硬體要求不需要解釋,不滿足條件就是不能玩遊戲,而遊戲中的規則是由遊戲設計師根據他的理念設計出來的,他可以決定要不要解釋(證明)或者是不解釋直接使用該設計,不接受該設計的玩家就必須選擇離開。 在有了公理系統之後,人們開始思考某個公理系統是否完備。所謂的完備性,是指某個公理系統是否能解決所有可能出現的陳述,也就是說這個遊戲規則是不是夠完整有沒有邏輯上的bug,當然規則不完整的遊戲也是可以自成一套系統的,只要不去觸發那些會當機卡死你自己的bug,在規則內愛怎麼玩就怎麼玩,沒人會阻止你。 大數學家希爾伯特在1900年提出的23問中的第2題是個野心非常大的提問,他試圖想要讓全世界的遊戲設計師(數學家)設計完所有可能的遊戲(公理系統)之後,再把所有的遊戲融合起來創造出一款內涵所有遊戲規則的遊戲。 他曾經發出了這麼一句豪語:We must know, we will know,我們必須知道、我們必將知道。這是個多麼無畏且自信的發言阿,從中可以窺探出這位大數學家的遠大理想,可是,這個理想卻被哥德爾不完備定理所粉碎了。簡單來理解這個定理,就是規則與規則之間必會找到不相容之處,一致性(Consistency)與完備性(Completeness)只能取其一。 例如遊戲一的必殺技是跳+普攻;遊戲二只有普攻且不能有跳。這時候這個想要進行大一統的新遊戲為了Consistency(一致性)就必須把「跳」的規則拿掉,這代需要捨棄Completeness(完備性)。當一個系統的輸入有二種可能的輸出代表該系統的行為不一致;設計上邏輯的漏洞代表不完備。只要是寫在遊戲企畫書上的規格,若系統沒有實作,QA的唯一標準動作就是報bug。 而這種bug產生的原因,究其根本是由於二個系統的規則套用到同一個實體而造成邏輯上的自我指涉造成無窮遞迴而永不停止,例如圖靈提出的halting problem或者是羅素的理髮師悖論。以前在學演算法或計算理論時沒人幫我把這些概念連繫在一起,在發現他們在講同一件事的當下讓我久久不能自己。 關於halting problem這邊就舉幾個例子解釋一下。 原始的陳述是在講說若有二個機器,其中機器A能夠判斷任何程式是否halting,若halting回傳true否則回傳false,另一個機器B則輸出跟機器跟A完全相反的結果,若A停B就跑,A跑B就停。但當機器B把自己餵進去的時候,就會就會產生邏輯矛盾讓程式永遠停不下來。 另一個例子是羅素的理髮師悖論,為了讓敘述的結構跟上面的一樣,因此我做了一些修改,我稱之為肥宅理髮師悖論:我們先定義理髮師這個職業的規則是只會幫除了自己以外的人理頭,另外定義肥宅這種人的規則是他懶得出門所以只能在家幫自己理頭。 但這時候如果某個理髮師其實就是肥宅的話,那麼他到底能理誰的頭? 最後的例子是上面提到被德爾不完備定理粉碎的大一統遊戲,當我們想要在這個新遊戲中發出必殺技時,若該遊戲是完備的,代表該操作必須要通過所有規則的檢測,很明顯,這二個遊戲對於跳的規則定義是完全相反的,就像是硬幣的正反面只能擇其一, 若是我們想要保留所有的規則,就會發現在把規則丟下去互相檢驗時會落入無窮迴圈:遊戲一說能跳,把規則餵進去遊戲二時卻發現不能跳,這時候套回遊戲一又發現能跳……這個檢測將永遠停不下來。 這個問題其實也是第三次數學危機產生原因,最後是通過加入一個新公理( Axiom of regularity / Axiom of foundation ) 把這種自我指涉的狀況排除掉。只是整個集合論所依賴的ZFC公理系統這樣就完備了嗎?目前看起來是還沒有,第三次數學危機只是表面上解決,上面那個加新公理的方法只是一種workaround,這種自我指涉矛而產生的矛盾陳述仍然存在。只要是可以描述的問題,我們就會希望可以透過計算來得到解答。──我猜這也是圖靈最初的美好幻想──我們想要的是一台終極強大且完美的邏輯機器,希望能夠幫助我們解決所有的問題。好奇心是人類的天性,人類自古以來就一直在追求著萬事萬物的終極解答。 嗯,答案是42,但為什麼? 可能要等到有一天有人真的想到一套完美的方法,可以從根本上解決這種矛盾問題,也就是說不再有undecidable … Continued
[UnrealEngine] 如何使用DataFragment設計進行資料欄位擴充
本文章內容基於UnrealEngine5.1版本 最近在看5.0新導入的MassFramework,看到裡面出現了FInstanceStruct這個詞引起了我的興趣。基本上這東西可以讓我們動態選擇struct,而在Unreal中是把這種組裝用的property稱為Fragment。這個概念我第一次看到是在Unreal官方釋出的Lyra範例專案中看到,只是當時實驗了一下,由於他是使用UObject搭上reflection macro來實現,我沒辦法把一個UObject直接放進DataTable的Row中做編輯。這帶來的結果,就是當我在製作Plugin的時候,若需要用到DataTable時,必須要預先預想好所有使用者可能會想要有的欄位:哪些要直接放到TableRow中、哪些要指向外部的asset讓使用者自己在自己定義的BP或DataAsset中擴充。然而,可以想像的到,多了一個uasset就代表編輯的時候要另外開檔案,這對企畫人員非常的不友善,而且檔案一多,在維護上也會有不小壓力。這次看到了FInstanceStruct,馬上直覺想到這東西或許可以解決這個困擾我很久的問題。稍微實驗了一下,馬上驗證了上面提到的問題可以靠FInstanceStruct的機制來解決,因此決定花一點時間整理二種方法的優缺點。 接著,讓我們來看看這二種方法個別怎麼實現吧。 使用InstanceStruct製作Fragment的例子 InstanceStruct是定義在StructUtils這個Plugin中,記得要先去開起來。雖然在5.1還是Experimental,不過它是MassEntity的相依功能,而這個Plugin已經進Beta,因此看起來是不太需要擔心,目前我用起來感覺還蠻穩定的。 在把Plugin開起來之後,首先我們需要要定義一個BaseFragment: 接著就可以在TableRow中宣告這個property,而這個property需要的meta如下: BaseStruct用來限定Editor中可以顯示哪些Struct出來,而ExcludeBaseStruct可以把這個BaseFragment從選項中排除。 然後就可以根據需求繼承這個BaseFragment並加入任何需要的資料。 實際在DataTable中使用起來的體驗如下圖,我們可以自由選到所有繼承自BaseFragment的struct,裡面對應可編輯的欄位也會動態反射顯示出來: 接著可以用以下方法拿出我們所定義的Fragment: 不過這方法的缺點是,相關的擴充功能只能在C++中進行,因為我們無法用BP去剛剛宣告出來的BaseFragment。 使用UObject製作Fragment的例子 基本概念跟上面的例子差不多,只是我們需要換成使用UObject宣告BaseFragment,另外要記得UCLASS裡要加上DefaultToInstanced跟EditInlineNew。 宣告使用,可以在BP或DataAsset中宣告,這邊宣告在DataAsset中: 接著就可以繼承下來做資料的擴充 當我們把他宣告在DataAsset中時,就可以根據狀況選擇需要的Fragment,另外除了C++外,也可以純使用BP下來做欄位的擴充,裡面的可編輯欄位也能動態的反射顯示出來。 使用法方就是把DataAsset讀進來後,直接對Fragment做Cast就行: 結論 二種製作Fragment的方法各有優缺點,就資料擴充性而言由於InstanceStruct只能在C++中進行擴充,而UObject則可以同時在C++/BP中做,這讓UObject版本帶來某方面的優勢,然而,這方法的缺點是需要遊戲設計者另外製做出BP或DataAsset才能夠做資料的編輯,也就是說會多一個uasset,這在某些需要定義大量道具、武器裝備數據的應用是無法接受的;相反的InstanceStruct則可以很自然的作為TableRow的擴充嵌入DataTable。關於這點,我們可以看下圖,CustomDataFragment使用的是InstanceStruct方法,而CustomDataAsset則是使用UObject的方法,這個DataAsset中包含了Fragment的定義。 因此哪種方法比較好,可能要看遊戲的設計而決定。例如UE5導入的MassFramework,裡面的FMassFragment是基於InstanceStruct的設計;而在Unreal官方釋出的Lyra範例專案中,他的裝備系統是基於UObject Fragment方法的設計。另外若是多個TableRow需要「共用」參數的話,或許UObject Fragment也是個不錯的解決方案。我自己是在資料表中同時提供二種方式,讓使用者自行決定要使用哪種方式做擴充。
[UnrealEngine][讀書心得] 關於UnrealEngine中的ISPC與SIMD的一些雜談
SIMD(Single Instruction Multiple Data)指的是用一個指令同時處理多個資料這個概念,只是我們常常被誤導說,在CPU上可以使用SIMD指令來加速──這種說法其實並不精確。對,我就是被誤導的那個。在UE4.23時官方整合了ISPC,以更好的幫助CPU做平行化加速,我之前一直把這東西跟SIMD畫上等號,沒去仔細思考這東西的存在到底想解決什麼問題。 根據指令數跟跟資料的處理方式,這個概念其實是根據Flynn’s Taxonomy來進行分類,而SIMD只是其中一種。 SIMD就現行的計算機架構上,分為以下三種: 不過由於Vector Architecture在商業競爭上輸給了Intel因此逐漸式微,以Scalar Processor為主的設計主宰了過去將近30~40年的時間。輸掉的原因有很多,但主要並不是技術問題,而是那個時代大部份的人不需要那麼強的計算力,客戶太少,再加上摩爾定律的推波助瀾下,人們的注意力自然就只集中在由Intel主推擁有最多客戶的架構上。然而近來由於AI時代的來臨,Vector Architecture架構又重新回到了人們眼界中了,例如google tensor裡面的TPU就包含了Vector processor,以及最近很火紅的RISC-V,由於設想目標是embedded devices,不像個人電腦需要考慮兼容性,因此可以大膽的拋棄舊包袱,轉而使用相關的Vector Operation指令。 另一方面,關於Multimedia SIMD,主要是由於Intel在網路起飛的時代發現我們基於Scalar Processor的架構還是需要Vector Architecture那種一個指令同時處理多個資料的加速能力,因此他的解決方式非常粗暴,就是加入新的register跟指令集的方式來支援這個概念。不過當我們開始疊加register跟指令集之後就開始失控了,每我們加一組新的register,我們就要追加一堆新的指令集來支援相關Vector操作,從XMM(128 bit)、YMM(256 bit)到ZMM(512 bit)。這些指令集的擴充非常快速,從1996年開始,幾乎每2~3年就有新的特性加入,然而為了兼容性,新的指令集在加入x86系統後就非常難被移除,因此現代指令集暴增的這麼快速有很大的原因可以歸究於Multimedia SIMD的發展。 接下來讓我們來稍微看一下這些擴充指令集,感受一下它發展的混亂吧:從1996年的MMX,它總共有57條擴充指令使用,到SSE又追加了70條,接著SSE2加入了144條指令,而SSE3只是在原有架構下的擴充,所以只追加了13條指令。接下來則是SSE4.1跟4.2,又分別加入了47條跟7條。前面的架構使用到的是XMM register,接下來為了增加平行化的數量,Intel推出了AVX系列,使用的是YMM register,然後又更進一步的推出了AVX-512,使用ZMM register。可惜的是我目前沒找到AVX的指令集數量,不過我猜應該有上百個。問了一下AI,他是跟我說要看所使用的具體版本。 對於最新的AVX512,Linus Torvalds在2020年的時曾經破口大罵說:我希望AVX-512可以死的痛苦一點(I hope AVX512 dies a painful death)。希望intel不要靠增加魔法指令來讓跑分好看,要他們回到正軌。 基本上這玩意耗能大,大部份的人享受不到好處,又不是什麼科研計算,也不是server,一般人在使用電腦的時候又常常需要切換process執行,這代表作業系統會不斷的進行Context Switch,要知道,每次Context Switch是需要把當前計算用到的register保存下來,在這一讀一寫之間很容易讓使用者感受到延遲,而且還多佔了空間。 另外在Intel 12th … Continued
[UnrealEngine][開發日誌] 關於GAS中FAttributeSetInitterDiscreteLevels的神坑
最近發現GAS的Attribute初始化時那張CurveTable,在打包版本時會把多餘的資料移除,造成FAttributeSetInitterDiscreteLevels在進行初始化時會失敗,猜測這個行為是在4.23以後(2020年3月的commit)的修改。 由於CurveTable本身設計是被用來當成連續型資料使用,因此進行重覆點的移除是可以理解的,因為可以減少個存的資料量進而增進效能,只要我們可以在2點之間利用內插法取得數值就行。 但是用在GAS的Attribute初始化上就出問題了,因為在GAS中他並不被當成連續型資料使用;而是轉成離散型資料,並以CurveTable上面的點進行屬性的分群。 移除重覆點,代表我們會遺失某個等級的資訊,例如當我們某個GAS Attribute在不同的等級上的數值都沒有變化,例如1~3級的移動速度都是200,他就只會保留等級1跟等級3,中間的等級2被當成重覆資料移除掉了。 當我們對等級2的角色進行屬性初始化時,就會因為找不到該等級資料而失敗。 這個議題比較噁心的點在於,它只會在打包版本發生,因為在editor中並沒有進行移除重覆資料的動作。 這個問題解決方法有幾種: 1. 使用ConsoleVariable把CurveTable.RemoveRedundantKeys這個預設行為關閉,但會影響所有的CurveTable,造成這項優化失效。 2. 重新設計AttributeSetInitter,不要離散化,而是直接用CurveTable