Всем привет!
Сегодня я расскажу про такую возможно полезную для кого-то вещь, как вызов функции по её имени в 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 кастится к типу, который имеет текущий рассматриваемый аргумент нашей функции.
ссылка на оригинал статьи https://habr.com/ru/articles/865524/
Добавить комментарий