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.