Привет!
В этой статье я попытаюсь в подробностях объяснить, как именно работают Blueprint функции с точки зрения C++ кода. Разберем разницу в C++ реализации между Blueprint функциями и C++ функциями, а также будет разобран пример одной из "Blueprint схем".
FFrame
FFrame - это класс, хранящий текущую исполняемую функцию (указатель UFunction (1*)), аргументы функции (указатель Locals), "байткод" хранящейся функции (указатель Code), контекст, то есть объект на котором эта функция вызывается (используется в случае, если FFrame хранит в себе C++ функцию), и пр.
Также FFrame это класс, который имеет свойство выполнять байткод данной функции (то есть байткод UFunction) (Также стоит сказать, что в случае если FFrame хранит C++ функцию, то указатель Code ни на что не указывает (как и Locals)).
Скрытый текст
Полный код класса FFrame:
struct FFrame : public FOutputDevice
{
public:
// Variables.
UFunction* Node; - сама функция
UObject* Object; - объект, на котором выполняется байткод функции Node
uint8* Code; - код нашей функции
uint8* Locals; - параметры этой функции
FProperty* MostRecentProperty;
uint8* MostRecentPropertyAddress;
uint8* MostRecentPropertyContainer;
/** The execution flow stack for compiled Kismet code */
FlowStackType FlowStack;
/** Previous frame on the stack */
FFrame* PreviousFrame;
/** contains information on any out parameters */
FOutParmRec* OutParms;
/** If a class is compiled in then this is set to the property chain for compiled-in functions. In that case, we follow the links to setup the args instead of executing by code. */
FField* PropertyChainForCompiledIn;
/** Currently executed native function */
UFunction* CurrentNativeFunction;
#if UE_USE_VIRTUAL_STACK_ALLOCATOR_FOR_SCRIPT_VM
FVirtualStackAllocator* CachedThreadVirtualStackAllocator;
#endif
/** Previous tracking frame */
FFrame* PreviousTrackingFrame;
bool bArrayContextFailed;
/** If this flag gets set (usually from throwing a EBlueprintExceptionType::AbortExecution exception), execution shall immediately stop and return */
bool bAbortingExecution;
#if PER_FUNCTION_SCRIPT_STATS
/** Increment for each PreviousFrame on the stack (Max 255) */
uint8 DepthCounter;
#endif
public:
// Constructors.
FFrame( UObject* InObject, UFunction* InNode, void* InLocals, FFrame* InPreviousFrame = NULL, FField* InPropertyChainForCompiledIn = NULL );
virtual ~FFrame()
{
#if DO_BLUEPRINT_GUARD
FBlueprintContextTracker& BlueprintExceptionTracker = FBlueprintContextTracker::Get();
if (BlueprintExceptionTracker.ScriptStack.Num())
{
BlueprintExceptionTracker.ScriptStack.Pop(EAllowShrinking::No);
}
// ensure that GTopTrackingStackFrame is accurate
if (BlueprintExceptionTracker.ScriptStack.Num() == 0)
{
ensure(PreviousTrackingFrame == nullptr);
}
else
{
ensure(BlueprintExceptionTracker.ScriptStack.Last() == PreviousTrackingFrame);
}
#endif
PopThreadLocalTopStackFrame(PreviousTrackingFrame);
if (PreviousTrackingFrame)
{
// we propagate bAbortingExecution to frames below to avoid losing abort state
// across heterogeneous frames (eg. bpvm -> c++ -> bpvm)
PreviousTrackingFrame->bAbortingExecution |= bAbortingExecution;
}
}
// Functions.
COREUOBJECT_API void Step(UObject* Context, RESULT_DECL);
/** Convenience function that calls Step, but also returns true if both MostRecentProperty and MostRecentPropertyAddress are non-null. */
FORCEINLINE_DEBUGGABLE bool StepAndCheckMostRecentProperty(UObject* Context, RESULT_DECL);
/** Replacement for Step that uses an explicitly specified property to unpack arguments **/
COREUOBJECT_API void StepExplicitProperty(void*const Result, FProperty* Property);
/** Replacement for Step that checks the for byte code, and if none exists, then PropertyChainForCompiledIn is used. Also, makes an effort to verify that the params are in the correct order and the types are compatible. **/
template<class TProperty>
FORCEINLINE_DEBUGGABLE void StepCompiledIn(void* Result);
FORCEINLINE_DEBUGGABLE void StepCompiledIn(void* Result, const FFieldClass* ExpectedPropertyType);
/** Replacement for Step that checks the for byte code, and if none exists, then PropertyChainForCompiledIn is used. Also, makes an effort to verify that the params are in the correct order and the types are compatible. **/
template<class TProperty, typename TNativeType>
FORCEINLINE_DEBUGGABLE TNativeType& StepCompiledInRef(void*const TemporaryBuffer);
COREUOBJECT_API virtual void Serialize( const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category ) override;
COREUOBJECT_API static void KismetExecutionMessage(const TCHAR* Message, ELogVerbosity::Type Verbosity, FName WarningId = FName());
/** Returns the current script op code */
const uint8 PeekCode() const { return *Code; }
/** Skips over the number of op codes specified by NumOps */
void SkipCode(const int32 NumOps) { Code += NumOps; }
template<typename T>
T Read();
template<typename TNumericType>
TNumericType ReadInt();
float ReadFloat();
double ReadDouble();
ScriptPointerType ReadPointer();
FName ReadName();
UObject* ReadObject();
int32 ReadWord();
FProperty* ReadProperty();
/** May return null */
FProperty* ReadPropertyUnchecked();
/**
* Reads a value from the bytestream, which represents the number of bytes to advance
* the code pointer for certain expressions.
*
* @param ExpressionField receives a pointer to the field representing the expression; used by various execs
* to drive VM logic
*/
CodeSkipSizeType ReadCodeSkipCount();
/**
* Reads a value from the bytestream which represents the number of bytes that should be zero'd out if a NULL context
* is encountered
*
* @param ExpressionField receives a pointer to the field representing the expression; used by various execs
* to drive VM logic
*/
VariableSizeType ReadVariableSize(FProperty** ExpressionField);
/**
* This will return the StackTrace of the current callstack from the last native entry point
**/
COREUOBJECT_API FString GetStackTrace() const;
/**
* This will return the StackTrace of the current callstack from the last native entry point
*
* @param StringBuilder to populate
**/
COREUOBJECT_API void GetStackTrace(FStringBuilderBase& StringBuilder) const;
/**
* This will return the StackTrace of the all script frames currently active
*
* @param bReturnEmpty if true, returns empty string when no script callstack found
* @param bTopOfStackOnly if true only returns the top of the callstack
**/
COREUOBJECT_API static FString GetScriptCallstack(bool bReturnEmpty = false, bool bTopOfStackOnly = false);
/**
* This will return the StackTrace of the all script frames currently active
*
* @param StringBuilder to populate
* @param bReturnEmpty if true, returns empty string when no script callstack found
* @param bTopOfStackOnly if true only returns the top of the callstack
**/
COREUOBJECT_API static void GetScriptCallstack(FStringBuilderBase& StringBuilder, bool bReturnEmpty = false, bool bTopOfStackOnly = false);
/**
* This will return a string of the form "ScopeName.FunctionName" associated with this stack frame:
*/
UE_DEPRECATED(5.1, "Please use GetStackDescription(FStringBuilderBase&).")
COREUOBJECT_API FString GetStackDescription() const;
/**
* This will append a string of the form "ScopeName.FunctionName" associated with this stack frame
*
* @param StringBuilder to populate
**/
COREUOBJECT_API void GetStackDescription(FStringBuilderBase& StringBuilder) const;
#if DO_BLUEPRINT_GUARD
static void InitPrintScriptCallstack();
#endif
COREUOBJECT_API static FFrame* PushThreadLocalTopStackFrame(FFrame* NewTopStackFrame);
COREUOBJECT_API static void PopThreadLocalTopStackFrame(FFrame* NewTopStackFrame);
COREUOBJECT_API static FFrame* GetThreadLocalTopStackFrame();
};
(1*) UFunction в случае C++ функций:
UFunction - класс, содержащий рефлексивные данные о некоторой функции (то есть если мы у себя в акторе (или где либо еще) помечаем функцию макросом UFUNCTION, то этот макрос (а точнее Unreal Header Tool) создаст объект класса UFunction и запишет в этот объект информацию о функции, которую он (Unreal Header Tool) прочитал).
Также макрос UFUNCTION заставляет генерировать Unreal Header Tool функцию execName (Name - наименование функции), которая вызывается при вызове нашей функции из Blueprint. Указатель на функцию execName хранится в экземпляре UFunction.
Подробнее о UFunction в Blueprint:
При создании чисто Blueprint функции, для нее сразу же создается свой экземпляр UFunction, а также генерируется байткод этой функции и сохраняется в поле Code экземпляра UFunction.
Принцип работы FFrame
В общем принцип работы класса FFrame достаточно прост:
Функции Blueprint в Unreal Engine исполняются по следующему принципу: байткод (некоторые инструкции (2*)) рассматриваемой Blueprint функции записывается в объект FFrame, а затем уже поэтапно читается методом Step.
(2*) Все инструкции записаны через enum EExprToken в файле Script.h:
enum EExprToken : uint8
{
// Variable references.
EX_LocalVariable = 0x00, // A local variable.
EX_InstanceVariable = 0x01, // An object variable.
EX_DefaultVariable = 0x02, // Default variable for a class context.
// = 0x03,
EX_Return = 0x04, // Return from function.
// = 0x05,
EX_Jump = 0x06, // Goto a local address in code.
EX_JumpIfNot = 0x07, // Goto if not expression.
// = 0x08,
EX_Assert = 0x09, // Assertion.
// = 0x0A,
EX_Nothing = 0x0B, // No operation.
EX_NothingInt32 = 0x0C, // No operation with an int32 argument (useful for debugging script disassembly)
// = 0x0D,
// = 0x0E,
EX_Let = 0x0F, // Assign an arbitrary size value to a variable.
// = 0x10,
EX_BitFieldConst = 0x11, // assign to a single bit, defined by an FProperty
EX_ClassContext = 0x12, // Class default object context.
EX_MetaCast = 0x13, // Metaclass cast.
EX_LetBool = 0x14, // Let boolean variable.
EX_EndParmValue = 0x15, // end of default value for optional function parameter
EX_EndFunctionParms = 0x16, // End of function call parameters.
EX_Self = 0x17, // Self object.
EX_Skip = 0x18, // Skippable expression.
EX_Context = 0x19, // Call a function through an object context.
EX_Context_FailSilent = 0x1A, // Call a function through an object context (can fail silently if the context is NULL; only generated for functions that don't have output or return values).
EX_VirtualFunction = 0x1B, // A function call with parameters.
EX_FinalFunction = 0x1C, // A prebound function call with parameters.
EX_IntConst = 0x1D, // Int constant.
EX_FloatConst = 0x1E, // Floating point constant.
EX_StringConst = 0x1F, // String constant.
EX_ObjectConst = 0x20, // An object constant.
EX_NameConst = 0x21, // A name constant.
EX_RotationConst = 0x22, // A rotation constant.
EX_VectorConst = 0x23, // A vector constant.
EX_ByteConst = 0x24, // A byte constant.
EX_IntZero = 0x25, // Zero.
EX_IntOne = 0x26, // One.
EX_True = 0x27, // Bool True.
EX_False = 0x28, // Bool False.
EX_TextConst = 0x29, // FText constant
EX_NoObject = 0x2A, // NoObject.
EX_TransformConst = 0x2B, // A transform constant
EX_IntConstByte = 0x2C, // Int constant that requires 1 byte.
EX_NoInterface = 0x2D, // A null interface (similar to EX_NoObject, but for interfaces)
EX_DynamicCast = 0x2E, // Safe dynamic class casting.
EX_StructConst = 0x2F, // An arbitrary UStruct constant
EX_EndStructConst = 0x30, // End of UStruct constant
EX_SetArray = 0x31, // Set the value of arbitrary array
EX_EndArray = 0x32,
EX_PropertyConst = 0x33, // FProperty constant.
EX_UnicodeStringConst = 0x34, // Unicode string constant.
EX_Int64Const = 0x35, // 64-bit integer constant.
EX_UInt64Const = 0x36, // 64-bit unsigned integer constant.
EX_DoubleConst = 0x37, // Double constant.
EX_Cast = 0x38, // A casting operator which reads the type as the subsequent byte
EX_SetSet = 0x39,
EX_EndSet = 0x3A,
EX_SetMap = 0x3B,
EX_EndMap = 0x3C,
EX_SetConst = 0x3D,
EX_EndSetConst = 0x3E,
EX_MapConst = 0x3F,
EX_EndMapConst = 0x40,
EX_Vector3fConst = 0x41, // A float vector constant.
EX_StructMemberContext = 0x42, // Context expression to address a property within a struct
EX_LetMulticastDelegate = 0x43, // Assignment to a multi-cast delegate
EX_LetDelegate = 0x44, // Assignment to a delegate
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
// = 0x47, // CST_ObjectToBool
EX_LocalOutVariable = 0x48, // local out (pass by reference) function parameter
// = 0x49, // CST_InterfaceToBool
EX_DeprecatedOp4A = 0x4A,
EX_InstanceDelegate = 0x4B, // const reference to a delegate or normal function object
EX_PushExecutionFlow = 0x4C, // push an address on to the execution flow stack for future execution when a EX_PopExecutionFlow is executed. Execution continues on normally and doesn't change to the pushed address.
EX_PopExecutionFlow = 0x4D, // continue execution at the last address previously pushed onto the execution flow stack.
EX_ComputedJump = 0x4E, // Goto a local address in code, specified by an integer value.
EX_PopExecutionFlowIfNot = 0x4F, // continue execution at the last address previously pushed onto the execution flow stack, if the condition is not true.
EX_Breakpoint = 0x50, // Breakpoint. Only observed in the editor, otherwise it behaves like EX_Nothing.
EX_InterfaceContext = 0x51, // Call a function through a native interface variable
EX_ObjToInterfaceCast = 0x52, // Converting an object reference to native interface variable
EX_EndOfScript = 0x53, // Last byte in script code
EX_CrossInterfaceCast = 0x54, // Converting an interface variable reference to native interface variable
EX_InterfaceToObjCast = 0x55, // Converting an interface variable reference to an object
// = 0x56,
// = 0x57,
// = 0x58,
// = 0x59,
EX_WireTracepoint = 0x5A, // Trace point. Only observed in the editor, otherwise it behaves like EX_Nothing.
EX_SkipOffsetConst = 0x5B, // A CodeSizeSkipOffset constant
EX_AddMulticastDelegate = 0x5C, // Adds a delegate to a multicast delegate's targets
EX_ClearMulticastDelegate = 0x5D, // Clears all delegates in a multicast target
EX_Tracepoint = 0x5E, // Trace point. Only observed in the editor, otherwise it behaves like EX_Nothing.
EX_LetObj = 0x5F, // assign to any object ref pointer
EX_LetWeakObjPtr = 0x60, // assign to a weak object pointer
EX_BindDelegate = 0x61, // bind object and name to delegate
EX_RemoveMulticastDelegate = 0x62, // Remove a delegate from a multicast delegate's targets
EX_CallMulticastDelegate = 0x63, // Call multicast delegate
EX_LetValueOnPersistentFrame = 0x64,
EX_ArrayConst = 0x65,
EX_EndArrayConst = 0x66,
EX_SoftObjectConst = 0x67,
EX_CallMath = 0x68, // static pure function from on local call space
EX_SwitchValue = 0x69,
EX_InstrumentationEvent = 0x6A, // Instrumentation event
EX_ArrayGetByRef = 0x6B,
EX_ClassSparseDataVariable = 0x6C, // Sparse data variable
EX_FieldPathConst = 0x6D,
// = 0x6E,
// = 0x6F,
EX_AutoRtfmTransact = 0x70, // AutoRTFM: run following code in a transaction
EX_AutoRtfmStopTransact = 0x71, // AutoRTFM: if in a transaction, abort or break, otherwise no operation
EX_AutoRtfmAbortIfNot = 0x72, // AutoRTFM: evaluate bool condition, abort transaction on false
EX_Max = 0xFF,
};
Тут стоит сказать, что сам байткод функций в FFrame практически полностью состоит из этих инструкций.
Code
В общем, структура указателя Code (байткод) строена следующим образом:
... EX_Assert [информация для EX_Assert] EX_DefaultVariable [информация для EX_DefaultVariable] ...
Весь байткод функции состоит из таких инструкций и информации для них.
Сама информация для инструкций при этом может быть как обычным числом, так и объектом UObject и так далее. Об этом говорят методы самого FFrame:
template<typename TNumericType>
TNumericType ReadInt(); - чтение информации для инструкции, на который указывает указатель Code, как int
float ReadFloat(); - чтение информации для инструкции, на который указывает указатель Code, как float
FName ReadName(); - чтение информации для инструкции, на который указывает указатель Code, как FName
UObject* ReadObject(); - чтение информации для инструкции, на который указывает указатель Code, как UObject
int32 ReadWord();
Step
Смысл метода Step заключается в том, чтобы корректно обработать некоторую инструкцию в байткоде (Code).
Скрытый текст
Полный код метода Step:
void FFrame::Step(UObject* Context, RESULT_DECL) // (3*)
{
int32 B = *Code++; - получение инструкции, и смещение указателя Code на информацию для этой инструкции.
(GNatives[B])(Context,*this,RESULT_PARAM); - обработка инструкции B (вызов некоторой функции из массива GNatives по полученному индексу B).
}
Скрытый текст
Так как мы выделяем всего один байт для самой инструкции, то можно сказать, что максимальное кол-во инструкций не должно превышать 255.
(3*) Макрос RESULT_DECL имеет вид:
#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM
GNatives
Как мы можем заметить, в методе Step вызывается функция из метода GNatives по номеру некоторой инструкции B.
Сам массив GNatives имеет вид:
FNativeFuncPtr GNatives[EX_Max];
Где FNativeFuncPtr это
typedef void (*FNativeFuncPtr)(UObject* Context, FFrame& TheStack, RESULT_DECL);
То есть указатель на функцию с тремя параметрами, которая принимает некотрый контекст (объект который выполняет эту инструкцию), объект FFrame и выходной параметр.
IMPLEMENT_VM_FUNCTION
IMPLEMENT_VM_FUNCTION - макрос, который добавляет некоторую функцию для обработки определенной инструкции в массив GNatives.
Скрытый текст
Полный код IMPLEMENT_VM_FUNCTION:
#define IMPLEMENT_VM_FUNCTION(BytecodeIndex, func) \ - / * первые два макроса в данный момент не так важны * /
STORE_INSTRUCTION_NAME(BytecodeIndex) \
IMPLEMENT_FUNCTION(func) \
static uint8 UObject##func##BytecodeTemp = GRegisterNative( BytecodeIndex, &UObject::func ); - добавление func в массив GNatives.
Рассмотрим добавление функции описания инструкции в массив на примере:
DEFINE_FUNCTION(UObject::execVirtualFunction) - объявление функции void UObject::execVirtualFunction( UObject* Context, FFrame& Stack, RESULT_DECL )
{
// Call the virtual function.
P_THIS->CallFunction( Stack, RESULT_PARAM, P_THIS->FindFunctionChecked(Stack.ReadName()) ); - описание самой инструкции (4*)
}
IMPLEMENT_VM_FUNCTION( EX_VirtualFunction, execVirtualFunction ); - добавление этой функции (которая описывает инструкцию вызова функции в байткоде) в массив GNatives.
Теперь, если в Step эта инструкция встретится в байткоде, то FFrame уже будет знать как обработать информацию, идущую после этой инструкции (то есть при встрече инструкции EX_VirtualFunction вызовется функция execVirtualFunction, в которой уже продолжится обработка байткода).
(4*) Полный код макроса P_THIS:
#define P_THIS_OBJECT (Context)
#define P_THIS_CAST(ClassType) ((ClassType*)P_THIS_OBJECT)
#define P_THIS P_THIS_CAST(ThisClass)
ThisClass - это typedef, который объявляется при вызове макроса DECLARE_CLASS (вызов макроса DECLARE_CLASS генерируется в процессе рефлексии макроса UCLASS).
ThisClass на самом деле имеет тип класса, для которого вызывается макрос UCLASS.
execVirtualFunction
Разберем теперь, что происходит в функции execVirtualFunction.
Функция ReadName читает то, на что указывает указатель Code как строку. То есть после инструкции EX_VirtualFunction сразу идет название функции.
Функция FindFunctionChecked ищет данную функцию в нашем контексте (объекте UObject от которого данная функция вызывается).
CallFunction
Функция CallFunction либо вызывает нашу функцию через Invoke (в случае C++ функции), либо выполняет нашу функцию через создание нового FFrame (в случае чисто Blueprint функции).
1) Через Invoke:
Function->Invoke(this, Stack, RESULT_PARAM);
Функция Invoke в свою очередь вызывает следующую функцию:
(*Func)(Obj, Stack, RESULT_PARAM)
Где Func - это поле UFunction типа FNativeFuncPtr.
То есть указатель на ту функцию, которую генерирует Unreal Header Tool (а именно статическую функцию execName), будет храниться именно в этом поле Func. Но это поле не пусто только в том случае, если наша функция была определена в C++ и помечена макросом UFUNCTION. В случае же чисто Blueprint функции, в ее объекте UFunction поле Func пусто. Существует только байткод данной функции (Script.GetData(), где Script это поле UFunction).
Разберем вид функции execName:
Пусть в нашем акторе объявлена следующая функция:
UFUNCTION()
void SetColor();
Тогда функция execSetColor будет иметь вид:
DEFINE_FUNCTION(ABaseActor::execSetColor) - расширяется в void ABaseActor::execSetColor( UObject* Context, FFrame& Stack, RESULT_DECL )
{
P_FINISH;
P_NATIVE_BEGIN;
P_THIS->SetColor();
P_NATIVE_END;
}
P_FINISH - инкрементирование переменной поля Code в переменной Stack (если Code != nullptr) (Это делается для того, чтобы показать в коде что инициализация параметров функции в байткоде завершилась. Сама расшифровка P_FINISH об этом говорит: PARAMS_FINISH). То есть после параметров функции в байткоде идет пустой 1 байт (скорее всего это EX_Nothing или что еще более вероятно EX_EndFunctionParms).
P_NATIVE_BEGIN - объявляет переменную ScopedNativeCallTimer типа FScopedNativeTimer (скорее всего этот таймер считает, сколько времени занимает выполнение нашей функции).
P_THIS->SetColor() - каст аргумента Context к ThisClass, и вызов нашей функции SetColor от имени Context (если же данная функция возвращает значение, то в параметре RESULT_DECL будет храниться ее возвращаемое значение).
Скрытый текст
Если бы данная функция принимала параметр bool (например), то перед P_FINISH выполнялся бы макрос P_GET_UBOOL(Z_Param_tt); который читает текущий указатель Stack.Code (байткод) нашего FFrame как bool, и возвращает результат в переменную Z_Param_tt.
Затем переменная Z_Param_tt передается в функцию SetColor.
То есть исходя из данной реализации, в стеке сначала после инструкции EX_VirtualFunction идет название функции, затем ее параметры, и на этом определение функции в стеке заканчивается (а именно концом определения функции в стеке может служить инструкция EX_EndFunctionParms).
2) Через создание нового FFrame:
В случае же чисто Blueprint функции, в функции CallFunction вызывается метод ProcessScriptFunction:
ProcessScriptFunction(this, Function, Stack, RESULT_PARAM, ProcessInternal);
Задача функции ProcessScriptFunction в том, что она создает новый FFrame, и считывает параметры рассматриваемой функции в указатель Locals нового FFrame. А затем в функции ProcessScriptFunction уже вызывается функция ProcessInternal, куда передается новосозданный FFrame.
ProcessInternal:
Определение:
DEFINE_FUNCTION(UObject::ProcessInternal)
Данная функция вызывает функцию ProcessLocalScriptFunction, которая обрабатывает байткод нашей UFunction уже в новом FFrame, где у нового FFrame в указателе Locals содержатся параметры обрабатываемой функции (параметры кладутся в Locals в функции ProcessScriptFunction).
Обработка байткода рассматриваемой функции в новом FFrame происходит таким же образом (то есть путем вызова метода Step).
По окончании обработки байткода рассматриваемой функции (указатель наткнулся на инструкцию EX_Return), возвращаемое значение данной функции (если оно есть) кладется в RESULT_PARAM (который тянется еще со времен инструкции EX_VirtualFunction), и происходит выход из функции ProcessLocalScriptFunction => обработка инструкции EX_VirtualFunction закончена.
Разбор примера
Разберем следующую "Blueprint схему":
Отдельный экземпляр FFrame создается для Event Tick. Его байткод (Code) при этом имеет следующий вид:
Где EX_VT - это EX_VirtualFunction,
EX_PE - EX_ParamsEnd,
VL - Vector Lenght,
GV - Get Velocity.
После получения параметра для функции Print String идет ее вызов (через Invoke, либо через создание нового FFrame). После окончания вызова Print String идет либо пустой байт (P_FINISH - в случае C++ функции), либо инструкция EX_RETURN (в случае Blueprint функции).
По окончанию вызова Event Tick, вызывается инструкция EX_RETURN.
Итоги
Когда мы в блюпринте создаем новые функции, то под них сразу же инициализируются свои экземпляры UFunction, которые затем попадут в FFrame (то есть в UFunction теперь содержится байткод нашей Blueprint функции) (экземпляр UFunction это и есть C++ интерпретация чистых Blueprint функций). Когда мы запускаем игру, то Unreal Engine идет по этим функциям в блюпринтах, и под каждую либо будет создавать новый FFrame (в случае чистой Blueprint функции), либо будет вызывать специальную статическую функцию execName, которую инициализирует генератор кода Unreal Header Tool через макрос DECLARE_FUNCTION (в случае C++ функции).