EN

Unreal Engine: как объявить пользовательское событие в C++

Также доступно на английском

Этот туториал покажет, как объявить пользовательское событие в C++ и сделать его доступным в Blueprints.

Дисклеймер

Я новичок в Unreal Engine. В какой-то момент мне понадобилось объявить событие в C++ так, чтобы оно было доступно в Blueprints.

Я потратил на это больше времени, чем ожидал — вокруг темы много путаницы. Поэтому решил написать эту небольшую статью.

Зачем нужны пользовательские события?

Пользовательские события в Unreal Engine — это способ оповестить другие части кода о том, что что-то произошло. Например, изменилось значение переменной, игрок подобрал предмет или персонаж получил урон. Подписчики события получают уведомление и могут на него отреагировать.

Простой пример: есть ActorComponentUSHealthAttributeComponent — с переменной Health. При её изменении хочется:

  • Обновить полоску здоровья в UI.
  • Запустить анимацию смерти, когда Health упадёт до нуля.

Откуда берётся путаница

Официальная документация по объявлению пользовательских событий предлагает использовать макрос DECLARE_EVENT.

Моя первая попытка объявить пользовательское событие выглядела так:

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class GAME_API USHealthAttributeComponent : public UActorComponent
{
...
public:
/**
* Событие, вызываемое при изменении Health.
* Принимает четыре параметра:
* - AActor* - указатель на Instigator (того, кто вызвал изменение здоровья, может быть nullptr)
* - UPawnAttributeComponent* - указатель на компонент, вызвавший событие
* - float - Новое значение здоровья
* - float - Старое значение здоровья
*/
DECLARE_EVENT_FourParams(USHealthAttributeComponent, FOnHealthChanged,
AActor* /* Instigator */,
UPawnAttributeComponent* /* AttributeComponent */,
float /* NewHealthValue */,
float /* NewHealth */
)
FOnHealthChanged OnHealthChanged;
}

Я потратил время, пытаясь понять, почему моё событие не отображается в Blueprint-е, и почему я не могу на него подписаться. Попробовал применить макрос UPROPERTY:

// `BlueprintAssignable` делает свойство доступным для назначения в Blueprints.
UPROPERTY(BlueprintAssignable)

Но UPROPERTY нельзя использовать вместе с DECLARE_EVENT — это даёт ошибку компиляции.

Тогда я начал искать другие способы объявления пользовательских событий и проверил форумы. Многие задают тот же вопрос, и все ответы указывают на макрос DECLARE_DYNAMIC_MULTICAST_DELEGATE.

Я ещё не разобрался в типах делегатов, поэтому не придал значения выбору макроса и попробовал DECLARE_MULTICAST_DELEGATE:

DECLARE_MULTICAST_DELEGATE_FourParams(
FOnHealthChanged,
AActor*,
USAttributeComponent*,
float,
float
);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class GAME_API USHealthAttributeComponent : public UActorComponent
{
...
public:
UPROPERTY(BlueprintAssignable)
FOnHealthChanged OnHealthChanged;
}

И снова ошибка компиляции. Пора разобраться глубже.

Делегаты и события

Что такое делегат? Это просто указатель на функцию, который можно присвоить переменной и вызвать позже. В Unreal Engine можно определить несколько типов делегатов. Опишу самые распространённые.

Для начала разберёмся с сериализацией.

Сериализация

Что значит быть сериализуемым? Это значит, что объект можно преобразовать в байты, сохранить в файл и восстановить позже.

Применительно к делегатам: граф событий со всеми связями можно сохранить в файлы проекта и восстановить при его открытии.

Нединамические и динамические делегаты

Делегаты бывают нединамические и динамические.

Нединамические нельзя использовать в Blueprints — они несериализуемы. Зато они быстрее.

Multicast и single-cast делегаты

Другое деление — по способу рассылки: multicast и single-cast.

  • У multicast-делегатов можно назначить сколько угодно получателей.
  • У single-cast-делегатов можно назначить только одного получателя; каждый раз при назначении нового предыдущий будет заменён.

События

События — особый тип делегатов, привязанных к конкретному OwningType. Отправить событие можно только изнутри этого типа. События тоже бывают динамическими/нединамическими, multicast/single-cast.

Главный недостаток: события не передают аргументов подписчикам — только сам факт того, что что-то произошло.

Также, если посмотреть исходный код макроса DECLARE_EVENT, можно найти примечание:

“NOTE: This behavior is not enforced, and this type should be considered deprecated for new delegates, use normal multicast instead”

Как объявить пользовательское событие?

Теперь можно собрать всё вместе. Нам нужно:

  • Оповестить нескольких подписчиков об изменении Health.
  • Передать данные: кто вызвал изменение, новое и старое значение.
  • Использовать событие в Blueprints.

Итак, нам нужен динамический multicast-делегат с четырьмя параметрами. Используем макрос DECLARE_DYNAMIC_MULTICAST_DELEGATE:

DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(
FOnHealthChanged, // Имя структуры, которая будет сгенерирована
// Параметры делегата (Тип, Имя):
AActor*, Instigator, // Кто вызвал изменение здоровья
USHealthAttributeComponent*, ChangedComponent, // Компонент, вызвавший событие
float, NewValue, // Новое значение здоровья
float, OldValue // Старое значение здоровья
);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class GAME_API USHealthAttributeComponent : public UActorComponent
{
...
public:
UPROPERTY(BlueprintAssignable)
FOnHealthChanged OnHealthChanged;

Вот и всё. Теперь мы можем использовать наше пользовательское событие в Blueprints. Чтобы его отправить, нужно использовать метод Broadcast:

void USHealthAttributeComponent::SetHealth(float NewHealth, AActor* Instigator = nullptr)
{
if (NewHealth != Health)
{
const float OldHealth = Health;
Health = NewHealth;
OnHealthChanged.Broadcast(Instigator, this, NewHealth, OldHealth);
}
}

Дополнительная информация

Если хотите узнать больше о делегатах, рекомендую прочитать статью Ben UI Advanced Delegates.