The Internals of Blueprint Functions in Unreal Engine 5

Hello!

In this article I will try to explain in detail how exactly Blueprint functions work from the point of view of C++ code. We will analyze the difference in C++ implementation between Blueprint functions and C++ functions, and also an example of one of the “Blueprint schemes” will be analyzed.

FFrame

FFrame is a class that stores the currently executing function (UFunction pointer (1*)), function arguments (Locals pointer), “bytecode” of the stored function (Code pointer), context, i.e. the object on which this function is called (used if FFrame stores a C++ function), etc.

Also, FFrame is a class that has the property of executing the bytecode of a given function (that is, the bytecode of UFunction) (It is also worth saying that if FFrame stores a C++ function, then the Code pointer does not point to anything (like Locals)).

Hidden text

Full code of the FFrame class:

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 in case of C++ functions:

UFunction is a class containing reflexive data about a certain function (that is, if we mark a function with the UFUNCTION macro in our actor (or somewhere else), then this macro (or more precisely, the Unreal Header Tool) will create an object of the UFunction class and write information about the function that it (the Unreal Header Tool) read into this object).

Also, the UFUNCTION macro makes the Unreal Header Tool generate the execName function (Name is the name of the function), which is called when our function is called from Blueprint. The pointer to the execName function is stored in the UFunction instance.

More about UFunction in Blueprint:

When creating a pure Blueprint function, its own UFunction instance is immediately created for it, and the bytecode of this function is generated and stored in the Code field of the UFunction instance.

How FFrame works

In general, the operating principle of the FFrame class is quite simple:

Blueprint functions in Unreal Engine are executed according to the following principle: bytecode (some instructions (2*)) of the Blueprint function in question is written to the FFrame object, and then read step by step using the Step method.

(2*) All instructions are written through enum EExprToken in the Script.h file:

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,
};

It is worth mentioning here that the bytecode of the functions in FFrame itself consists almost entirely of these instructions.

Code

In general, the structure of the Code pointer (bytecode) is built as follows:

… EX_Assert [информация для EX_Assert] EX_DefaultVariable [информация для EX_DefaultVariable] …

The entire bytecode of a function consists of such instructions and information for them.

The information for the instructions itself can be either a regular number or a UObject, etc. This is indicated by the methods of FFrame itself:

template<typename TNumericType>
TNumericType ReadInt(); - чтение информации для инструкции, на который указывает указатель Code, как int
float ReadFloat(); - чтение информации для инструкции, на который указывает указатель Code, как float
FName ReadName(); - чтение информации для инструкции, на который указывает указатель Code, как FName
UObject* ReadObject(); - чтение информации для инструкции, на который указывает указатель Code, как UObject
int32 ReadWord();

Step

The purpose of the Step method is to correctly process some instruction in the bytecode (Code).

Hidden text

Full code of the Step method:

void FFrame::Step(UObject* Context, RESULT_DECL) // (3*)
{
	int32 B = *Code++; - получение инструкции, и смещение указателя Code на информацию для этой инструкции.
	(GNatives[B])(Context,*this,RESULT_PARAM); - обработка инструкции B (вызов некоторой функции из массива GNatives по полученному индексу B).
}
Hidden text

Since we allocate only one byte for the instruction itself, we can say that the maximum number of instructions should not exceed 255.

(3*) The RESULT_DECL macro looks like this:

#define RESULT_PARAM Z_Param__Result
#define RESULT_DECL void*const RESULT_PARAM

GNatives

As we can see, in the Step method, a function is called from the GNatives method by the number of some instruction B.

The GNatives array itself looks like this:

FNativeFuncPtr GNatives[EX_Max];

Where is FNativeFuncPtr

typedef void (*FNativeFuncPtr)(UObject* Context, FFrame& TheStack, RESULT_DECL);

That is, a pointer to a function with three parameters, which takes some context (the object that executes this instruction), an FFrame object and an output parameter.

IMPLEMENT_VM_FUNCTION

IMPLEMENT_VM_FUNCTION – a macro that adds some function to handle a specific instruction to the GNatives array.

Hidden text

Full code of 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.

Let's consider adding a function to describe an instruction to an array using an example:

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.

Now, if this instruction is encountered in the bytecode in Step, then FFrame will already know how to process the information coming after this instruction (that is, when the EX_VirtualFunction instruction is encountered, the execVirtualFunction function will be called, in which the bytecode processing will continue).

(4*) Full code of the P_THIS macro:

#define P_THIS_OBJECT		(Context)
#define P_THIS_CAST(ClassType)		((ClassType*)P_THIS_OBJECT)
#define P_THIS		P_THIS_CAST(ThisClass)

ThisClass is a typedef that is declared by calling the DECLARE_CLASS macro (the DECLARE_CLASS macro call is generated by reflection of the UCLASS macro).

ThisClass actually has the type of the class on which the UCLASS macro is called.

execVirtualFunction

Let's now look at what happens in the execVirtualFunction function.

The ReadName function reads what the Code pointer points to as a string. That is, the EX_VirtualFunction instruction is immediately followed by the function name.

The FindFunctionChecked function searches for this function in our context (the UObject from which this function is called).

CallFunction

The CallFunction function either calls our function via Invoke (in the case of a C++ function), or executes our function by creating a new FFrame (in the case of a pure Blueprint function).

1) Via Invoke:

Function->Invoke(this, Stack, RESULT_PARAM);

The Invoke function in turn calls the following function:

(*Func)(Obj, Stack, RESULT_PARAM)

Where Func is a UFunction field of type FNativeFuncPtr.

That is, the pointer to the function generated by the Unreal Header Tool (namely, the static function execName) will be stored in this Func field. But this field is not empty only if our function was defined in C++ and marked with the UFUNCTION macro. In the case of a purely Blueprint function, the Func field in its UFunction object is empty. There is only the bytecode of this function (Script.GetData(), where Script is the UFunction field).

Let's look at the execName function:

Let the following function be declared in our actor:

UFUNCTION()
void SetColor();

Then the execSetColor function will look like:

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 – incrementing the variable of the Code field in the Stack variable (if Code != nullptr) (This is done to show in the code that the initialization of the function parameters in the bytecode has been completed. The decoding of P_FINISH itself says this: PARAMS_FINISH). That is, after the function parameters in the bytecode there is an empty 1 byte (most likely this is EX_Nothing or, even more likely, EX_EndFunctionParms).

P_NATIVE_BEGIN – declares a variable ScopedNativeCallTimer of type FScopedNativeTimer (most likely this timer counts how long it takes to execute our function).

P_THIS->SetColor() – cast the Context argument to ThisClass, and call our SetColor function on behalf of Context (if this function returns a value, then its return value will be stored in the RESULT_DECL parameter).

Hidden text

If this function took a bool parameter (for example), then before P_FINISH the macro P_GET_UBOOL(Z_Param_tt); would be executed, which reads the current Stack.Code pointer (bytecode) of our FFrame as a bool, and returns the result to the Z_Param_tt variable.

The Z_Param_tt variable is then passed to the SetColor function.

That is, based on this implementation, in the stack, first after the EX_VirtualFunction instruction, comes the name of the function, then its parameters, and this is where the definition of the function in the stack ends (namely, the end of the definition of the function in the stack can be the EX_EndFunctionParms instruction).

2) By creating a new FFrame:

In the case of a pure Blueprint function, the ProcessScriptFunction method is called in the CallFunction function:

ProcessScriptFunction(this, Function, Stack, RESULT_PARAM, ProcessInternal);

The task of the ProcessScriptFunction function is that it creates a new FFrame and reads the parameters of the function in question into the Locals pointer of the new FFrame. And then in the ProcessScriptFunction function, the ProcessInternal function is called, where the newly created FFrame is passed.

ProcessInternal:

Definition:

DEFINE_FUNCTION(UObject::ProcessInternal)

This function calls the ProcessLocalScriptFunction function, which processes the bytecode of our UFunction in a new FFrame, where the new FFrame contains the parameters of the processed function in the Locals pointer (the parameters are placed in Locals in the ProcessScriptFunction function).

The processing of the bytecode of the function in question in the new FFrame occurs in the same way (that is, by calling the Step method).

After the bytecode of the function in question has been processed (the pointer has encountered the EX_Return instruction), the return value of this function (if any) is placed in RESULT_PARAM (which has been around since the EX_VirtualFunction instruction), and the ProcessLocalScriptFunction function exits => the EX_VirtualFunction instruction has been processed.

Example analysis

Let's look at the following “Blueprint scheme”:

A separate instance of FFrame is created for Event Tick. Its bytecode (Code) looks like this:

Where EX_VT is EX_VirtualFunction,

EX_PE – EX_ParamsEnd,

VL – Vector Length,

GV – Get Velocity.

After receiving the parameter for the Print String function, it is called (via Invoke or by creating a new FFrame). After the Print String call is complete, either an empty byte (P_FINISH – in the case of a C++ function) or an EX_RETURN instruction (in the case of a Blueprint function) is sent.

When the Event Tick call completes, the EX_RETURN instruction is called.

Results

When we create new functions in a blueprint, their own UFunction instances are immediately initialized for them, which will then end up in FFrame (that is, UFunction now contains the bytecode of our Blueprint function) (an UFunction instance is a C++ interpretation of pure Blueprint functions). When we launch the game, Unreal Engine goes through these functions in blueprints, and for each one it will either create a new FFrame (in the case of a pure Blueprint function), or will call a special static function execName, which is initialized by the Unreal Header Tool code generator via the DECLARE_FUNCTION macro (in the case of a C++ function).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *