Всем привет!
Сегодня я расскажу про такую возможно полезную для кого-то вещь, как вызов функции по её имени в 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 кастится к типу, который имеет текущий рассматриваемый аргумент нашей функции.
PlatinumKiller
Ну как сказать, спасибо, но лучше такое не показывать, а то вот яндекс до сих пор документацию через Javadoc генерит