环境:Unreal5.6 语言:C++
面向:Unreal初学者
总起
蓝图在Unreal中非常重要,基本是个必学概念,它分别承担了以下的功能:
- 游戏逻辑编写(类似Unity中的C#脚本),拥有定义或调用事件、函数、变量等能力;
- 可复用的预制件(类似Unity中的Prefab);
- 动画蓝图(类似于Unity中的Animator);
- 关卡蓝图,这个其实没什么特别的,就是每个关卡独有的全局事件列表;
- 控件蓝图,将UI视觉元素与交互逻辑整合在一起。

蓝图的前身是Kismet,Unreal3时期功能较为有限,逻辑大部分还使用Unreal Script来编写。到了Unreal4,蓝图正式推出,整合了前两者的能力。
因此如果程序员想脱离蓝图,纯用C++开发会痛苦无比。
当然最近我有了解到现在AI编程针对蓝图的适配有限(因为现在的AI编程本质就是做文本分析嘛),所以有人尝试让AI直接生成C++代码,似乎结果还不错,不过C++这个编译使用起来还是比较麻烦的......(想念C# 1秒钟)
自定义蓝图节点
说回到当前的一个需求:想要定义一个两个Int值相加的节点。
首先Unreal中本身自带了一个 Add 节点,并且支持任意类型的相加:

其次是使用 UFUNCTION,直接在C++中定义一个函数,这应该最常用的方式了:
cpp
// .h文件
UFUNCTION(BlueprintPure, Category = "MyTestActor|Math", meta = (DisplayName = "Actor Static Add"))
static int32 StaticMyAddInts(int32 a, int32 b);
// .cpp文件
int32 AMyTestActor::StaticMyAddInts(int32 a, int32 b)
{
return a + b;
}
这里需要注意一些细节:
- BlueprintPure,无执行引脚,需要有返回值(区别于BlueprintCallable有执行引脚);
- Category描述是右键添加节点时的分类;
- DisplayName就是显示的名字。
结果:

最后是继承于 K2Node,完全自定义节点,灵活度最高,也是最麻烦的一种。好处是可以实现动态引脚、完全自定义外观、自定义编译时展开等。
cpp
// .h文件
class MYEDITOR_API UMyBPNode_AddTwoInts : public UK2Node
{
GENERATED_BODY()
public:
// 纯节点(无执行引脚)
virtual bool IsNodePure() const override { return true; }
// 创建引脚
virtual void AllocateDefaultPins() override;
// 节点外观
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual FText GetTooltipText() const override;
// 菜单栏的注册
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual FText GetMenuCategory() const override;
// 编译时展开
virtual void ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
};
// .cpp文件(样式类的就不做多过多赘述,主要是展开节点)
void UMyBPNode_AddTwoInts::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
Super::ExpandNode(CompilerContext, SourceGraph);
// 1. 找到目标函数:UKismetMathLibrary::Add_IntInt
UFunction* AddFunction = UKismetMathLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetMathLibrary, Add_IntInt));
if (!AddFunction)
{
CompilerContext.MessageLog.Error(*LOCTEXT("AddFunctionNotFound", "Add_IntInt function not found.").ToString(), this);
return;
}
// 2. 在编译图中生成一个中间函数调用节点
UK2Node_CallFunction* CallAddNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
CallAddNode->SetFromFunction(AddFunction);
CallAddNode->AllocateDefaultPins();
// 3. 获取原节点和中间节点的引脚
UEdGraphPin* InputAPin = FindPinChecked(TEXT("A"));
UEdGraphPin* InputBPin = FindPinChecked(TEXT("B"));
UEdGraphPin* ResultPin = FindPinChecked(TEXT("Result"));
UEdGraphPin* CallInputAPin = CallAddNode->FindPinChecked(TEXT("A"));
UEdGraphPin* CallInputBPin = CallAddNode->FindPinChecked(TEXT("B"));
UEdGraphPin* CallReturnPin = CallAddNode->GetReturnValuePin();
// 4. 将原节点的引脚链接转移到中间节点
CompilerContext.MovePinLinksToIntermediate(*InputAPin, *CallInputAPin);
CompilerContext.MovePinLinksToIntermediate(*InputBPin, *CallInputBPin);
CompilerContext.MovePinLinksToIntermediate(*ResultPin, *CallReturnPin);
// 5. 断开原节点的所有链接(编译器将忽略它)
BreakAllNodeLinks();
}
这种方式定义的节点类主要存在于编辑器和编译期,给Kismet进行编译使用,上述两数相加的节点便不会打到游戏中,取而代之的是编译时的展开(ExpandNode),将自定义的函数放到UK2Node_CallFunction 的一个中间节点中,然后将现有的连接转移到这种中间节点上。
结果:

如果想要做到像Unreal中添加任意类型的端口,则需要使用到wildcard端口。
定义的这个类应该需要放在UncookedOnly 模块中。其他的模块类型:
- Runtime,游戏的核心逻辑,在任意情况下都会加载,一般Unreal工程默认创建的模块就是Runtime的;
- Editor,纯编辑器专用的,与UncookedOnly的区别在于这个Cook的过程:Cook过程会将Editor数据转换成运行时数据,此时如果需要这个过程的选择UncookedOnly,不需要选Editor即可。
想要创建模块的话:创建模块文件夹,在下面创建[模块名].Build.cs、[模块名].cpp、[模块名].h文件,然后在 [项目名].uproject、[项目名].Target.cs文件中注册。(具体操作就不赘述了,另外如果想要做成Plugin则不需要处理[项目名].Target.cs,当然创建Plugin引擎内可以通过Edit -> Plugins -> Add创建)