Как вызвать функцию, имея только ее имя в Unreal Engine

от автора

Всем привет!

Сегодня я расскажу про такую возможно полезную для кого-то вещь, как вызов функции по её имени в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *