shangs00 2019-12-14
目录
大家都知道,蓝图是UE4提供的极其容易上手的一种可视化脚本,更具体的就不说了。
纯靠蓝图搭建的UE4游戏是存在的,但是这类游戏往往优化很差(除非游戏玩法本身的性能需求不高)。更合适的流程往往需要程序员编写C++代码创建一些蓝图可用元素,而设计师再通过蓝图快速搭建游戏。
蓝图命名:"BP"+类别缩写+"_"+名字
例如: BPA_Player
蓝图类别 | 前缀 |
---|---|
蓝图Actor | BPA_ |
蓝图结构 | BPS_ |
蓝图枚举 | BPE_ |
蓝图接口 | BPI_ |
蓝图函数库 | BFL_ |
蓝图宏库 | BML_ |
在Project Settings -》 Packaging -》 Experimental 下面有个选项:Nativize Blueprint Assets
如果勾选这个选项,那么在打包时会将蓝图脚本编译成C++代码。
C++中常常使用UE4中的一些宏来设置想要暴露于蓝图的类、属性、方法等(实质内部是UE4的反射机制)。
一般使用UCLASS([specifiers])暴露类至蓝图,并添加头文件#include "XXX.generated.h",在类里第一行使用GENERATED_BODY()。
UCLASS([specifier, specifier, ...], [meta(key=value, key=value, ...)]) class ClassName : public ParentName { GENERATED_BODY() }
常用[specifiers]参数:
至于meta(元数据说明符)参数,主要是规范(限制)用,相对没有specifiers常用。此处不做多讲,下文同理,感兴趣可以参考具体官方文档。
//例子: #include "AMyActor.generated.h" UCLASS(Blueprintable) class AMyActor : public AActor { GENERATED_BODY() };
使用UPROPERTY([specifiers])宏暴露属性至蓝图。
UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)]) Type VariableName;
常用[specifier]参数:
此处注意EditAnyWhere和BlueprintReadWrite的区别,前者表示在虚幻编辑器中可以在“属性”窗口中对该属性值进行编辑。然而若需要在蓝图脚本编辑器中设置该属性,则需要使用BlueprintReadWrite,相当于为该属性自动添加了get和set方法。
//例子: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Character") float health;
使用UPROPERTY([specifiers])宏暴露属性至蓝图,如:
UFUNCTION([specifier, specifier, ...], [meta(key=value, key=value, ...)]) ReturnType FunctionName([Parameter, Parameter, ...]);
常用[specifier]参数:
注意:对于BlueprintNativeEvent函数,需要一些特殊处理:
// 例子: // 头文件 UFUNCTION(BlueprintNativeEvent) void CountdownHasFinished(); virtual void CountdownHasFinished_Implementation(); // 源文件 void ACountdown::CountdownHasFinished_Implementation() { CountdownText->SetText(TEXT(“Go!”)); } void ACountdown::BeginPlay() { Super::BeginPlay(); CountdownHasFinished(); }
结构体可包含变量,包括UProperty变量、函数和运算符,且只需用USTRUCT([specifiers])标记该结构体和内置一句GENERATED_USTRUCT_BODY(),无需继承。
USTRUCT([Specifier, Specifier, ...]) struct StructName { GENERATED_USTRUCT_BODY() };
常用[specifier]参数:
与UObject不同的是,UStruct不会被垃圾回收,必须自行管理其生命周期。UStruct应该是纯传统数据类型,包含UObject反射支持,可以在虚幻编辑器、蓝图操控、序列化、联网等中编辑。
//例子:结构体 USTRUCT(BlueprintType) struct FCharcterStatus { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Character") float health; };
枚举则直接用UENUM([specifiers])标记。
//例子:枚举 UENUM(Meta = (Bitflags)) enum class EColorBits { ECB_Red, ECB_Green, ECB_Blue };
接口类有助于确保一组(可能)不相关的类实现一组通用函数。在某些游戏功能可能被大量复杂而不同的类共享的情况下,这非常有用。
例如,某个游戏可能有这样一个系统,依靠该系统输入一个触发器体积可以激活陷阱、警告敌人或向玩家奖励点数。这可以通过针对陷阱、敌人和点数奖励器执行“ReactToTrigger”函数来实现。
然而,陷阱可能派生自“AActor”,敌人可能派生自专门的“APawn”或“ACharacter”子类,点数奖励可能派生自“UDataAsset”。所有这些类都需要共享功能,但它们没有除“UObject”之外的共同上级。在这种情况下,推荐使用接口。
UINTERFACE([specifier, specifier, ...], [meta(key=value, key=value, ...)]) class UClassName : public UInterface { GENERATED_BODY() };
常用[specifier]参数:
//例子: #include "ReactToTriggerInterface.generated.h" UINTERFACE(Blueprintable) class UReactToTriggerInterface : public UInterface { GENERATED_BODY() };
一个将蓝图和C++结合的常见方案是先大量使用蓝图制作项目,后续再用C++把复杂的蓝图重写一遍,从而提升优化效果。
然而现在蓝图有Nativize Blueprint Assets的功能,可以将蓝图编译成C++代码。所以个人认为绝大部分蓝图并不需要重写,直接Nativize Blueprint Assets即可。而对于某些包含复杂逻辑计算的蓝图,则才适合用C++来替代蓝图。
推荐看参考里面的[UE4]使用C++重写蓝图,SpawnObject根据类型动态创建UObject博客来加深理解。
创建一个C++类作为蓝图的父类(C++类继承蓝图一样的父类),在UE4中修改蓝图的父类。
用C++中实现好的方法逐个替换蓝图中方法,每次替换一个方法就必须要运行游戏进行详细测试,防止修改太多万一出错无法定位问题所在(尽量避免出现要同时替换2个以上蓝图方法才能正常运行游戏。这一点非常重要。同样也是防止修改太多万一出错无法定位问题所在)
如下图所示:保留原蓝图的实现,方便C++代码查错。
创建一个继承自UObject的C++类,一般加后缀Helper,并且加上BlueprintType标签,共蓝图作为变量类型使用。
在蓝图中添加一个名为MyHelper的变量,类型是第一步创建的C++类型。
在使用helper对象之前,必须先实例化。接着要初始化helper的成员变量值。其中当前蓝图对象的引用(也就是self)要传递给me参数,这是关键,用helper的成员对象保存起来。
最终用helper对象的C++方法一个一个替换原来用蓝图写的功能。
两个方案都要注意的事项:
- C++类中的方法、成员变量与蓝图一一对应,并且方法和成员变量名称不能与蓝图的重复。
- 在替换蓝图的过渡时期,代表相同含义的蓝图变量和C++类变量可能同时存在。那么给变量赋值时,应注意2个变量都要同时赋值,以保证蓝图方法和C++方法都能正常运行。
- A蓝图不能直接使用B蓝图的变量,A蓝图把要公开的变量封装在函数内返回,并且只返回UE4自带的基础变量类型,不能返回自定义类型,以方便C++重写时返回C++中的成员变量。
参考博客原文的结论是:使用继承和组合都可以实现C++重写蓝图,但是组合比继承要更好,耦合度更低。
虚幻引擎4 官方文档 | 中文文档 | 虚幻架构 创建和实现游戏性类的参考
虚幻引擎4 官方文档 | 英文文档 | 虚幻架构 创建和实现游戏性类的参考
[UE4]使用C++重写蓝图,SpawnObject根据类型动态创建UObject
系列其他文章:Aery的UE4 C++开发之旅系列文章