虚幻引擎编程基础(二)
虛幻引擎編程基礎(二)
文章目錄
- 虛幻引擎編程基礎(二)
- 一、前言
- 二、多線程
- 2.1 FRunnable & FRunnableThread
- 2.2 使用線程池的AsyncTask
- 2.3 TaskGraph
- 三、垃圾回收
- 四、常用代碼塊
- 3.1 藍圖和C++交互
- 3.2 Json文件的讀寫
- 3.3 動態創建材質、控制后處理
- 3.4 對象內存常駐的四種方式
- 3.5 其他
- 參考文章
一、前言
在 虛幻引擎編程基礎(一) 中整理一些基礎的數據結構用法。
本文主要會繼續簡單地整理一些相關的基礎內容,如多線程,垃圾回收,以及一些常用的代碼塊。
以下是筆者的一些筆記。如有錯誤,還請見諒。
二、多線程
線程是操作系統能夠進行運行調度的最小單位。
一個進程中可以并發多個線程,每條線程并行執行不同的任務。
游戲引擎中,多線程用的一點也不少,比如渲染模塊、物理模塊、網絡通信、音頻系統、IO等。
UE4使用多線程的方式非常豐富,可以分為下面三類:
- 標準多線程實現FRunnable;
- 使用線程池的AsyncTask;
- TaskGraph;
2.1 FRunnable & FRunnableThread
UE4是跨平臺的引擎,對各個平臺線程實現進行了封裝,抽象出了 FRunnable,類似與Java中的多線程方式。
主要需要認識的類有:
FRunnable FRunnableThread其中:
FRunnable是需要繼承實現的線程執行體。其接口如下:
class CORE_API FRunnable { public:/*** Initializes the runnable object.** This method is called in the context of the thread object that aggregates this, not the* thread that passes this runnable to a new thread.** @return True if initialization was successful, false otherwise* @see Run, Stop, Exit*/virtual bool Init(){return true;}/*** Runs the runnable object.** This is where all per object thread work is done. This is only called if the initialization was successful.** @return The exit code of the runnable object* @see Init, Stop, Exit*/virtual uint32 Run() = 0;/*** Stops the runnable object.** This is called if a thread is requested to terminate early.* @see Init, Run, Exit*/virtual void Stop() { }/*** Exits the runnable object.** Called in the context of the aggregating thread to perform any cleanup.* @see Init, Run, Stop*/virtual void Exit() { }/*** Gets single thread interface pointer used for ticking this runnable when multi-threading is disabled.* If the interface is not implemented, this runnable will not be ticked when FPlatformProcess::SupportsMultithreading() is false.** @return Pointer to the single thread interface or nullptr if not implemented.*/virtual class FSingleThreadRunnable* GetSingleThreadInterface( ){return nullptr;}/** Virtual destructor */virtual ~FRunnable() { } };FRunnableThreadc,才是真正負責創建的多線程,它持有FRunnable的實例。
- 通過FRunnableThread::Create創建相應平臺的線程。
例如Windows平臺:
以上就是Unreal自帶最基礎的多線程的實現方式。
渲染線程的創建就是使用的這種方法。
在LauchEngineLoop.cpp的FEngineLoop::PreInitPreStartupScreen函數中會調用StartRenderingThread函數:
// Turn on the threaded rendering flag. GIsThreadedRendering = true;// Create the rendering thread. // 創建渲染線程 GRenderingThreadRunnable = new FRenderingThread();Trace::ThreadGroupBegin(TEXT("Render")); PRAGMA_DISABLE_DEPRECATION_WARNINGSGRenderingThread = PRAGMA_ENABLE_DEPRECATION_WARNINGSFRunnableThread::Create(GRenderingThreadRunnable, *BuildRenderingThreadName(ThreadCount), 0, FPlatformAffinity::GetRenderingThreadPriority(), FPlatformAffinity::GetRenderingThreadMask(), FPlatformAffinity::GetRenderingThreadFlags());FRenderingThread的Run函數,實際調用了RenderingThreadMain。
/** The rendering thread main loop */ void RenderingThreadMain( FEvent* TaskGraphBoundSyncEvent ) {LLM_SCOPE(ELLMTag::RenderingThreadMemory);ENamedThreads::Type RenderThread = ENamedThreads::Type(ENamedThreads::ActualRenderingThread);ENamedThreads::SetRenderThread(RenderThread);ENamedThreads::SetRenderThread_Local(ENamedThreads::Type(ENamedThreads::ActualRenderingThread_Local));// 把當前線作為渲染線程掛接到TaskGraphFTaskGraphInterface::Get().AttachToThread(RenderThread);FPlatformMisc::MemoryBarrier();// Inform main thread that the render thread has been attached to the taskgraph and is ready to receive tasksif( TaskGraphBoundSyncEvent != NULL ){TaskGraphBoundSyncEvent->Trigger();}// set the thread back to real time modeFPlatformProcess::SetRealTimeMode();#if STATSif (FThreadStats::WillEverCollectData()){FThreadStats::ExplicitFlush(); // flush the stats and set update the scope so we don't flush again until a frame update, this helps prevent fragmentation} #endifFCoreDelegates::PostRenderingThreadCreated.Broadcast();check(GIsThreadedRendering);// 告訴TaskGraph系統,使用該線程一直處理渲染任務,直到請求退出FTaskGraphInterface::Get().ProcessThreadUntilRequestReturn(RenderThread);FPlatformMisc::MemoryBarrier();check(!GIsThreadedRendering);FCoreDelegates::PreRenderingThreadDestroyed.Broadcast();#if STATSif (FThreadStats::WillEverCollectData()){FThreadStats::ExplicitFlush(); // Another explicit flush to clean up the ScopeCount established above for any stats lingering since the last frame} #endifENamedThreads::SetRenderThread(ENamedThreads::GameThread);ENamedThreads::SetRenderThread_Local(ENamedThreads::GameThread_Local);FPlatformMisc::MemoryBarrier(); }其中,RenderingThreadMain會通過TaskGraph來一直執行渲染命令。
下面也將對TaskGraph進行介紹。
2.2 使用線程池的AsyncTask
線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。頻繁創建和銷毀線程也會帶來極大的開銷。
通常我們更加關心的是任務可以并發執行,并不想管理線程的創建,銷毀和調度。
通過將任務處理成隊列,交由線程池統一執行,可以提升任務的執行效率。
UE4提供了對應的線程池來滿足我們的需求。
AsyncTask系統是一套基于線程池的異步任務處理系統。
可供我們直接使用的有:
- FAsyncTask,異步任務,自動加入線程池;
- FAutoDeleteAsyncTask,異步任務,任務完成后會自動銷毀;
具體的細節在《Exploring in UE4》多線程機制詳解原理分析有詳細記錄。
2.3 TaskGraph
由于基于任務的線程模式幾乎必須要手動的進行同步和模式數據的管理,因此對于那些重量級別的常駐線程需要另一個更為強大的工具進行處理。
Task Graph 系統是UE4一套抽象的異步任務處理系統,可以創建多個多線程任務,指定各個任務之間的依賴關系,按照該關系來依次處理任務。
Task Graph中的線程為FWorkerThread。
- FWorkerThread封裝了FRunnableThread作為真正的線程體。
- 而FTaskThreadBase是對FRunnable的又一個封裝結構。
FTaskThreadBase又分為FTaskThreadAnyThread和FNamedTaskThread,分別表示非指定名稱的任意Task線程執行體和有名字的Task線程執行體。
在創建時調用Startup默認構建24個FWorkerThread工作線程(這里支持最大的線程數量也就是24),其中里面有5個是默認帶名字的線程。
例如RHIThread,AudioThread,GameThread,ActualRenderingThread都是有名線程,其他是無名。
enum Type {UnusedAnchor = -1, #if STATSStatsThread, #endifRHIThread,AudioThread,GameThread,// The render thread is sometimes the game thread and is sometimes the actual rendering threadActualRenderingThread = GameThread + 1, };對于有名字的線程,我們是在外部進行創建他們的Runnable,而對于沒有名字的線程,要在初始化的時候就創建他們的Runnable。
比如說經常使用的ENQUEUE_RENDER_COMMAND,就是把Lamda表示式傳送到渲染線程執行。
template<typename TSTR, typename LAMBDA> FORCEINLINE_DEBUGGABLE void EnqueueUniqueRenderCommand(LAMBDA&& Lambda) {QUICK_SCOPE_CYCLE_COUNTER(STAT_EnqueueUniqueRenderCommand);typedef TEnqueueUniqueRenderCommandType<TSTR, LAMBDA> EURCType;#if 0 // UE_SERVER && UE_BUILD_DEBUGUE_LOG(LogRHI, Warning, TEXT("Render command '%s' is being executed on a dedicated server."), TSTR::TStr()) #endifif (IsInRenderingThread()){FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();Lambda(RHICmdList);}else{if (ShouldExecuteOnRenderThread()){CheckNotBlockedOnRenderThread();TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));}else{EURCType TempCommand(Forward<LAMBDA>(Lambda));FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId());TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef());}} }#define ENQUEUE_RENDER_COMMAND(Type) \struct Type##Name \{ \static const char* CStr() { return #Type; } \static const TCHAR* TStr() { return TEXT(#Type); } \}; \EnqueueUniqueRenderCommand<Type##Name>在EnqueueUniqueRenderCommand中會通過CreateTask,將Lambda表示創建為GraphTask。
TEnqueueUniqueRenderCommandType繼承自FRenderCommand,其DoTask函數將調用外部傳入的Lambda表示式,同時把RHICmdList作為參數傳入。
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) {TRACE_CPUPROFILER_EVENT_SCOPE_ON_CHANNEL_STR(TSTR::TStr(), RenderCommandsChannel);FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();// 執行外部傳入的表達式Lambda(RHICmdList); }再回顧下前面所說的FRenderingThread。
其Run函數調用了的關鍵函數:RenderingThreadMain。
在這里有兩個關鍵代碼:
第一句作用為:
- 為線程進行標記(有名線程),使得可以通過ENamedThreads::Type來操作對應的線程。
第二句ProcessThreadUntilRequestReturn:
virtual void ProcessThreadUntilRequestReturn(ENamedThreads::Type CurrentThread) final override {int32 QueueIndex = ENamedThreads::GetQueueIndex(CurrentThread);CurrentThread = ENamedThreads::GetThreadIndex(CurrentThread);check(CurrentThread >= 0 && CurrentThread < NumNamedThreads);check(CurrentThread == GetCurrentThread());Thread(CurrentThread).ProcessTasksUntilQuit(QueueIndex); }其中的ProcessTasksUntilQuit:
- 是一個While循環。
在ProcessTasksNamedThread中:
// 核心邏輯如下while (!Queue(QueueIndex).QuitForReturn) {//...// 從隊伍中取出TaskFBaseGraphTask* Task = Queue(QueueIndex).StallQueue.Pop(0, bStallQueueAllowStall);// 執行任務,會調用到DoTaskTask->Execute(NewTasks, ENamedThreads::Type(ThreadId | (QueueIndex << ENamedThreads::QueueIndexShift))); }那么DoTask就會執行那些 通過ENQUEUE_RENDER_COMMAND宏,塞入的宏。
三、垃圾回收
在 虛幻引擎編程基礎(一) 中的智能指針部分,提到了虛幻中的內存管理類一般分為兩類:
垃圾回收(garbage collection, 縮寫GC)是一種自動的內存管理機制。
當一個電腦上的動態內存不需要時,就應該予以釋放,這種自動內存的資源管理,稱為垃圾回收。垃圾回收可以減少程序員的負擔,也能減少程序員犯錯的機會。
以下僅僅記錄 UE4垃圾回收 提到的結論。
分類:
| 分類一 | 引用計數 | 通過額外的計數來實時計算對單個對象的引用次數,當引用次數為0時回收對象。 引用計數的GC是實時的。像微軟COM對象的加減引用值以及C++中的智能指針都是通過引用計數來實現GC的。 |
| 追蹤式GC(UE4) | 達到GC條件時(內存不夠用、到達GC間隔時間或者強制GC)通過掃描系統中是否有對象的引用來判斷對象是否存活,然后回收無用對象 | |
| 分類二 | 保守式GC | 并不能準備識別每一個無用的對象(比如在32位程序中的一個4字節的值,它是不能判斷出它是一個對象指針或者是一個數字的),但是能保證在不會錯誤的回收存活的對象的情況下回收一部分無用對象。 保守式GC,不需要額外的數據來支持查找對象的引用,它將所有的內存數據假定為指針,通過一些條件來判定這個指針是否是一個合法的對象。 |
| 精確式GC(UE4) | 是指在回收過程中能準確得識別和回收每一個無用對象的GC方式。 為了準確識別每一個對象的引用,通過需要一些額外的數據(比如虛幻中的屬性UProperty)。 | |
| 分類三 | 搬遷式GC | 在GC過程中需要移動對象在內存中的位置。 當然移動對象位置后需要將所有引用到這個對象的地方更新到新位置(有的通過句柄來實現、而有的可能需要修改所有引用內存的指針)。 |
| 非搬遷式GC(UE4) | 在GC過程中不需要移動對象的內存位置 | |
| 分類四 | 實時GC | 不需要停止用戶執行的GC方式 |
| 非實時GC(UE4) | 需要停止用戶程序的執行(stop the world) | |
| 分類五 | 漸進式GC | 不會在對象拋棄時立即回收占用的內存資源,而在GC達成一定條件時進行回收操作 |
| 非漸進式GC(UE4) | 在對象拋棄時立即回收占用的內存資源 |
UE4采用追蹤式、精確式、非搬遷式、非實時、非漸進式的標記清掃(Mark-Sweep)GC算法。
算法分為兩個階段:
- 標記階段(GC Mark)
- 清掃階段(GC Sweep)
只有被UPROPERTY宏修飾或在AddReferencedObjects函數被手動添加引用的UObject*成員變量,才能被GC識別和追蹤。
GC通過這個機制,建立起引用鏈(Reference Chain)網絡。
沒有被UPROPERTY宏修飾或在AddReferencedObjects函數被沒添加引用的UObject*成員變量,無法被虛幻引擎識別,這些對象不會進入引用鏈網絡,不會影響GC系統工作。
垃圾回收器定時或某些階段(如:LoadMap、內存較低等)從根節點Root對象開始搜索,從而追蹤所有被引用的對象。
當UObject對象沒有直接或間接被根節點Root對象引用或被設置為PendingKill狀態,就被GC標記成垃圾,并最終被GC回收。
詳細地流程可以查看 淺談UE4的垃圾回收。
四、常用代碼塊
3.1 藍圖和C++交互
UFUNCTION
如何讓C++的函數可以被藍圖調用:
- BlueprintCallable 關鍵字
如何在C++調用藍圖實現的函數:
- BlueprintImplementableEvent 關鍵字
3.2 Json文件的讀寫
Json文件示例:
- {…} 表示是一個數據對象
- […] 表示是一個數組
步驟1:添加依賴模塊JSon。
步驟2:添加頭文件
#include "Json.h"步驟3:讀取字符串轉換為JsonObject。
3.3 動態創建材質、控制后處理
動態創建材質:
UMaterialInstanceDynamic* MatInstance = UMaterialInstanceDynamic::Create(Mat, MeshComp);運行時修改后處理材質:
APostProcessVolume* PPV = Cast<APostProcessVolume>(Actors[0]); if (PPV) {FPostProcessSettings& PostProcessSettings = PPV->Settings;if (TestMatIns){TestMatInsDyna = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, TestMatIns);FWeightedBlendable WeightedBlendable;WeightedBlendable.Object = TestMatInsDyna;WeightedBlendable.Weight = 1;PostProcessSettings.WeightedBlendables.Array.Add(WeightedBlendable);} }3.4 對象內存常駐的四種方式
UE對象內存常駐的四種方式(防止GC的辦法):
作為成員變量,并標記為UPROPERTY();
創建對象后 AddToRoot() ;(退出游戲時需要RemoveFromRoot)
FStreamableManager Load資源時,bManageActiveHandle設置為true;
FGCObjectScopeGuard 在指定代碼區域內保持對象;
URPOPERTY()用法
URPOPERTY() UObject* MyObj;AddToRoot()用法
UMyObject* MyObj = NewObject<UMyObject>(); MyObj.AddToRoot();FStreamableManager 用法
FSoftObjectPath AssetPath(TEXT("/Game/Mannequin/Animations/ThirdPersonWalk.ThirdPersonWalk")); FStreamableManager& AssetLoader = UAssetManager::GetStreamableManager(); //hold object in memory. TSharedPtr<FStreamableHandle> Handle = AssetLoader.RequestSyncLoad(AssetPath, true); UObject* Obj = Handle->GetLoadedAsset(); //free memory of object. Handle->ReleaseHandle();FGCObjectScopeGuard 用法
{FGCObjectScopeGuard(UObject* GladOS = NewObject<...>(...));GladOS->SpawnCell();RunGC();GladOS->IsStillAlive(); // Object will not be removed by GC }3.5 其他
遍歷世界的Actor
for (TActorIterator<AActor> It(GetEditorWorld(), AActor::StaticClass(), Flags); It; ++It) {//... }遍歷某個Actor上所有的Component
const TSet<UActorComponent*> CompSet = Actor->GetComponents(); for(UActorComponent* UniqueComp : CompSet) {// ... }TSubclassOf<> :提供UClass類型安全性的模板類。
Cast<>:類型轉換。
參考文章
- UE4 C++基礎教程 - 多線程
- 虛幻4筆記-渲染線程源碼和TaskGraph多線程機制源碼實現分析
- 《Exploring in UE4》多線程機制詳解原理分析
- 虛幻4之TaskGraph:十分鐘上手TaskGraph
- 虛幻4之TaskGraph: 實現Fork-Join模型
- UE4垃圾回收
- 淺談UE4的垃圾回收
- 對象內存常駐的四種方式
- UE4 C++讀寫Json文件
- 讀取Json文件
- DynamicMaterialInstance
- Modify Post Process Settings At Run-time
總結
以上是生活随笔為你收集整理的虚幻引擎编程基础(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: break,returned,和cont
- 下一篇: 柯基数据:先进的知识图谱技术,构建行业知