Unreal Engine: как объявить пользовательское событие в C++
Этот туториал покажет, как объявить пользовательское событие в C++ и сделать его доступным в Blueprints.
Дисклеймер
Я новичок в Unreal Engine. В какой-то момент мне понадобилось объявить событие в C++ так, чтобы оно было доступно в Blueprints.
Я потратил на это больше времени, чем ожидал — вокруг темы много путаницы. Поэтому решил написать эту небольшую статью.
Зачем нужны пользовательские события?
Пользовательские события в Unreal Engine — это способ оповестить другие части кода о том, что что-то произошло. Например, изменилось значение переменной, игрок подобрал предмет или персонаж получил урон. Подписчики события получают уведомление и могут на него отреагировать.
Простой пример: есть ActorComponent — USHealthAttributeComponent — с переменной 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.