深入Unreal藍圖開發:理解藍圖技術架構

前面幾篇博客談了幾種常用的藍圖擴展方式,其中也對藍圖的底層機制進行了部分的解析,但是還不夠整體。這篇文章談一下目前我對藍圖技術架構的系統性的理解,包括藍圖從編輯到運行的整個過程。

藍圖的發展歷程

藍圖是一個突破性的創新,它能夠讓遊戲設計師親手創造自己想要的“遊戲體驗”。使用可視化編程的方式,可以大大的加速那種“以體驗爲核心”的遊戲開發的迭代速度,這是一次大膽的嘗試,也是一次成功的嘗試!(藍圖對於國內流行的那種“以數值成長爲核心,以挖坑爲目的”的遊戲開發,可能沒有那麼大的意義)

就像很多其他的創新一樣,它也是有一個漸進的過程的。它的萌芽就是Unreal Engine 3時代的Kismet。在Unreal Engine 3中,Unreal Script還是主要開發語言,但是可以使用Kismet爲關卡添加可視化的事件處理腳本,類似於今天的Level Blueprint。
在這裏插入圖片描述

Unreal Engine 3 官方文檔:Kismet Visual Scripting

Blueprint 這個名字很可能是UE4開發了一大半之後才定的。這就是爲啥UE4源碼裏面那麼多藍圖相關的模塊都以Kismet命名,連藍圖節點的基類也是class UK2Node啦,又有少量模塊用的是Blueprint這個名字,其實指代的都是同一系統。

以實例理解藍圖的整個機制

這篇博客的目的是把藍圖的整個體系結構完整的梳理一遍,但是如果只是講抽象的框架的,會很枯燥,所以我打算以“案例分析”的方式,從一個最簡單的藍圖入手,講解每一步的實際機制是怎樣的。
在這裏插入圖片描述
這個案例很簡單

  • 新建一個從Actor派生的藍圖
  • 在它的Event Graph中,編輯BeginPlay事件,調用PrintString,顯示一個Hello World!

我儘量細的講一下我這個案例涉及到的每一步的理解!

新建藍圖:BP_HelloWorld

在這裏插入圖片描述
這個過程的核心是創建了一個 class UBlueprint 對象的實例,這個對象在編輯器中可以被作爲一種Asset Object來處理。class UBlueprint是一個class UObject的派生類。理論上任何UObject都可以成爲一個Asset Object,它的創建、存儲、對象引用關係等都遵循Unreal的資源管理機制。

具體到代碼的話:當我們在編輯器中新建一個藍圖的時候,Unreal Editor會調用UBlueprintFactory::FactoryCreateNew()來創建一個新的class UBlueprint對象;

UObject* UBlueprintFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn, FName CallingContext)
{
    	// ......
    	// 略去非主幹流程代碼若干
    	// ......
    
		UClass* BlueprintClass = nullptr;
		UClass* BlueprintGeneratedClass = nullptr;

		IKismetCompilerInterface& KismetCompilerModule = FModuleManager::LoadModuleChecked<IKismetCompilerInterface>("KismetCompiler");
		KismetCompilerModule.GetBlueprintTypesForClass(ParentClass, BlueprintClass, BlueprintGeneratedClass);

		return FKismetEditorUtilities::CreateBlueprint(ParentClass, InParent, Name, BPTYPE_Normal, BlueprintClass, BlueprintGeneratedClass, CallingContext);
}

/** Create a new Blueprint and initialize it to a valid state. */
UBlueprint* FKismetEditorUtilities::CreateBlueprint(UClass* ParentClass, UObject* Outer, const FName NewBPName, EBlueprintType BlueprintType, 
            TSubclassOf<UBlueprint> BlueprintClassType, TSubclassOf<UBlueprintGeneratedClass> BlueprintGeneratedClassType, FName CallingContext)
{
	// ......
  	// 略去細節處理流程代碼若干
  	// ......

	// Create new UBlueprint object
	UBlueprint* NewBP = NewObject<UBlueprint>(Outer, *BlueprintClassType, NewBPName, RF_Public | RF_Standalone | RF_Transactional | RF_LoadCompleted);
	NewBP->Status = BS_BeingCreated;
	NewBP->BlueprintType = BlueprintType;
	NewBP->ParentClass = ParentClass;
	NewBP->BlueprintSystemVersion = UBlueprint::GetCurrentBlueprintSystemVersion();
	NewBP->bIsNewlyCreated = true;
	NewBP->bLegacyNeedToPurgeSkelRefs = false;
	NewBP->GenerateNewGuid();

  	// ......
  	// 後面還有一些其他處理
  	// . Create SimpleConstructionScript and UserConstructionScript
	// . Create default event graph(s)
	// . Create initial UClass
  	// ......
}

詳見引擎相關源代碼:

  1. class UBlueprint: Source/Runtime/Engine/Classes/Engine/Blueprint.h
  2. class UBlueprintFactory:Source/Editor/UnrealEd/Classes/Factories/BlueprintFactory.h
  3. class FKismetEditorUtilities: Source/Editor/UnrealEd/Public/Kismet2/KismetEditorUtilities.h

另外,這個操作還創建了一個class UPackage對象,作爲class UBlueprint對象的Outer對象,這個我在後面“保存藍圖”那一小節再展開。

雙擊打開BP_HelloWorld

當我們在Content Browser中雙擊一個“BP_HelloWorld”這個藍圖時,Unreal Editor會啓動藍圖編輯器,它是一個獨立編輯器(Standalone Editor),這個操作是Asset Object的標準行爲,就像Material、Texture等對象一樣。
在這裏插入圖片描述
Unreal Editor通過管理AssetTypeAction來實現上述功能。具體到藍圖的話,有一個class FAssetTypeActions_Blueprint,它實現了class UBlueprint所對應的AssetTypeActions。啓動藍圖編輯器這個操作,就是通過:FAssetTypeActions_Blueprint::OpenAssetEditor()來實現的

class ASSETTOOLS_API FAssetTypeActions_Blueprint : public FAssetTypeActions_ClassTypeBase
{
public:
	virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<class IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;
};

這個函數它則調用“Kismet”模塊,生成、初始化一個IBlueprintEditor實例,也就是我們天天在用的藍圖編輯器。

void FAssetTypeActions_Blueprint::OpenAssetEditor( const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor )
{
	EToolkitMode::Type Mode = EditWithinLevelEditor.IsValid() ? EToolkitMode::WorldCentric : EToolkitMode::Standalone;

	for (UObject* Object : InObjects)
	{
		if (UBlueprint* Blueprint = Cast<UBlueprint>(Object))
		{
				FBlueprintEditorModule& BlueprintEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>("Kismet");
				TSharedRef< IBlueprintEditor > NewKismetEditor = BlueprintEditorModule.CreateBlueprintEditor(Mode, EditWithinLevelEditor, Blueprint, ShouldUseDataOnlyEditor(Blueprint));
		}
	}
}

詳見引擎相關源代碼:

  1. class FAssetTypeActions_Blueprint:Source/Developer/AssetTools/Public/AssetTypeActions/AssetTypeActions_Blueprint.h
  2. class FBlueprintEditorModule: Source/Editor/Kismet/BlueprintEditorModule.h
  3. class IBlueprintEditor: Source/Editor/Kismet/BlueprintEditorModule.h

添加節點:PrintString

在這裏插入圖片描述
我們在藍圖編輯器裏面的每放入一個藍圖節點,就會對應的生成一個class UEdGraphNode的派生類對象,例如前面一篇博客介紹的裏面自己所實現的:class UBPNode_SaySomething : public UK2Node(你猜對了:UK2Node是從UEdGraphNode派生的)。UEdGraphNode會管理多個“針腳”,也就是class UEdGraphPin對象。編輯藍圖的過程,主要就是就是創建這些對象,並連接/斷開這些針腳對象等。引擎中有一批覈心的class UK2Node的派生類,也就是引擎默認提供的那些藍圖節點,具體見下圖:
在這裏插入圖片描述
詳見引擎相關源代碼:

  1. UEdGraph相關代碼目錄:Source/Runtime/Engine/Classes/EdGraph
  2. 引擎提供的藍圖節點相關代碼目錄:Source/Editor/BlueprintGraph/Class

對於我們這個例子來說,新添加的“PrintString”這個節點,是創建的一個class UK2Node_CallFunction的實例,它是class UK2Node的派生類。它內部保存了一個UFunction對象指針,指向下面這個函數:

void UKismetSystemLibrary::PrintString(UObject* WorldContextObject, const FString& InString, bool bPrintToScreen, bool bPrintToLog, FLinearColor TextColor, float Duration)

詳見:Source/Runtime/Engine/Classes/Kismet/KismetSystemLibrary.h

另外還有一個比較有意思的點是:藍圖編輯器中的Event Graph編輯是如何實現的?我想在這裏套用一下“Model-View-Controller”模式:

  • 藍圖編輯器管理一個class UEdGraph對象,這個相當於Model
    • 其他的基於Graph的編輯器可能使用class UEdGraph的派生類,例如Material Editor:class UMaterialGraph : public UEdGraph
  • 它使用class UEdGraphSchema_K2來定義藍圖Graph的行爲,相當於Controller
    • 這些行爲包括:測試Pin之間是否可以連接、創建或刪除連接等等
    • 它是class UEdGraphSchema的派生類
    • 詳見:Source/Editor/BlueprintGraph/Classes/EdGraphSchema_K2.h
  • 整體的UI、Node佈局等,都是一個複用的SGraphEditor,相當於View
    • Graph中的每個Node對應一個可擴展的Widget,可以從class SGraphNode派生之後添加的SGraphEditor中。對於藍圖來說,它們都是:class SGraphNodeK2Base的派生類
    • 詳見:Source/Editor/GraphEditor/Public/KismetNodes/SGraphNodeK2Base.h

點擊[Compile]按鈕:編譯藍圖

在這裏插入圖片描述
當點擊[Compile]按鈕時,藍圖會進行編譯。編譯的結果就是一個UBlueprintGeneratedClass對象,這個編譯出來的對象保存在UBlueprint的父類中:UBlueprintCore::GeneratedClass

藍圖編譯流程的入口函數爲:

  • void FBlueprintEditor::Compile()
  • 這個函數的核心操作是調用:void FKismetEditorUtilities::CompileBlueprint(UBlueprint* BlueprintObj, EBlueprintCompileOptions CompileFlags, FCompilerResultsLog* pResults)
  • 詳見:Source/Editor/Kismet/Private/BlueprintEditor.cpp
  • 詳見:Source/Editor/UnrealEd/Private/Kismet2/Kismet2.cpp

4.21版本之後的,藍圖編譯通過FBlueprintCompilationManager異步進行,對於分析藍圖原理來說增加了難度,可以修改項目中的“DefaultEditor.ini”,添加下面兩行關閉這一特性。

[/Script/UnrealEd.BlueprintEditorProjectSettings]
bDisableCompilationManager=true

就我們這個例子來說,編譯的核心過程如下:

void FKismetCompilerContext::Compile()
{
	CompileClassLayout(EInternalCompilerFlags::None);
	CompileFunctions(EInternalCompilerFlags::None);
}

可見,藍圖編譯主要由兩部分:Class Layout,以及根據Graph生成相應的字節碼。

Class Layout也就是這個藍圖類包含哪些屬性(即class UProperty對象),包含哪些函數(即class UFunction對象),主要是通過這兩個函數完成:

  • UProperty* FKismetCompilerContext::CreateVariable(const FName VarName, const FEdGraphPinType& VarType)
  • void FKismetCompilerContext::CreateFunctionList()

下面就看一下藍圖Graph編譯生成字節碼的過程。首先來分享一個查看藍圖編譯結果的方法,我們可以修改工程裏面的:DefaultEngine.ini,增加一下兩行:

[Kismet]
CompileDisplaysBinaryBackend=true

就可以在OutputLog窗口裏看到編譯出的字節碼,我們這個Hello World編譯的Log如下:

BlueprintLog: New page: Compile BP_HelloWorld
LogK2Compiler: [function ExecuteUbergraph_BP_HelloWorld]:
Label_0x0:
     $4E: Computed Jump, offset specified by expression:
         $0: Local variable named EntryPoint
Label_0xA:
     $5E: .. debug site ..
Label_0xB:
     $68: Call Math (stack node KismetSystemLibrary::PrintString)
       $17: EX_Self
       $1F: literal ansi string "Hello"
       $27: EX_True
       $27: EX_True
       $2F: literal struct LinearColor (serialized size: 16)
         $1E: literal float 0.000000
         $1E: literal float 0.660000
         $1E: literal float 1.000000
         $1E: literal float 1.000000
         $30: EX_EndStructConst
       $1E: literal float 2.000000
       $16: EX_EndFunctionParms
Label_0x46:
     $5A: .. wire debug site ..
Label_0x47:
     $6: Jump to offset 0x53
Label_0x4C:
     $5E: .. debug site ..
Label_0x4D:
     $5A: .. wire debug site ..
Label_0x4E:
     $6: Jump to offset 0xA
Label_0x53:
     $4: Return expression
       $B: EX_Nothing
Label_0x55:
     $53: EX_EndOfScript
LogK2Compiler: [function ReceiveBeginPlay]:
Label_0x0:
     $5E: .. debug site ..
Label_0x1:
     $5A: .. wire debug site ..
Label_0x2:
     $5E: .. debug site ..
Label_0x3:
     $46: Local Final Script Function (stack node BP_HelloWorld_C::ExecuteUbergraph_BP_HelloWorld)
       $1D: literal int32 76
       $16: EX_EndFunctionParms
Label_0x12:
     $5A: .. wire debug site ..
Label_0x13:
     $4: Return expression
       $B: EX_Nothing
Label_0x15:
     $53: EX_EndOfScript

在藍圖編譯時,會把所有的Event Graph組合形成一個Uber Graph,然後遍歷Graph的所有節點,生成一個線性的列表,保存到“TArray<UEdGraphNode*> FKismetFunctionContext::LinearExecutionList”;接着遍歷每個藍圖節點,生成相應的“語句”,正確的名詞是:Statement,保存到“TMap< UEdGraphNode*, TArray<FBlueprintCompiledStatement*> > FKismetFunctionContext::StatementsPerNode”,一個Node在編譯過程中可以產生多個Statement;最後調用FScriptBuilderBase::GenerateCodeForStatement()將Statement轉換成字節碼,保存到TArray<uint8>``UFunction::Script 這個成員變量中。

對於我們這個案例來說,PrintString是使用class UK2Node_CallFunction實現的:

  • 它通過void FKCHandler_CallFunction::CreateFunctionCallStatement(FKismetFunctionContext& Context, UEdGraphNode* Node, UEdGraphPin* SelfPin)來創建一系列的Statement,最重要的是一個“KCST_CallFunction”。
  • 最後通過void FScriptBuilderBase::EmitFunctionCall(FKismetCompilerContext& CompilerContext, FKismetFunctionContext& FunctionContext, FBlueprintCompiledStatement& Statement, UEdGraphNode* SourceNode)來生成藍圖字節碼;根據被調用函數的不同,可能轉換成以下幾種字節碼:
    • EX_CallMath、EX_LocalFinalFunction、EX_FinalFunction、EX_LocalVirtualFunction、EX_VirtualFunction
    • 我們這個PrintString調用的是UKismetSystemLibrary::PrintString(),是EX_FinalFunction

點擊[Save]按鈕:保存藍圖

在這裏插入圖片描述
這個藍圖保存之後,磁盤上會多出一個“BP_HelloWorld.uasset”文件,這個文件本質上就是UObject序列化的結果,但是有一個細節需要注意一下。

UObject的序列化常用的分爲兩個部分:

  1. UPROPERTY的話,會通過反射信息自動由底層進行序列化
  2. 可以在派生類中重載void Serialize(FArchive& Ar)函數可以添加定製化的代碼
  3. 對於自定義的Struct,可以實現一套“>>”、“<<”操作符,以及Serialize()函數

序列化屬於虛幻引擎的基礎設施,網上這方面相關的帖子很多,這裏就不重複了。

值得一提的是,其實這個BP_HelloWorld.uasset並不直接對於class UBlueprint對象,而是對應一個class UPackage對象。Unreal Editor的Asset處理有一個基礎流程,在新建Asset對象時,默認會創建一個class UPackage實例,作爲這個Asset的Outer對象。

UObject* UAssetToolsImpl::CreateAsset(const FString& AssetName, const FString& PackagePath, UClass* AssetClass, UFactory* Factory, FName CallingContext)
{

	const FString PackageName = UPackageTools::SanitizePackageName(PackagePath + TEXT("/") + AssetName);

	UClass* ClassToUse = AssetClass ? AssetClass : (Factory ? Factory->GetSupportedClass() : nullptr);

  	//! 請注意這裏:創建Package對象
	UPackage* Pkg = CreatePackage(nullptr,*PackageName);

	UObject* NewObj = nullptr;
	EObjectFlags Flags = RF_Public|RF_Standalone|RF_Transactional;
	if ( Factory )
	{  
    	//! 請注意這裏:Pkg作爲Outer
		NewObj = Factory->FactoryCreateNew(ClassToUse, Pkg, FName( *AssetName ), Flags, nullptr, GWarn, CallingContext);
	}
	else if ( AssetClass )
	{
    	//! 請注意這裏:Pkg作爲Outer
		NewObj = NewObject<UObject>(Pkg, ClassToUse, FName(*AssetName), Flags);
	}


	return NewObj;
}

這個Package對象在序列化時,也是作爲標準的UObject進入序列化流程,但是它起着一個重要的作用:

  • 在整個UObject及其子對象組成的樹狀結構中,只有最外層(Outermost)的對象是同一個對象時,纔會被序列化到一個.uasset文件中
    • 詳見:UPackage* UObjectBaseUtility::GetOutermost() const

這樣就巧妙的解決了序列化時,如何判斷對象之間的關係是聚合、還是鏈接的問題!我們來考慮另外一個例子:class UStaticMeshComponent:你可以想象一下,當Level中具有一個AStaticMeshActor,它包含UStaticMeshComponent,其靜態模型是引用的另外一個UStaticMesh對象,那麼序列化的過程是怎麼樣的呢?

  • 如果UStaticMesh對象序列進入Component、Actor,以至於進入Level,那就不對啦!因爲一個靜態模型可能在關卡中放置多個實例,如果每個都保存一遍,那就不只是浪費資源了,而是個錯誤的設計啦!
  • 在引擎中,因爲UStaticMesh對象是保存在另外一個.uasset文件中,也就是說它的Outermost對象是另外一個Package,所以在UStaticMeshComponent序列化的時候,它是通過“路徑鏈接”的方式記錄的,而不是完整對象!

把BP_HelloWorld拖放到關卡中

在這裏插入圖片描述
因爲BP_HelloWorld是一個從Actor派生的,所以它可以添加到關卡中。當我們吧BP_HelloWorld拖放到窗口中的時候,和C++創建的Actor派生類一樣,其核心操作都調用了AActor* UWorld::SpawnActor( UClass* Class, FTransform const* UserTransformPtr, const FActorSpawnParameters& SpawnParameters )來創建一個新的class AActor派生類對象。對於我們這個例子來說,第一個參數UClass *Class是一個UBlueprintGeneratedClass對象,也就是前面我們是的藍圖編譯產生的那個UBlueprintGeneratedClass

點擊[Play]按鈕:運行藍圖

在這裏插入圖片描述
下面我們就看看這個藍圖在關卡運行時的調用過程。首先,BP_HelloWorld是一個標準的Actor,但是它的BeginPlay事件和C++的Actor派生類重載BeginPlay()實現又有差別。下面我們就先看一下這個事件節點,然後再從字節碼解釋執行的層面看看PrintString節點是如何被調用的。

BeginPlay事件:AActor::ReceiveBeginPlay()

藍圖編輯器中的BeginPlay事件節點對應的並不是AActor::BeginPlay(),而是AActor::ReceiveBeginPlay()這個事件,我們看一下它的聲明:

/** Event when play begins for this actor. */
UFUNCTION(BlueprintImplementableEvent, meta=(DisplayName = "BeginPlay"))
void ReceiveBeginPlay();

從這個聲明可以看出:

  1. DisplayName = "BeginPlay",它只是看上去叫做“BeginPlay”,但是和AActor::BeginPlay()函數是兩個東西。AActor::BeginPlay()是C++的實現,並在裏面調用了ReceiveBeginPlay();
  2. ReceiveBeginPlay()是一個“用藍圖實現的事件”,這種函數我們不需要使用C++寫它的函數體。
  3. ReceiveBeginPlay()的函數體由UBT生成。生成的代碼如下:
static FName NAME_AActor_ReceiveBeginPlay = FName(TEXT("ReceiveBeginPlay"));
void AActor::ReceiveBeginPlay()
{
	ProcessEvent(FindFunctionChecked(NAME_AActor_ReceiveBeginPlay),NULL);
}

這段自動生成的代碼實際上是做了兩件事:

  1. 找到名爲“ReceiveBeginPlay”的UFunction對象;
  2. 執行“ProcessEvent”函數。

我們先來看一下這個“FindFunctionChecked()”操作,它的調用過程如下:

  • UObject::FindFunctionChecked(),this==BP_MyActor對象實例
    • UObject::FindFunction(),其實現爲:GetClass()->FindFunctionByName(InName)
      • UClass::FindFunctionByName(),this==BP_MyActor的UClass對象實例;在這個例子中,this的類型爲UClass的子類:UBlueprintGeneratedClass;
      • 上述函數就返回了“ReceiveBeginPlay”對應的一個UFunction對象指針;

在這個例子中,返回的UFunction對象,對應的就是一個“Kismet callable function”(代碼註釋裏的說法),或者是說“藍圖函數”,其字節碼就定義在在它的父類UStruct上:TArray<uint8> UStruct::Script。在藍圖編輯器中拉的那個Graph。

接下來,這個UFunction對象作爲參數,調用了“AActor::ProcessEvent()”函數,這個函數是父類:UObject::ProcessEvent()的一個簡單封裝。後者就是藍圖字節碼解釋執行的部分了!

藍圖字節碼的解釋執行

首先我們看一下藍圖的字節碼長什麼樣子吧。 在CoreUObject/Public/UObject/Script.h這個文件中有一個enum EExprToken,這個枚舉就是藍圖的字節碼定義。如果學過彙編語言、JAVA VM或者.Net CLR IL的話,對這些東西並不會陌生:

//
// Evaluatable expression item types.
//
enum EExprToken
{
  ...
  EX_Return = 0x04,	// Return from function.
  EX_Jump = 0x06,	// Goto a local address in code.
  EX_JumpIfNot  = 0x07,	// Goto if not expression.
  EX_Let  = 0x0F,	// Assign an arbitrary size value to a variable.

  EX_LocalVirtualFunction = 0x45, // Special instructions to quickly call a virtual function that we know is going to run only locally
  EX_LocalFinalFunction = 0x46, // Special instructions to quickly call a final function that we know is going to run only locally
  ...
};

這些字節碼又是怎樣被解釋執行的呢?這部分功能完全是由UObject這個巨大的基類來完成的,引擎並沒有一個單獨的Blueprint VM之類的模塊。這個不必吐槽,這是Unreal的傳統,從Unreal第一代的Unreal Script就是這樣的。引擎中使用一個全局查找表,把上述字節碼映射到函數指針。在運行時,從一個字節碼數組中逐個取出字節碼,並查找函數指針,進行調用,也就完成了所謂的“字節碼解釋執行”的過程。

具體的說,引擎定義了一個全局變量:FNativeFuncPtr GNatives[EX_Max],它保存了一個“字節碼到FNativeFuncPtr的查找表。在引擎中通過DEFINE_FUNCTIONIMPLEMENT_VM_FUNCTION來定義藍圖字節碼對應的C++函數,並註冊到這個全局映射表中,例如字節碼“EX_Jump”對應的函數:

DEFINE_FUNCTION(UObject::execJump)
{
	CHECK_RUNAWAY;

	// Jump immediate.
	CodeSkipSizeType Offset = Stack.ReadCodeSkipCount();
	Stack.Code = &Stack.Node->Script[Offset];
}
IMPLEMENT_VM_FUNCTION( EX_Jump, execJump );

字節碼解釋執行的過程在ProcessLocalScriptFunction()函數中。它使用一個循環while (*Stack.Code != EX_Return)從當前的棧上取出每個字節碼,也就是UFunction對象中的那個TArray<uint8> Script成員中的每個元素,解釋字節碼的代碼十分直觀:

void FFrame::Step(UObject* Context, RESULT_DECL)
{
	int32 B = *Code++;
	(GNatives[B])(Context,*this,RESULT_PARAM);
}

詳見相關引擎源碼:

  1. CoreUObject/Public/UObject/Script.h
  2. CoreUObject/Private/UObject/ScriptCore.h
Hello World的執行

在我們這個例子中,這個函數做了以下幾件核心的事情:

  1. 創建了一個 FFrame 對象,這個對象就是執行這個UFunction所需要的的“棧”對象,他內部保存了一個uint8* Code指針,相當於彙編語言的PC,指向當前需要的字節碼;
  2. 調用這個UFunction::Invoke(),this就是剛纔找到的那個代表ReceiveBeginPlay的UFunction對象;
  3. 調用ProcessLocalScriptFunction()函數,解釋執行字節碼。

我們的PrintString對應的字節碼是EX_FinalFunction,最終通過下面這個函數來實現。

DEFINE_FUNCTION(UObject::execFinalFunction)
{
	// Call the final function.
	P_THIS->CallFunction( Stack, RESULT_PARAM, (UFunction*)Stack.ReadObject() );
}
IMPLEMENT_VM_FUNCTION( EX_FinalFunction, execFinalFunction );

它內部通過void UFunction::Invoke(UObject* Obj, FFrame& Stack, RESULT_DECL)調用到UKismetSystemLibrary::PrintString()

小結一下

OK,羅裏吧嗦說了這麼多,下面讓我們用簡練的語言概述一下上面所有內容

  1. 藍圖首先作爲一種引擎的Asset對象,可以被Unreal Editor的Asset機制所管理,並且可以被Blueprint Editor來編輯;
  2. 在Blueprint Editor中,藍圖的Event Graph以class UEdGraph對象的方式被Graph Editor來編輯;
  3. 藍圖通過編譯過程,生成一個UClass的派生類對象,即UBlueprintGeneratedClass對象實例;這個實例對象就像C++的UObject派生類對應的UClass那樣,擁有UProperty和UFunction;
  4. 與C++生成的UClass不同的是,這些UFunction可能會使用藍圖字節碼;
  5. 在運行時,並不存在一個單獨的“藍圖虛擬機”模塊,藍圖字節碼的解釋執行完全是有UObject這個巨大的基類來完成的;
  6. 每個字節碼對應一個Native函數指針,通過GNatives[ByteCode]查找、調用;
  7. UObject通過解釋執行藍圖腳本字節碼,調用相應的C++實現的Thunk函數來完成具體的操作;

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章