суббота, 20 января 2024 г.

Работа с Blueprint-перечислениями (UUserDefinedEnum) из кода на С++ Unreal Engine 5.2+

    Недавно прилетела нетривиальная задача – хотелось дёргать и работать со значениями перечисления, созданного в редакторе UE для блюпринтов.  Навскидку удалось отказаться от этого, просто переведя это само перечисление на плюсы, но осадок остался.  Сел разбираться.

    В документации ясно, что все перечисления из БП имеют тип UUserDefinedEnum, но если прописать UUserDefinedEnum* аргументом в открытом для блюпринтов коде C++, то окажется, что это ассет со всем блюпринтовым Enum   и метод будет принимать на входе сам тип, а не то, что нужно.  Конечно, такое удобно для определённых целей, но всё же, хочется принимать значения.

    Чтобы протестировать то, как же изнутри выглядят такие переменные, я попробовал создать в БП свойство блюпринтового перечисления, найти его через рефлексию и достать оттуда класс.  И оказалось… Там, по сути, обычный FByteProperty.

    Получить его  можно через код вида:

// this – это объект блюпринтового класса, где такое
// свойство есть (в примере это TestEnum) 
UClass* Me = this->GetClass();
if (FProperty* Prop = Me->FindPropertyByName(TEXT("TestEnum")))
{
	if (Prop->GetClass()->GetName() == TEXT("ByteProperty"))
	{
		const FByteProperty* ByteProp = static_cast<FByteProperty*>(Prop);
		uint8 Value = 0;
		ByteProp->GetValue_InContainer(this, &Value);
		// Какой-нибудь вывод значения
		// GEditor->AddOnScreenDebugMessage(
		//     0, 500, FColor::Red, FString::FormatAsNumber(Value)
		// );
	}
}

    Более-менее разбирающийся читатель сейчас уже лезет за вилами и топором – как же так,  мы тут Cast не используем. А вот почему – вот  FProperty,  а вот FByteProperty . Классы связаны, но эта "склейка" иерархий классов возникает где-то на TProperty из-за  чего, судя по всему, компилятор путается и  Cast не работает. Замечу, что это какой-то косяк пятой версии, а, возможно, и самой студии, в четвёртой всё было норм.  Ну или шаблонная магия всё поломала. Опять же, отладчик показывает именно такое. Value, понятное дело, будет содержать числовое значение перечисления.

    Кстати, установка свойства работает аналогично:

const uint8 Value = 1;
ByteProp->SetValue_InContainer(this, Value);

    Чтобы получить более-менее внятное текстовое значение перечисления нужно что-то вида:


// Дорогая операция, лучше закешировать, например передать перечисление аргументом
// PATH_TO_BLUEPRINT_ENUM –  путь к перечислению, можно достать
// через Copy Reference в Content Browser
const UObject* Obj = StaticLoadObject(
	UUserDefinedEnum::StaticClass(), 
	this, 
	TEXT("PATH_TO_BLUEPRINT_ENUM")
); 
if (Obj)
{
	if (const UUserDefinedEnum* UDE = Cast<UUserDefinedEnum>(Obj))
	{
		// Проверка значения на валидность
		if (Value >= 0 && Value < UDE->GetMaxEnumValue())
		{
			// тут будет что-то типа 
			// <имя перечисленияя>::NewEnumerator0
			const FName Name = UDE->GetNameByValue(Value); 
			// а тут реальное, отображаемое в редакторе, 
			// имя значения перечисления
			const FText DisplayName = UDE->GetDisplayNameTextByValue(Value); 
			GEditor->AddOnScreenDebugMessage(
				0, 500, FColor::Red, Name.ToString() + DisplayName.ToString()
			);
		}
		else
		{
			LogRuntimeWarning(FText::FromString(TEXT("Invalid enum value passed")));
		}
	}
}


  Стоит отметить, что при таких условиях нет никакого нормального сильно типобезопасного способа передать в плюсовый код значение перечисления БП или вернуть его. Но опять же, при таких условиях можно тупо передавать целые числа, используя  ToInteger, а возвращать их через преобразование инта применяя Utilities/Enum/Byte To <имя перечисления>, обернув для проверки корректности вызов в блюпринтовую функцию.

    В общем, это сложновато, но в принципе, возможный сценарий решения таких проблем. Но C++-перечисления работают все же лучше, конечно.

Комментариев нет:

Отправить комментарий