Как стать автором
Обновить

Асинхронность в blueprints и Unreal Engine

Уровень сложностиПростой
Время на прочтение3 мин
Количество просмотров1.8K

Предисловие

Если вы давно работаете с unreal engine, то точно должны знать, что в движке есть различные ноды, которые можно вызвать сейчас, а получить результат функции потом, да еще и продолжить логику, когда функция выполнится.

Встроенные асинхронные ноды
Встроенные асинхронные ноды

Но, при попытке создать свои, вы обнаружите, что "провалиться" в их логику не получается. Так как же тогда быть? В этом нам поможет класс UBlueprintAsyncActionBase. Именно благодаря ему можно создавать функции, которые можно вызвать как отдельные асинхронные ноды.


Делаем свои асинхронные ноды

Итак, для примера, мы хотим создать 2 ноды, каждая из которых будет возвращать bool результат выполнения, а также иметь execution выход, который будет активироваться по завершению функций. Для этого нам надо создать новый C++ класс, который будет наследоваться от UBlueprintAsyncActionBase. Для примера, назовем его UAsyncAction.

Код из .h файла:

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncAction.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncDone, const bool, WasSuccessful);

/**
 * 
 */
UCLASS()
class YOUR_MODULE_API UAsyncAction : public UBlueprintAsyncActionBase
{
    GENERATED_BODY()

public:
    // Наш асинхронный execution выход
    UPROPERTY(BlueprintAssignable)
    FOnAsyncDone OnAsyncDone;

    // Первая асинхронная нода
    UFUNCTION(BlueprintCallable, BlueprintInternalUseOnly)
    static UAsyncAction* FirstAsyncFunction(UObject* Object);

    // Вторая "асинхронная" нода
    UFUNCTION(BlueprintCallable, BlueprintInternalUseOnly)
    static UAsyncAction* SecondAsyncFunction(const TArray<int32> IntArray);

protected:
    virtual void Activate() override;

private:
    TWeakObjectPtr<> WeakObject;
    TArray<int32> Array;
    uint8 bFirstFunction : 1;
};

Вызываемые функции

Начнем с того, что мы написали 2 статик функции, которые возвращают нам ссылку на наш же класс. Но ведь мы хотим возвращать bool результат. Все очень просто, это наши "конструкторы". Как можно заметить, мы сделали их BlueprintInternalUseOnly. Это важно, т.к. те функции, что мы будем вызывать, движок сделает за нас. Нам лишь надо показать ему сигнатуры.

Возвращаем результат

Хорошо, а как нам вернуть то, что мы хотим? Для этого мы создали делегат FOnAsyncDone OnAsyncDone. Каждый наш созданный делегат будет выходом для всех наших "функций-конструкторов". Благодаря делегату у нас будет execution выход OnAsyncDone, который будет возвращать булево значение WasSuccessful.

Главная функция

Мы создали наши вызываемые функции, дали им нужный выход. Но, раз это конструкторы, то где у нас будет логика? У класса UBlueprintAsyncActionBase есть, по сути, лишь 1 интересующая нас функция – Activate(). Она является нашим "BeginPlay()". В ней и будет находиться логика всех наших созданных функций. Да именно так. Всех.

Кэш

Т.к. реализация у нас будет в Activate(), а вызывать мы будем наши статик функции, то нам надо сохранить аргументы, потому добавим соответствующие поля.

Реализация

Код из .cpp файла:

// Fill out your copyright notice in the Description page of Project Settings.


#include "AsyncAction.h"

UAsyncAction* UAsyncAction::FirstAsyncFunction(UObject* Object)
{
	const auto Action = NewObject<UAsyncAction>();
	Action->WeakObject = Object;
	Action->bFirstFunction = true;

	return Action;
}

UAsyncAction* UAsyncAction::SecondAsyncFunction(const TArray<int32> IntArray)
{
	const auto Action = NewObject<UAsyncAction>();
	Action->Array = IntArray;
	Action->bFirstFunction = false;

	return Action;
}

void UAsyncAction::Activate()
{
	Super::Activate();

	if (bFirstFunction)
	{
		AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask,
		          [&]
		          {
			          FPlatformProcess::Sleep(3.0f);
			          OnAsyncDone.Broadcast(WeakObject.IsValid());
		          	  SetReadyToDestroy();
		          });
	}
	else
	{
        FPlatformProcess::Sleep(3.0f);
		OnAsyncDone.Broadcast(Array.Num() > 0);
		SetReadyToDestroy();
	}
}

Как видно, в наших статик функциях мы просто создаем экземпляр нашего класса и передаем в него наши аргументы. Раз у нас 2 функции, а реализациях их в одном месте, то используем флаг bFirstFunction для их разделения.

Также, нужно вызвать SetReadyToDestroy(), если наша логика завершена. Это важно, т.к. каждый вызов мы создаем uobject нашего класса, который должен быть удален сборщиком мусора по завершении логики.


Итог

Итак, получается, вызов первой функции у нас должен возвращать валидность переданного указателя на объект через 3 секунды, а вторая, возвращать, пустой ли переданный массив.

Вызов первой ноды
Вызов первой ноды
Результат первой ноды
Результат первой ноды

Как видно, первая функция работает, и, при этом, асинхронно.

Асинхронность

Вызов второй ноды
Вызов второй ноды
Результат второй ноды
Результат второй ноды

Как можно заметить, "асинхронный" выход второй ноды отработал раньше, чем принт Function started. Именно потому я и пометил вторую ноду как асинхронную в кавычках. Если вы вызываете латентную(а именно такой и является данный тип нод) ноду, то, если она работает не асинхронно, то отработает она как обычная функция. А именно, перед вызовом следующей за ней логики.

Теперь вы умеете создавать латентные асинхронные ноды в unreal engine!

Теги:
Хабы:
+3
Комментарии1

Публикации

Истории

Работа

Программист C++
115 вакансий
QT разработчик
9 вакансий

Ближайшие события

25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань