创建异步 Blueprint 节点

在工程中有一些异步操作的需求,比如下载文件的行为,一个下载命令等待下载完成之后回调,而且要尽量避免太多的事件绑定和解绑操作。
在蓝图的节点中有一些异步的操作,比如 PlayMontage/DownloadImage 等,都是异步操作中有多个输出节点的:

我们能不能自己写一个这样的异步操作的节点呢?那必然是可以的。
可以在 Engine\Source\Runtime\UMG\Public\Blueprint\AsyncTaskDownloadImage.h 中查看 DownloadImage 节点的实现 (C++API)。
可以自己仿照这自己写一个出来:

  1. 关键就是要继承UBlueprintAsyncActionBase,这个是必须的。
  2. 然后写一个 static 的函数,返回类的指针,并用 UFUNCTIONmate标记为BlueprintInternalUseOnly="true"
  3. 声明并定义几个派发器成员,这些派发器成员就是异步的节点,也就是蓝图节点右侧的 exec 节点。

我们先来看一下 DownloadImage 的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UTexture2DDynamic;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FDownloadImageDelegate, UTexture2DDynamic*, Texture);

UCLASS()
class UMG_API UAsyncTaskDownloadImage : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()
public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTaskDownloadImage* DownloadImage(FString URL);

public:
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnSuccess;
UPROPERTY(BlueprintAssignable)
FDownloadImageDelegate OnFail;

public:
void Start(FString URL);

private:
/** Handles image requests coming from the web */
void HandleImageRequest(FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, bool bSucceeded);
};

对比上面的 DownloadImage 的节点,可以看到:
0. 首先声明了一个动态多播代理FDownloadImageDelegate,并且需要传入一个参数

  1. UAsyncTaskDownloadImage类中声明了两个事件派发器 OnSuccessOnFail,这也是蓝图节点右侧的 Exec 和参数Texture,本质都是派发器(动态多播代理)
  2. UAsyncTaskDownloadImagestatic 函数 DownloadImage 接收一个 FString 的参数,返回一个UAsyncTaskDownloadImage*,这个返回就是把派发器的执行节点在蓝图中显示出来

即:声明的动态多播的成员和该多播的参数都会显示在蓝图节点的右侧。

我自己实现了一个异步行为的操作,先在蓝图中看操作:

行为就是,先创建一个 AsyncActionObject 的对象作为后期触发异步操作的对象,然后执行 CreateAsyncTask 里面对上一步创建的 AsyncActionObject 进行事件绑定。
然后我们就可以在那个 ActionObj 调用 OnActionStart 之类的操作就可以调用 CreateAsyncTask 右侧的相关节点。
注意:我做的限制是,一个对象对应一个 Task, 如果当前传入的 AsyncActionObject 正在被其他的 task 绑定,则创建 task 会失败。
然后就是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// AsyncTask.h
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "AsyncActionObject.h"

// unreal header
#include "Array.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "AsyncTask.generated.h"

#ifndef PRINT_LOG
#define PRINT_LOG(lOG_TEXT) UKismetSystemLibrary::PrintString(this,lOG_TEXT,true,true);
#endif

#ifndef PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
#define PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO 0
#endif

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAsyncTaskDelegate,UAsyncActionObject*,ActionObj);

UCLASS(BlueprintType)
class UAsyncTask : public UBlueprintAsyncActionBase
{
GENERATED_UCLASS_BODY()

public:
UFUNCTION(BlueprintCallable, meta=( BlueprintInternalUseOnly="true" ))
static UAsyncTask* CreateAsyncTask(UAsyncActionObject* ActionObj);

public:

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnStart;

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnAbort;

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnUpdate;

UPROPERTY(BlueprintAssignable)
FAsyncTaskDelegate OnFinishd;

protected:
void StartTask(UAsyncActionObject* ActionObj);
virtual void OnActionStart(UAsyncActionObject* ActionObj);
virtual void OnActionAbort(UAsyncActionObject* ActionObj);
virtual void OnActionUpdate(UAsyncActionObject* ActionObj);
virtual void OnActionFinishd(UAsyncActionObject* ActionObj);
};

然后是实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// AsyncTask.cpp
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.

#include "AsyncTask.h"
#include "Modules/ModuleManager.h"

//----------------------------------------------------------------------//
// UAsyncTask
//----------------------------------------------------------------------//

UAsyncTask::UAsyncTask(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
if (HasAnyFlags(RF_ClassDefaultObject) == false )
{
AddToRoot();
}
}

UAsyncTask* UAsyncTask::CreateAsyncTask(UAsyncActionObject* ActionObj)
{
UAsyncTask* AsyncActionTask = NewObject<UAsyncTask>();
AsyncActionTask->StartTask(ActionObj);
return AsyncActionTask;
}

void UAsyncTask::StartTask(UAsyncActionObject* ActionObj)
{
if(ActionObj && !ActionObj->Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::StartTask Bind Event"));
#endif
(ActionObj->OnStart).BindUObject(this,&UAsyncTask::OnActionStart);
(ActionObj->OnAbort).BindUObject(this,&UAsyncTask::OnActionAbort);
(ActionObj->OnUpdate).BindUObject(this,&UAsyncTask::OnActionUpdate);
(ActionObj->OnFinishd).BindUObject(this,&UAsyncTask::OnActionFinishd);
}
}

void UAsyncTask::OnActionStart(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnStart"));
#endif
OnStart.Broadcast(ActionObj);
}
void UAsyncTask::OnActionAbort(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionAbort"));
#endif
OnAbort.Broadcast(ActionObj);
}
void UAsyncTask::OnActionUpdate(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionUpdate"));
#endif
OnUpdate.Broadcast(ActionObj);
}
void UAsyncTask::OnActionFinishd(UAsyncActionObject* ActionObj)
{
#if PRINT_ASYNC_ACTION_TOOLS_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncTask::OnActionFinishd"));
#endif
OnFinishd.Broadcast(ActionObj);
}

AsyncActionObject 类的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// AsyncActionObject.h
// Fill out your copyright notice in the Description page of Project Settings.
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

// unreal header
#include "Array.h"
#include "Kismet/KismetSystemLibrary.h"
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "AsyncActionObject.generated.h"

#ifndef PRINT_LOG
#define PRINT_LOG(lOG_TEXT) UKismetSystemLibrary::PrintString(this,lOG_TEXT,true,true);
#endif

#ifndef PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
#define PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO 0

#endif


class UAsyncActionObject;

DECLARE_DELEGATE_OneParam(FAsyncActionDelegate,UAsyncActionObject*);

UCLASS(BlueprintType,Blueprintable)
class UAsyncActionObject : public UObject
{
GENERATED_BODY()
public:
UAsyncActionObject(const FObjectInitializer& objectInitializer);
FAsyncActionDelegate OnStart;
FAsyncActionDelegate OnAbort;
FAsyncActionDelegate OnUpdate;
FAsyncActionDelegate OnFinishd;

public:
UFUNCTION(BlueprintCallable)
virtual bool OnActionStart(FString& rReason);
UFUNCTION(BlueprintCallable)
virtual void OnActionAbort();
UFUNCTION(BlueprintCallable)
virtual void OnActionUpdate();
UFUNCTION(BlueprintCallable)
virtual void OnActionFinishd();

public:
// Action status (BP Executable function)
UFUNCTION(BlueprintCallable)
virtual bool Action_IsRunning()const;
UFUNCTION(BlueprintCallable)
virtual bool Action_ExecutableStart()const;

protected:
// End Action
virtual void EndAction();
virtual void UnBindAll();

virtual void InitDelegateList();
virtual bool Action_IsStarted()const;
virtual bool Action_EventIsBinded()const;

protected:
bool mActionStarted=false;
TArray<FAsyncActionDelegate*> DelegateList;
};

以及它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// AsyncActionObject.cpp
// Fill out your copyright notice in the Description page of Project Settings.
// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
// Fill out your copyright notice in the Description page of Project Settings.

#include "AsyncActionObject.h"


UAsyncActionObject::UAsyncActionObject(const FObjectInitializer& objectInitializer)
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::UAsyncActionObject"));
#endif
InitDelegateList();
}

bool UAsyncActionObject::OnActionStart(FString& rReason)
{
rReason.Reset();
bool local_bResault=false;

if(Action_ExecutableStart())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionStart"));
#endif
mActionStarted=true;
OnStart.ExecuteIfBound(this);

local_bResault=true;
}else{

#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("call UAsyncActionObject::OnActionStart Faild."));
#endif

local_bResault=false;
if(Action_IsStarted())
{

#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("StartFaild: Action is Started."));
#endif

rReason.Append(FString(TEXT("Action is Started.\n")));
}
if(!Action_EventIsBinded())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("StartFaild: Action is not bind anything event."));
#endif
rReason.Append(FString(TEXT("Action is not bind to anything Task")));
}
}
return local_bResault;
}

void UAsyncActionObject::OnActionAbort()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionAbort"));
#endif
OnAbort.ExecuteIfBound(this);
EndAction();
}
}

void UAsyncActionObject::OnActionUpdate()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionUpdate"));
#endif
OnUpdate.ExecuteIfBound(this);
}
}
void UAsyncActionObject::OnActionFinishd()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::OnActionFinishd"));
#endif
OnFinishd.ExecuteIfBound(this);
EndAction();
}
}

void UAsyncActionObject::EndAction()
{
if(Action_IsRunning())
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::EndAction"));
#endif
UnBindAll();
mActionStarted=false;
}
}

void UAsyncActionObject::UnBindAll()
{
#if PRINT_ASYNC_ACTION_OBJ_DEBUG_INFO
PRINT_LOG(TEXT("UAsyncActionObject::UnBindAll"));
#endif
for(auto& DeleIndex:DelegateList)
{
if(DeleIndex->IsBound())
{
DeleIndex->Unbind();
}
}
}

void UAsyncActionObject::InitDelegateList()
{
if(!Action_IsStarted())
{
DelegateList.AddUnique(&OnStart);
DelegateList.AddUnique(&OnAbort);
DelegateList.AddUnique(&OnUpdate);
DelegateList.AddUnique(&OnFinishd);
}
}

bool UAsyncActionObject::Action_EventIsBinded()const
{
bool EventIsBinded=true;
for(auto& DeleIndex:DelegateList)
{
if(!DeleIndex->IsBound())
EventIsBinded=false;
}
return EventIsBinded;
}

bool UAsyncActionObject::Action_IsStarted()const
{
return mActionStarted;
}

bool UAsyncActionObject::Action_IsRunning()const
{
return Action_IsStarted() && Action_EventIsBinded();
}

bool UAsyncActionObject::Action_ExecutableStart()const
{
return !Action_IsStarted() && Action_EventIsBinded();
}

其实就是本质上裹了两层派发器而已…
举个例子的用途:可以在行为树里监听某个动画被终止或者结束了之后然后再执行其他的行为,可以解决不同模块之间之间的耦合。