前面幾篇部落格談了幾種常用的藍圖擴充套件方式,其中也對藍圖的底層機制進行了部分的解析,但是還不夠整體。這篇文章談一下目前我對藍圖技術架構的系統性的理解,包括藍圖從編輯到執行的整個過程。
藍圖的發展歷程
藍圖是一個突破性的創新,它能夠讓遊戲設計師親手創造自己想要的“遊戲體驗”。使用視覺化程式設計的方式,可以大大的加速那種“以體驗為核心”的遊戲開發的迭代速度,這是一次大膽的嘗試,也是一次成功的嘗試!(藍圖對於國內流行的那種“以數值成長為核心,以挖坑為目的”的遊戲開發,可能沒有那麼大的意義)
就像很多其他的創新一樣,它也是有一個漸進的過程的。它的萌芽就是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
// ……
}
詳見引擎相關原始碼:
class UBlueprint
: Source/Runtime/Engine/Classes/Engine/Blueprint。h
class UBlueprintFactory
:Source/Editor/UnrealEd/Classes/Factories/BlueprintFactory。h
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
));
}
}
}
詳見引擎相關原始碼:
class FAssetTypeActions_Blueprint
:Source/Developer/AssetTools/Public/AssetTypeActions/AssetTypeActions_Blueprint。h
class FBlueprintEditorModule
: Source/Editor/Kismet/BlueprintEditorModule。h
class IBlueprintEditor
: Source/Editor/Kismet/BlueprintEditorModule。h
新增節點:PrintString
我們在藍圖編輯器裡面的每放入一個藍圖節點,就會對應的生成一個
class UEdGraphNode
的派生類物件,例如前面一篇部落格介紹的裡面自己所實現的:
class UBPNode_SaySomething : public UK2Node
(你猜對了:
UK2Node
是從
UEdGraphNode
派生的)。
UEdGraphNode
會管理多個“針腳”,也就是
class UEdGraphPin
物件。編輯藍圖的過程,主要就是就是建立這些物件,並連線/斷開這些針腳物件等。引擎中有一批核心的
class UK2Node
的派生類,也就是引擎預設提供的那些藍圖節點,具體見下圖:
詳見引擎相關原始碼:
UEdGraph相關程式碼目錄
:Source/Runtime/Engine/Classes/EdGraph
引擎提供的藍圖節點相關程式碼目錄
: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
”;接著遍歷每個藍圖節點,生成相應的“語句”,正確的名詞是:Statement,儲存到“
TMap< UEdGraphNode*, TArray
”,一個Node在編譯過程中可以產生多個Statement;最後呼叫
FScriptBuilderBase::GenerateCodeForStatement()
將Statement轉換成位元組碼,儲存到
TArray
這個成員變數中。
對於我們這個案例來說,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的序列化常用的分為兩個部分:
UPROPERTY的話,會透過反射資訊自動由底層進行序列化
可以在派生類中過載
void Serialize(FArchive& Ar)
函式可以新增定製化的程式碼
對於自定義的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
();
從這個宣告可以看出:
DisplayName = “BeginPlay”
,它只是看上去叫做“BeginPlay”,但是和AActor::BeginPlay()函式是兩個東西。AActor::BeginPlay()是C++的實現,並在裡面呼叫了ReceiveBeginPlay();
ReceiveBeginPlay()是一個“用藍圖實現的事件”,這種函式我們不需要使用C++寫它的函式體。
ReceiveBeginPlay()的函式體由UBT生成。生成的程式碼如下:
static
FName
NAME_AActor_ReceiveBeginPlay
=
FName
(
TEXT
(
“ReceiveBeginPlay”
));
void
AActor
::
ReceiveBeginPlay
()
{
ProcessEvent
(
FindFunctionChecked
(
NAME_AActor_ReceiveBeginPlay
),
NULL
);
}
這段自動生成的程式碼實際上是做了兩件事:
找到名為“ReceiveBeginPlay”的UFunction物件;
執行“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
。在藍圖編輯器中拉的那個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_FUNCTION
、
IMPLEMENT_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
成員中的每個元素,解釋位元組碼的程式碼十分直觀:
void
FFrame
::
Step
(
UObject
*
Context
,
RESULT_DECL
)
{
int32
B
=
*
Code
++
;
(
GNatives
[
B
])(
Context
,
*
this
,
RESULT_PARAM
);
}
詳見相關引擎原始碼:
CoreUObject/Public/UObject/Script。h
CoreUObject/Private/UObject/ScriptCore。h
Hello World的執行
在我們這個例子中,這個函式做了以下幾件核心的事情:
建立了一個 FFrame 物件,這個物件就是執行這個UFunction所需要的的“棧”物件,他內部儲存了一個
uint8* Code
指標,相當於組合語言的PC,指向當前需要的位元組碼;
呼叫這個
UFunction::Invoke()
,this就是剛才找到的那個代表
ReceiveBeginPlay
的UFunction物件;
呼叫
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,羅裡吧嗦說了這麼多,下面讓我們用簡練的語言
概述一下上面所有內容
:
藍圖首先作為一種引擎的Asset物件,可以被Unreal Editor的Asset機制所管理,並且可以被Blueprint Editor來編輯;
在Blueprint Editor中,藍圖的Event Graph以
class UEdGraph
物件的方式被Graph Editor來編輯;
藍圖透過編譯過程,生成一個UClass的派生類物件,即UBlueprintGeneratedClass物件例項;這個例項物件就像C++的UObject派生類對應的UClass那樣,擁有UProperty和UFunction;
與C++生成的UClass不同的是,這些UFunction可能會使用藍圖位元組碼;
在執行時,並不存在一個單獨的“藍圖虛擬機器”模組,藍圖位元組碼的解釋執行完全是有UObject這個巨大的基類來完成的;
每個位元組碼對應一個Native函式指標,透過
GNatives[ByteCode]
查詢、呼叫;
UObject透過解釋執行藍圖指令碼位元組碼,呼叫相應的C++實現的Thunk函式來完成具體的操作;
參考資料
官方文件:Blueprint Compiler Overview