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

藍圖的發展歷程

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

就像很多其他的創新一樣,它也是有一個漸進的過程的。它的萌芽就是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 FKismetFunctionContext::LinearExecutionList

”;接著遍歷每個藍圖節點,生成相應的“語句”,正確的名詞是:Statement,儲存到“

TMap< UEdGraphNode*, TArray > FKismetFunctionContext::StatementsPerNode

”,一個Node在編譯過程中可以產生多個Statement;最後呼叫

FScriptBuilderBase::GenerateCodeForStatement()

將Statement轉換成位元組碼,儲存到

TArray``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的序列化常用的分為兩個部分:

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 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_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 Script

成員中的每個元素,解釋位元組碼的程式碼十分直觀:

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