Предисловие
Если вы давно работаете с 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!