目录
蓝图 是UE4中引入的一个强大新功能,是创建新UClasses的一种方法,无需书写或编译任何代码;当创建一个蓝图时,可以选择扩展C++类或另一个蓝图类;然后,可添加、排列及自定义Components,使用可视化的脚本语言实现自定义的逻辑,对Event及交互做出反应,定义变量,处理输入,及创建一种完全自定义的对象类型;
每个蓝图都具有一个Construction Script(C++中的构造函数类似),创建对象时运行它;该脚本可动态地基于多个因素构建Actor实例,如,一个可以自动调整大小来填充建筑物间空隙的篱笆;从这个角度来说,蓝图也可以看成是一个非常强大的预制系统;
一,蓝图函数库
在开发过程中,通常会发现需要通过函数集来简化项目开发;这些函数通常无所属,可在多种游戏代码之间重复使用;通过蓝图编译时通常也会需要这些函数;如需要公开共享效用函数,不想将它们和一种特定的游戏对象类型捆绑在一起;对于这类情况,使用 蓝图函数库 ;
蓝图函数库是一个静态函数的合集,提供不与特定游戏对象绑定的功能;这些库可群组化为逻辑函数集(如 AI 蓝图库),或包含提供多种不同功能区域(如系统蓝图库)访问的函数;
创建蓝图函数库与使用 UFUNCTION() 宏对蓝图公开函数十分相似;所有蓝图类均继承自 UBlueprintFunctionLibrary,而非派生自 Actor 或 UObject,只应包含静态方法;
UCLASS()
class UAnalyticsBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_UCLASS_BODY()
/** Starts an analytics session without any custom attributes specified */
UFUNCTION(BlueprintCallable, Category = "Analytics")
static bool StartSession();
蓝图函数库为 UObject 非直接派生,需要标准 UCLASS() 和 GENERATED_UCLASS_BODY() 宏;还将在可从蓝图进行调用的函数上添加 UFUNCTION() 宏;蓝图函数库中的函数可被设计为 BlueprintCallable 或 BlueprintPure,取决于调用是否存在副作用;
//函数实现
bool UAnalyticsBlueprintLibrary::StartSession()
{
TSharedPtr<IAnalyticsProvider> Provider = FAnalytics::Get().GetDefaultConfiguredProvider();
if (Provider.IsValid())
{
return Provider->StartSession();
}
else
{
UE_LOG(LogAnalyticsBPLib, Warning, TEXT("StartSession: Failed to get the default analytics provider. Double check your [Analytics] configuration in your INI"));
}
return false;
}
AAIController* UAIBlueprintHelperLibrary::GetAIController(AActor* ControlledActor)
{
APawn* AsPawn = Cast<APawn>(ControlledActor);
if (AsPawn != nullptr)
{
return Cast<AAIController>(AsPawn->GetController());
}
return Cast<AAIController>(ControlledActor);
}
二,蓝图编译器
与普通C++类一样,蓝图在使用前需要进行编译;当单击 编译(Compile)按钮时,系统将把蓝图资源的属性和图表转换为类;
术语
- $ FKismetCompilerContext,执行编译工作的类,系统为每次编译生成一个新实例,存储正在编译的类、蓝图等的引用;
- $ FKismetFunctionContext,保存用于编译单个函数的信息,如对关联图表、属性和生成的UFunction的引用;
- $ FNodeHandlingFunctor,一个辅助工具类,用于处理编译器中的一个节点类(单件),包含用于注册引脚连接和生成编译语句的函数;
- $ FKismetCompiledStatement,编译器中的工作单元,编译器将节点转换为一组已编译语句,后端将这些语句转换为字节码操作;
- $ FKismetTerm,图表中的终端(文字、常量或变量引用),每个数据引脚连接都与其中一个终端关联!还可在"NodeHandlingFunctor"中为Scratch变量、中间结果等创建自己的术语;
编译过程
清理类:
类是现场编译的,意味着相同的 UBlueprintGeneratedClass 会被一次又一次地清理和重用, 因此指向类的指针不必固定;CleanAndSanitizeClass() 将属性和函数从类中移到临时包中的垃圾类中, 然后清除类中的任何数据;
创建类属性:
编译器在蓝图的 NewVariables 以及其他一些地方(构造脚本等)上进行迭代, 以查找类所需的所有UProperty,然后在函数 CreateClassVariablesFromBlueprint() 中创建UClass作用域上的UProperty;
创建函数列表:
编译器通过处理事件图表,处理函数图表和预编译函数(如为每个context调用 PrecompileFunction()) 来为类创建函数列表;
处理事件图表,由 CreateAndProcessUberGraph() 函数执行,此函数将所有事件图表复制到一个大图表中,之后节点有机会扩展;然后, 此函数为图表中的每个事件节点创建一个函数存根,并为每个事件图表创建一个 FKismetFunctionContext ;
处理函数图表,函数图表的处理由 ProcessOneFunctionGraph() 函数完成, 此函数将每个图表复制到一个临时图表中,其中节点将获得机会而展开;此函数还将为每个函数图表都创建一个 FKismetFunctionContext ;
预编译函数,函数的预编译由每个context的 PrecompileFunction() 处理;
绑定和链接类:
现在编译器已经了解类的所有UProperty和UFunction,因此可以绑定和链接该类,这包括填充属性链、属性大小、函数图等;此时,从本质上看,它具有一个类标头 -
减去最终的标记和元数据 - 以及一个类默认对象(CDO);
编译函数:
下一步是为剩余的节点生成 FKismetCompiledStatment 对象, 此操作使用 AppendStatementForNode() 通过节点处理器的 Compile() 函数完成;此函数可以在编译函数中创建 FKismetTerm 对象,但前提是这些对象仅在本地使用;
完成编译类:
为了完成编译类,编译器将确定类标记,并从父类传播标记和元数据,最后执行一些最终检查,以确保编译过程中一切正常;
后端发出生成的代码:后端将每个函数上下文中的语句集合转换为代码;
- FKismetCompilerVMBackend
- FKismetCppBackend
复制类默认对象属性:
编译器使用一个特殊的函数 CopyPropertiesForUnrelatedObjects() 将类的旧CDO中的值复制到新CDO中;属性通过标记序列化复制, 因此只要名称一致,它们就应当会被正确地传输;在此阶段,CDO的组件将被重新实例化并进行适当的修复;操作时以GeneratedClass CDO为准;
重新实例化:
由于类可能已经更改了大小,且属性可能已经过添加或删除, 因此编译器需要用刚编译的类重新实例化所有对象;这个过程使用 TObjectIterator 查找类的所有实例,生成一个新实例,然后使用 CopyPropertiesForUnrelatedObjects() 函数将旧实例复制到新实例;
三,向蓝图公开游戏元素
使用蓝图可以使代码非常灵活,从而提供许多好处;如, 游戏设计人员可能希望在游戏中实现一种新型武器;作为程序员,现在编写武器代码的方式与传统方式几乎完全相同,而不同之处仅在于公开了一些重要的功能, 比如射速和Fire()函数;在游戏测试后,设计师确定需要改变枪支的射速以呈曲线射击;设计师可以简单地进入蓝图并直接更改射速,而不是对射速重新编码并重新编译游戏,从而节省设计师和程序员的时间;
使类可蓝图化
为创建从某个类扩展而来的蓝图,必须将该类定义为 Blueprintable ; 这涉及到在类定义之前的 UCLASS() 宏中添加此关键字;此关键字使蓝图系统能够感知该类, 以便显示在 New Blueprint 对话框的类列表中,且可选择它作为所创建蓝图的父类;
//最简单的声明形式
UCLASS(Blueprintable)
class AMyBlueprintableClass : AActor
{
GENERATED_BODY()
}
关键字 | 说明 |
Blueprintable | 将该类公开为可接受的用于创建蓝图的基类;默认值为NotBlueprintable,但继承时除外;由子类继承; |
BlueprintType | 将该类公开为可用于蓝图中的变量的类型; |
NotBlueprintable | 指定此类不是用于创建蓝图的可接受基类;否定指定可蓝图化关键字的父类的效果; |
可读和可写属性
为了将C++类中定义的变量公开给从该类扩展而来的蓝图,必须使用 UPROPERTY() 宏中列出的下列关键字之一来定义该变量; 这些关键字使蓝图系统能够感知该变量,以便在 My Blueprint 面板中显示该变量, 并且可以设置或访问该变量的值;
//Character's Health
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Character")
float health;
关键字 | 说明 |
BlueprintReadOnly | 此属性可以由蓝图读取,但不能修改; |
BlueprintReadWrite | 此属性可以通过蓝图读取或写入; |
Multicast Delegate Keywords | 说明 |
BlueprintAssignable | 应公开属性以在蓝图中赋值; |
BlueprintCallable | 公开属性以在蓝图图表中调用; |
可执行和可覆盖函数
为了从蓝图调用本地函数,必须使用 UFUNCTION() 宏中列出的下列关键字之一来定义该函数;这些关键字使蓝图系统能够感知该函数,以便在上下文菜单或控制板中显示该函数,并可将其添加到图表并执行 - 或在事件的情况下,可覆盖和执行这些函数;
//最简单的声明形式
UFUNCTION(BlueprintCallable, Category="Weapon")
void Fire();
在创建函数签名时,注意,通过引用传递某一参数将使该参数成为蓝图节点上的输出引脚;若要使参数通过引用传递并仍然显示为输入,请使用"UPARAM()"宏;
UFUNCTION(BlueprintCallable, Category = "Example Nodes")
static void HandleTargets(UPARAM(ref) TArray<FVector>& InputLocations, TArray<FVector>& OutputLocations);
还可使用"UPARAM()"来更改引脚的显示名称;
/** 根据采用度数的旋转值制作一个旋转体{绕Z轴的旋转、绕X轴的旋转、绕Y轴的旋转} */
UFUNCTION(BlueprintPure, Category="Math|Rotator", meta=(Keywords="construct build rotation rotate rotator makerotator", NativeMakeFunc))
static FRotator MakeRotator(
UPARAM(DisplayName="X (Roll)") float Roll,
UPARAM(DisplayName="Y (Pitch)") float Pitch,
UPARAM(DisplayName="Z (Yaw)") float Yaw);
关键字 Blueprint to Native Communication | 说明 |
BlueprintCallable | 可从蓝图中调用的本地函数,执行本地代码,将更改所调用对象的某些内容或一些其他全局状态;意味着,必须"调度"此函数,或者明确地告诉它相对于其他节点执行的顺序;用白色的执行线来执行此操作;所有蓝图可调用函数将按照它们在白色执行行上出现的顺序调用; |
BlueprintPure | 可从蓝图中调用的本地函数,执行本地代码,不会更改它所调用对象的任何内容或任何其他全局状态;意味着调用这个节点不会改变任何东西,只需要输入,然后告诉输出;它们是诸如数学节点(+、-、*等)、或变量获取器或任何永远都不会改变任何内容之类的东西;不需要调度,也没有白色的执行线的连接;由编译器根据哪个BlueprintCallable节点需要这些节点生成的数据而自动计算出来; |
关键字 Native to Blueprint Communication | 说明 |
BlueprintImplementableEvent | 这是允许本地函数调用蓝图的主要方式;就像在蓝图中实现的虚拟函数; 如不存在实现,则该函数调用将被忽略; 必须注意的是,如BlueprintImplementableEvent没有返回值或输出参数,那么它将显示为一个事件,可通过右击并在蓝图事件图表中选择它来使用; 如有返回值或任何输出参数,将在 My Blueprints 选项卡中列出,然后可通过右单并选择实现的函数来覆盖; 注意,BlueprintImplementableEvents没有函数的本地实现; |
BlueprintNativeEvent | 除了如果蓝图没有覆盖此函数,则调用此函数的本地默认实现之外,此函数与上述函数别无二致; 如想要某种默认行为(而蓝图没有实现它),但又希望蓝图能够在需要时覆盖功能,那么此函数将大有帮助;这些函数的使用成本更高,所以只将它们用在需要功能的地方; 当覆盖BlueprintNativeEvent时,如需要,仍然可以调用本地实现,方法是在事件或函数条目节点上右击,并选择"Add call to parent"; |
四,将C++暴露给蓝图
可从两个主要方面考虑使用 C++ 或蓝图:
- 速度
- 表达式复杂度
这两因素之外,便主要取决于游戏的复杂度和团队的构成;如团队中美术师的数量多于程序员,则蓝图的优越性大于 C++ 代码;相反如程序员数量较多,则最好使用 C++;还需预计开发过程中人员的缺席;在 Epic,普遍的工作流程是 - 内容设计师创建十分复杂的蓝图后,程序员编写新蓝图节点的代码,明确如何将大量工作压缩到 C++ 中,这样就能将功能块移入一个新的 C++ 函数中;较好的方法是大量使用蓝图,复杂度达到一定程度转入 C++,使功能的表达更加简洁(否则对非程序人员而言过于复杂);
速度
实际上,蓝图执行的速度比 C++ 执行慢;这并不意味着会影响性能,但需大量计算或进行高频率操作时,最好使用 C++;也可根据团队状况和项目性能对二者进行完美结合;如蓝图功能较多,可将部分功能移入 C++ 使速度加快,蓝图中剩余的部分功能可保留灵活性;如分析表明蓝图中的一项操作耗时极长,考虑只将该部分移入 C++,其他部分仍然保留在蓝图中;
如控制上千个 Actor 的群组系统通过蓝图可视化脚本执行时间极长;在这类情况下,在 C++ 中处理决定、路径、和其他群组功能性能更佳,之后可将部分调整参数和控制函数公开到蓝图;
复杂度
就表达式复杂度而言,部分操作在 C++ 中更容易执行;蓝图可良好地执行大量操作,但部分内容不易在节点中表达;操作大型数据集、执行字符串操作,在大量数据集上执行复杂算术等内容均十分复杂,不易在视觉系统中进行操作;这些内容更适合于 C++,可轻松进行查看,了解具体细节;群组系统更适合 C++ 代码的另一个原因在于表达式复杂度;
范例
部分功能块适合在 C++ 中处理,部分适合在蓝图中处理,如:
-
程序员可在 C++ 中创建自定义事件的角色类,之后蓝图可用于该角色类的延展,实际指定模型并进行默认设置;
-
可在 C++ 中实现能力系统的基础类,之后设计师再创建实际执行操作的蓝图;
-
"收集"或"重生"函数为可在蓝图中实现的事件,这样的可拾取道具可被覆写,以便设计师生成不同粒子发射器和声效;
创建蓝图 API:提示和技巧
创建对蓝图公开的 API 时需要考虑以下几点:
-
可选参数便于在蓝图中处理:
/**
* 将字符串显示到日志中,也可选择显示到屏幕上。
* 如 Print To Log 为 true,它将显示在 Output Log 窗口中。否则它将被记录为"Verbose",通常不会显示。
*
* @param InString 登出字符串
* @param bPrintToScreen 是否将输出显示到屏幕上
* @param bPrintToLog 是否将输出保存到日志中
* @param bPrintToConsole 是否将输出显示到控制台
* @param TextColor 是否将输出显示到控制台
*/
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", CallableWithoutWorldContext, Keywords = "log print", AdvancedDisplay = "2"), Category="Utilities|String")
static void PrintString(UObject* WorldContextObject, const FString& InString = FString(TEXT("Hello")), bool bPrintToScreen = true, bool bPrintToLog = true, FLinearColor TextColor = FLinearColor(0.0,0.66,1.0));
- 带有大量返回参数的函数和返回结构体的函数之间优先前者;
UFUNCTION(BlueprintCallable, Category = "Example Nodes")
static void MultipleOutputs(int32& OutputInteger, FVector& OutputVector);
- 可在现有函数上添加新参数,但如要进行移除或变更,则需要否决原始函数并添加一个新函数;必须使用否决元数据,使新函数的信息显示在蓝图中;
UFUNCTION(BlueprintCallable, Category="Collision", meta=(DeprecatedFunction, DeprecationMessage = "Use new CapsuleOverlapActors", WorldContext="WorldContextObject", AutoCreateRefTerm="ActorsToIgnore"))
static ENGINE_API bool CapsuleOverlapActors_DEPRECATED(UObject* WorldContextObject, const FVector CapsulePos, float Radius, float HalfHeight, EOverlapFilterOption Filter, UClass* ActorClassFilter, const TArray<AActor*>& ActorsToIgnore, TArray<class AActor*>& OutActors);
- 如函数需要接受枚举,考虑将"expand enum as execs"用作元数据,可使节点更易于使用;
UFUNCTION(BlueprintCallable, Category = "DataTable", meta = (ExpandEnumAsExecs="OutResult", DataTablePin="CurveTable"))
static void EvaluateCurveTableRow(UCurveTable* CurveTable, FName RowName, float InXY, TEnumAsByte<EEvaluateCurveTableResult::Type>& OutResult, float& OutXY);
- 许多完成耗时较长的操作均为隐藏函数;
/**
* 执行带延迟的隐藏操作。
*
* @param WorldContext 世界背景。
* @param Duration 延迟长度。
* @param LatentInfo 隐藏操作。
*/
UFUNCTION(BlueprintCallable, Category="Utilities|FlowControl", meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo", Duration="0.2"))
static void Delay(UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo );
- 如有可能,考虑将函数放入共享库;便于在多个类之间使用,避开"target"引脚;
class DOCUMENTATIONCODE_API UTestBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
- 尽可能将节点标记为纯,可避免在节点上使用连线的执行引脚;
/* 在 0 和 最大 - 1 之间返回一致分配的随机数 */
UFUNCTION(BlueprintPure, Category="Math|Random")
static int32 RandomInteger(int32 Max);
- 将一个函数标记为
const
也可使蓝图节点不带执行引脚;
/**
* 获得 actor 到世界的转换。
* @return 从 actor 空间转换到世界空间的转换。
*/
UFUNCTION(BlueprintCallable, meta=(DisplayName = "GetActorTransform"), Category="Utilities|Transformation")
FTransform GetTransform() const;