Всем привет!

Сегодня я расскажу про такую возможно полезную для кого-то вещь, как вызов функции по её имени в Unreal Engine 5 (причем с любым возвращаемым значением и любым кол-вом аргументов у данной функции). Также будет разобрано практическое применение данного алгоритма на примере создания меню графических настроек.

Все примеры будут разобраны на C++.

Использование

Все алгоритмы вызовов функций по имени будут сводиться к получению объекта UFunction функции, которую мы хотим вызвать (поэтому данная функция обязательно должна быть помечена макросом UFUNCTION), созданию объекта типа FFrame, и вызову метода CallFunction (этот метод должен вызываться у объекта, в котором данная функция объявлена), которая и производит тот самый вызов нашей функции.

1) Вызов функции без аргументов

Для Вызываемой функции:

В .h файле:

protected:
	UFUNCTION()
	void FFramePrintFunc();

В .cpp файле:

void ATest::FFramePrintFunc()
{
	UE_LOG(LogTemp, Error, TEXT("Vizov"));
}

Код вызова (из другого класса):

void ATestVizov::FFrameCallableFunc(ATest* TestActor)
{
	auto Func = TestActor->FindFunction("FFramePrintFunc");

	FFrame FuncFFrame(TestActor, Func, nullptr);
	TestActor->CallFunction(FuncFFrame, nullptr, Func);
}

Теперь, при вызове функции FFrameCallableFunc класса ATestVizon, будет вызываться функция FFramePrintFunc класса ATest.

Разбор функции FFrameCallableFunc:

  • FindFunction() - функция, которая возвращает объект UFunction нужной нам функции по имени.

  • Конструктор FFrame: первым аргументом передаем объект, который будет вызывать нашу функцию (который имеет к ней доступ); вторым аргументом передаем объект UFunction вызываемой функции; третий аргумент предназначен для случая, когда вызываемая функция имеет кол-во аргументов большее нуля.

  • CallFunction: первым аргументом передаем созданный FFrame; во второй аргумент передастся значение, которое данная функция возвращает (если вообще возвращает); третьим аргументом передаем объект UFunction вызываемой функции

2) Вызов функции с одним аргументом

Для того, чтобы передать какой-либо аргумент в функцию при ее вызове через CallFunction, нужно чтобы передаваемая переменная был помечен макросом UPROPERTY (то есть для этой переменной существовал объект типа FProperty).

Для Вызываемой функции:

В .h файле (класса ATest):

protected:
	UFUNCTION()
	void FFramePrintFunc(int32 Prop);

В .cpp файле (класса ATest):

void ATest::FFramePrintFunc(int32 Prop)
{
	UE_LOG(LogTemp, Error, TEXT("%d"), Prop);
}

В .h файле (класса ATestVizov):

protected:
	UPROPERTY()
	int32 Prop;

Код вызова (из другого класса):

void ATestVizov::FFrameCallableFunc(ATest* TestActor)
{
	auto Func = TestActor->FindFunction("FFramePrintFunc");

	FFrame FuncFFrame(TestActor, Func, this, (FFrame*)0, GetClass()->FindPropertyByName("Prop"));
	TestActor->CallFunction(FuncFFrame, nullptr, Func);
}

Разбор функции FFrameCallableFunc:

  • Конструктор FFrame: третьим аргументом передаем указатель на объект, где хранится наша переменная, которую мы хотим передать в качестве аргумента в функцию; четвертый аргумент нам не потребуется, поэтому оставляем его значение по умолчанию; пятым аргументом передаем указатель на сам объект FProperty переменной, которой мы хотим передать в качестве аргумента в функцию.

3) Вызов функции с двумя (и больше) аргументами

Для Вызываемой функции:

В .h файле (класса ATest):

protected:
	UFUNCTION()
	int32 FFramePrintFunc(int32 Prop1, int32 Prop2);

В .cpp файле (класса ATest):

int32 FFramePrintFunc(int32 Prop1, int32 Prop2)
{
	UE_LOG(LogTemp, Error, TEXT("%d, %d"), Prop1, Prop2);
	return 5;
}

В .h файле (класса ATestVizov):

protected:
	UPROPERTY()
	int32 Prop1;

	UPROPERTY()
	int32 Prop2;

Код вызова (из другого класса):

void ATestVizov::FFrameCallableFunc(ATest* TestActor)
{
	auto Func = TestActor->FindFunction("FFramePrintFunc");

	GetClass()->FindPropertyByName("Prop1")->Next = GetClass()->FindPropertyByName("Prop2");
	FFrame FuncFFrame(TestActor, Func, this, (FFrame*)0, GetClass()->FindPropertyByName("Prop1"));

	int32 Result;
	TestActor->CallFunction(FuncFFrame, &Result, Func);
}

Разбор функции FFrameCallableFunc:

  • При передаче более одного аргумента в функцию, нужно записывать указатель на FProperty следующего аргумента в поле Next объекта FProperty текущего аргумента.

Практическое применение

Данный алгоритм удобно применять при создании меню графических настроек:

1) Создаем отдельный виджет WBP_UserSettings со списком, который будет содержать наши настройки (то есть этот список будет содержать другие виджеты, отвечающие за различные настройки):

2) Создаем виджет WBP_Setting, который будет отвечать за какую-нибудь отдельную настройку:

C++ часть данного виджета:

.h файл:

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "SettingWB.generated.h"

class UGameUserSettings;
class UButton;
class UTextBlock;
class UWidgetAnimation;

UCLASS()
class STARCRAFT_API USettingWB : public UUserWidget
{
	GENERATED_BODY()
	
	static inline const TMap<int32, FString> QualityNames = { {-1, "Custom"}, {0, "Low"}, {1, "Medium"}, {2, "High"}, {3, "Epic"}, {4, "Ultra"} };

private:
	UGameUserSettings* GameUserSettings;

// Widget Vars
protected:
	UPROPERTY(meta = (BindWidget))
	UTextBlock* SettingNameText;

	UPROPERTY(meta = (BindWidget))
	UTextBlock* SettingResult;

	UPROPERTY(meta = (BindWidget))
	UButton* SettingButton;

	UPROPERTY(meta = (BindWidget))
	UButton* SettingHoveredButton;

	UPROPERTY(meta = (BindWidgetAnim), Transient)
	UWidgetAnimation* SettingOnAnim;

	UPROPERTY(meta = (BindWidgetAnim), Transient)
	UWidgetAnimation* SettingHoveredAnim;

// Other Vars
protected:
	UPROPERTY()
	int32 NewSettingValue;

	UPROPERTY(EditInstanceOnly, BlueprintReadWrite)
	int32 MaxSettingValue;

	UPROPERTY(EditInstanceOnly, BlueprintReadWrite)
	FName SettingName;

	UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "GUS Function Names")
	FName GUSFuncGetterName;

	UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "GUS Function Names")
	FName GUSFuncSetterName;

private:
	UFUNCTION()
	void OnSettingButtonClicked();

	UFUNCTION()
	void OnSettingHoveredButtonHovered();

	UFUNCTION()
	void OnSettingHoveredButtonUnhovered();

	int32 CallGUSFuncGetter();
	void CallGUSFuncSetter();

public:
	virtual bool Initialize() override;
};

.cpp файл:

#include "UI/Menu/UserSettingsMenu/SettingWB.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/GameUserSettings.h"
#include "Components/Button.h"
#include "Components/TextBlock.h"
#include "Components/Image.h"
#include "Animation/WidgetAnimation.h"

bool USettingWB::Initialize()
{
	bool InitRes = Super::Initialize();

	if (UGameplayStatics::GetGameInstance(GetWorld()))
		GameUserSettings = UGameplayStatics::GetGameInstance(GetWorld())->GetEngine()->GetGameUserSettings();
	
	if (SettingResult && GameUserSettings)
	{
		int32 CurrentScalabilityValue = CallGUSFuncGetter();
		SettingResult->SetText(FText::FromString(*QualityNames.Find(CurrentScalabilityValue)));
	}

	if (SettingNameText && GameUserSettings)
	{
		SettingNameText->SetText(FText::FromString(SettingName.ToString()));
	}

	if (SettingButton)
		SettingButton->OnClicked.AddDynamic(this, &USettingWB::OnSettingButtonClicked);

	if (SettingHoveredButton)
	{
		SettingHoveredButton->OnHovered.AddDynamic(this, &USettingWB::OnSettingHoveredButtonHovered);
		SettingHoveredButton->OnUnhovered.AddDynamic(this, &USettingWB::OnSettingHoveredButtonUnhovered);
	}

	return InitRes;
}

int32 USettingWB::CallGUSFuncGetter()
{
	auto Func = GameUserSettings->FindFunction(GUSFuncGetterName);
	
	FFrame FuncFFrame(GameUserSettings, Func, nullptr);
	int32 CurrentSettingValue;
	GameUserSettings->CallFunction(FuncFFrame, &CurrentSettingValue, Func);

	return CurrentSettingValue;
}

void USettingWB::CallGUSFuncSetter()
{
	auto Func = GameUserSettings->FindFunction(GUSFuncSetterName);
	
	FFrame FuncFFrame(GameUserSettings, Func, this, (FFrame*)0, GetClass()->FindPropertyByName("NewSettingValue"));
	GameUserSettings->CallFunction(FuncFFrame, nullptr, Func);
}

void USettingWB::OnSettingButtonClicked()
{
	PlayAnimation(SettingOnAnim);

	int32 CurrentScalabilityLevel = CallGUSFuncGetter();
	NewSettingValue = CurrentScalabilityLevel == MaxSettingValue ? 0 : CurrentScalabilityLevel + 1;
	CallGUSFuncSetter();

	SettingResult->SetText(FText::FromString(*QualityNames.Find(CallGUSFuncGetter())));
	GameUserSettings->ApplySettings(true);
}

void USettingWB::OnSettingHoveredButtonHovered()
{
	PlayAnimation(SettingHoveredAnim);
}

void USettingWB::OnSettingHoveredButtonUnhovered()
{
	PlayAnimationReverse(SettingHoveredAnim);
}

Как можно видеть, при нажатии на кнопку SettingButton, вызывается функция CallGUSFuncSetter(), которая в свою очередь вызывает функцию-сеттер класса UGameUserSettings (отвечающую за настройку. Например SetOverallScalabilityLevel) по имени. Затем происходит обновление текстового блока SettingResult при помощи вызова функции CallGUSFuncGetter(), которая в свою очередь вызывает функцию-геттер класса UGameUserSettings (отвечающую за настройку. Например GetOverallScalabilityLevel) по имени.

3) Заполняем список виджета WBP_UserSettings виджетами WBP_Setting (попутно заполняя проперти данных виджетов WBP_Setting):

Как можно видеть, в проперти GUSFunc Getter Name и GUSFunc Setter Name мы пишем названия функции-геттера и функции-сеттера класса UGameUserSettings, отвечающих за данную настройку.

4) Результат:

Внутреннее устройство CallFunction

Хоть я уже и разбирал внутреннее устройство функции CallFuntion и класса FFrame в этой статье, но все таки некоторые тонкости работы этой функции остались за кадром. А именно, то что нас сейчас и интересует: как происходит парсинг аргументов функции из FFrame непосредственно в exec функции.

При парсинге аргумента в exec функции, вызывается один из макросов с приставкой P_GET_... (1) в котором и происходит вызов функции под названием StepCompiledIn класса FFrame.

Код функции StepCompiledIn:

FORCEINLINE_DEBUGGABLE void FFrame::StepCompiledIn(void* Result, const FFieldClass* ExpectedPropertyType)
{
	if (Code)
	{
		Step(Object, Result);
	}
	else
	{
		checkSlow(ExpectedPropertyType && ExpectedPropertyType->IsChildOf(FProperty::StaticClass()));
		checkSlow(PropertyChainForCompiledIn && PropertyChainForCompiledIn->IsA(ExpectedPropertyType));
		FProperty* Property = (FProperty*)PropertyChainForCompiledIn;
		PropertyChainForCompiledIn = Property->Next;
		StepExplicitProperty(Result, Property);
	}
}

Как можно заметить, функция разбита на две части: первая часть предназначена для чисто Blueprint функций (указатель на Code (байткод данной функции) не nullptr); вторая часть предназначена для C++ функций.

Сейчас, чтобы далеко не отходить от примеров в разделе Использование, разберем работу только второй части функции StepCompiledIn.

Как можно видеть, во второй части функции StepCompiledIn используется переменная PropertyChainForCompiledIn типа FProperty, которая и хранит наши аргументы (именно по этому мы в конструктор FFrame клали указатель типа FProperty).

Сначала значение текущего аргумента сохраняется в новосозданный указатель Property, потом указателю PropertyChainForCompiledIn присваивается адрес следующего объекта FProperty (поле Next класса FProperty), который хранит следующий по счету аргумент нашей функции, а затем вызывается функция StepExplicitProperty, которая проводит различные преобразования (2) с полем Locals класса FFrame, и тем самым достает значение переменной из данного объекта FProperty, кладя его в переменную Result.

(1) Пример одного и подобных макросов для аргумента типа bool:

#define P_GET_UBOOL(ParamName) uint32 ParamName##32 = 0; bool ParamName=false; Stack.StepCompiledIn(&ParamName##32); ParamName = !!ParamName##32;

(2) В функции StepExplicitProperty вызывается функция ContainerPtrToValuePtr класса FProperty, которая просто-напросто кастит значение указателя Locals (указатель на актора, где объявлен этот FProperty) к типу uint8*, и прибавляет к нему нужное кол-во байт, тем самым передвигая указатель Locals на место в памяти, где располагается нужное нам поле, и уже этот передвинутый указатель Locals кастится к типу, который имеет текущий рассматриваемый аргумент нашей функции.

Комментарии (1)


  1. PlatinumKiller
    11.12.2024 16:18

    Ну как сказать, спасибо, но лучше такое не показывать, а то вот яндекс до сих пор документацию через Javadoc генерит