做项目做到一定阶段,蓝图越来越像一座盘根错节的数据迷宫,这时候你大概率会冒出同一个念头:能不能把反复拖拽的那一坨逻辑,收成一个干干净净的节点,右键一搜就能放出来用?我第一次被这个念头支配,是因为想在一个模拟项目里复用一套“评分——分级——分流”的处理逻辑:输入一个分数,既要返回一段评语,又得让蓝图根据等级走向三个完全不同的后续流程。蓝图自带的函数库完全做不了后面这件事,因为它的Return节点只能结束一条执行线,给不了我多路分支。于是我硬着头皮开始啃UE5自定义蓝图节点的实现路径,发现只要把输入输出参数与多逻辑引脚的底层逻辑搞清楚,其他一切都会顺起来。这篇文章就把我从选型到编译、从踩坑到打磨的完整过程摊开讲,适合想把节点面板彻底掌控在自己手里的开发者。
1. 为什么要绕开“函数库封装”,而不是继续拼蓝图
1.1 蓝图网络里,节点到底扮演什么角色
在UE5里,一个蓝图节点在编辑器和运行时是两种完全不同的东西。编辑器里它是一个UEdGraphNode,引脚是UEdGraphPin,引脚上挂着的类型信息叫PinType;编译之后,它会被翻译成中间语言指令,再变成最终的可执行代码。你要是只把它当成“一个图形化的函数调用”,就很难理解自定义节点为什么这么麻烦,也很难明白为什么会有“多逻辑引脚”这种概念。
我习惯用一个类比:一张蓝图就是做菜的流程表,节点是灶台上的一次操作,exec引脚像是“下一步该做哪件事”的便签,数据引脚则是从冰箱到砧板再到锅里传递的配料。编译过程,就是把这份流程表翻译成火头师傅脑子里真正会去执行的顺序。自定义节点的本质,是往这个“便签系统”里插入一个你自己设计的操作台。这样想,你自然就会明白:数据引脚决定“拿什么”,exec引脚决定“做完之后去干嘛”,两者是完全不同层面的东西。
1.2 什么需求逼得你不得不写UK2Node
不是所有需求都值得上自定义节点。但如果遇到下面这几类情况,蓝图自带的能力几乎都会卡壳:
- 多路分支执行:一个节点执行完,根据结果走两条或更多条不同的后续逻辑线。
Branch、Switch是引擎预先写好的,一旦逻辑变成“评分高于80走A线、60到80走B线、低于60走C线”,原生节点就拼不出一个清爽的封装了。 - 多返回值加执行分支组合:函数库可以带多个
OUT参数返回多个值,但它的执行Pin只有一个“Then”,无法按返回值分流。 - 节点自身需要可配置属性:比如阈值、开关、模式,想通过细节面板直接编辑,而不是每次都在蓝图里传参。
- 给美术、策划屏蔽实现细节:他们只关心输入什么、输出什么,不想看到十几层的连线和临时变量。
我做的“评分分级”节点正好占了前三条,所以别无选择,只能走上UK2Node这条路。如果你只是想把一段算法封装成单输出节点,UBlueprintFunctionLibrary仍然是最优解,没必要杀鸡用牛刀。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三条路线怎么选:FunctionLibrary、UK2Node还是插件级扩展
2.1 静态函数库:最简单,但卡死了执行引脚
UBlueprintFunctionLibrary是大多数人第一次接触“自定义节点”的入口。写一个静态函数,加上BlueprintCallable标记,蓝图里就能搜到。它的最大优点是零额外成本,纯C++函数就够,不需要关心引脚分配和编译流程。
cpp复制UCLASS()
class UDemoLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
UFUNCTION(BlueprintCallable, Category = "Demo")
static FString GetScoreReport(float Score);
};
它也确实好用,但局限性非常明显:执行引脚固定只有一个输出,函数返回之后流程只能继续往下走,你没法让它“根据分数走向三条不同分支”。如果你想做的节点只是“算点东西然后吐出来”,这条路完全够用;但想要多逻辑引脚,就必须升级方案。
2.2 UK2Node完整路径:从“封装函数”到“定制节点行为”
真正的自定义执行流节点,需要继承UK2Node。这个类允许你完全控制节点在蓝图编辑器里的表现:创建哪些输入输出引脚、标题是什么、出现在哪个菜单分类、编译时如何生成逻辑。
一个最基础的自定义节点类长这样:
cpp复制UCLASS()
class UDemoScoreNode : public UK2Node
{
GENERATED_BODY()
public:
virtual void AllocateDefaultPins() override;
virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
virtual FText GetMenuCategory() const override;
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual bool ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
};
这里面AllocateDefaultPins是最核心的入口,你在这个函数里创建多少个引脚,节点上就显示多少个接口。后面第3、4章会详细展开。这里先记住一个结论:UK2Node的编写重点不在“逻辑怎么写”,而在“引脚怎么声明、编译时怎么映射到标准节点”。真正的计算逻辑通常仍然放在C++静态函数里,节点本身只负责接线和分流。
2.3 菜单注册与模块挂载:让节点出现在蓝图编辑器里
光有类还不够,你得让蓝图编辑器在节点面板里收录它。UE5中仍然是通过GetMenuActions来注册:
cpp复制void UDemoScoreNode::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
UClass* ActionKey = GetClass();
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
{
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
}
}
只要这个节点类被模块加载,蓝图编辑器会扫描到它,右键菜单就能搜到。这条路径在UE5的多个版本里都是稳定的,项目里已经有其他自定义节点模块的话,照抄这个模式即可。很多初学者卡在这里,并不是因为注册多复杂,而是因为没把这个函数写进类里。
三条路线放一起看更直观:
| 方案 | 实现成本 | 执行引脚控制 | 数据引脚控制 | 节点外观控制 | 适用场景 |
|---|---|---|---|---|---|
| FunctionLibrary | 极低 | 固定一个输出 | 参数表驱动 | 几乎无 | 单输出、纯计算封装 |
| UK2Node | 高 | 完全自由 | 完全自由 | 可深度定制 | 复杂执行流、多分支、属性配置 |
| 插件级扩展 | 很高 | 取决于上层实现 | 取决于上层实现 | 可扩展编辑器UI | 要配合自定义资源/蓝图类型 |
3. 输入输出参数的真实实现:数据引脚不是“加个变量”那么简单
3.1 AllocateDefaultPins创建数据引脚:类型、名称、默认值
所有引脚都在AllocateDefaultPins里产生。以我那个评分节点为例,它需要一个输入Exec、一个输入浮点分数、三个输出Exec以及一个字符串输出:
cpp复制void UDemoScoreNode::AllocateDefaultPins()
{
// 输入执行引脚
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, TEXT("In"));
// 输入数据引脚:UE5中建议用 PC_Real + PC_Double 表示蓝图里的浮点数
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Real, UEdGraphSchema_K2::PC_Double, nullptr, TEXT("Score"));
// 三个输出执行引脚
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TEXT("High"));
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TEXT("Mid"));
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TEXT("Low"));
// 输出数据引脚:字符串报告
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_String, TEXT("Report"));
}
这里有两个细节容易翻车。第一,CreatePin的参数顺序是方向、类别、子类别、子类别对象、引脚名、容器类型,不是想当然的“名称在前”。第二,UE5里浮点数引脚的类型体系有了变化,以前常用的PC_Float逐渐让位于PC_Real配合PC_Double子类别。如果你直接写PC_Float,早期看起来没问题,一旦在蓝图里接另一个默认的浮点变量引脚,类型不匹配的红线就会冒出来。所以新项目我统一用PC_Real + PC_Double。
创建完引脚之后还要注意默认值。对数据引脚,最好显式设置默认值,否则节点悬空时是0、空字符串还是非法值,行为会很不明确:
cpp复制UEdGraphPin* ScorePin = FindPin(TEXT("Score"));
if (ScorePin)
{
GetDefault<UEdGraphSchema_K2>()->SetPinAutogeneratedDefaultValue(ScorePin, TEXT("0"));
}
3.2 引脚类型与可视化属性:Object、Enum、数组和const输入
实际项目里不会只有浮点和字符串。对象引用、枚举、结构体、数组都会出现在自定义节点上。它们的区别主要在PinSubCategory和PinSubCategoryObject:
cpp复制// 输入一个Actor引用
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, AActor::StaticClass(), TEXT("Target"));
// 输入一个枚举
UEnum* LevelEnum = FindObject<UEnum>(nullptr, TEXT("/Script/Demo.EDemoScoreLevel"));
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, LevelEnum, TEXT("Level"));
// 输入一个结构体(FVector为例)
CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, TBaseStructure<FVector>::Get(), TEXT("Location"));
// 数组输入:基类物件加 then set container type Array
UEdGraphPin* ArrayPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Int, TEXT("Values"));
ArrayPin->PinType.ContainerType = EPinContainerType::Array;
每次创建对象引脚,都要把PinSubCategoryObject设置成对应的类,否则编辑器不知道这个引脚该显示成什么、能接什么。枚举引脚用PC_Byte加UEnum指针,蓝图里会自动渲染成下拉框式的枚举选择器。
还有一些容易被忽略的修饰属性。比如输入引脚如果只想让蓝图使用而不希望被写值,可以设置PinType.bIsConst = true,编译期会强制输入连接只能来自常量输出;希望默认折叠、需要用户手动打开高级引脚来显示,就设置Pin->bAdvancedView = true。这些细节加起来,才是“输入输出参数”真正可控的样子,而不是机械地创建几个Pin就完事。
3.3 参数化:让用户能在细节面板改你的输入输出规则
自定义节点真正的爽点之一,是它自身可以带属性。还是回到评分节点,我想让策划直接在细节面板里改高低阈值,而不是每次都在蓝图里连两个整数。做法是把阈值声明成节点属性:
cpp复制UPROPERTY(EditAnywhere, Category = "Score Thresholds")
int32 LowThreshold = 60;
UPROPERTY(EditAnywhere, Category = "Score Thresholds")
int32 HighThreshold = 80;
声明之后,选中节点时右侧细节面板就能编辑。但有一个隐藏陷阱:当你改动这些数值时,节点本身不会自动刷新,除非你重写PostEditChangeProperty并调用ReconstructNode。我第一次就没写这一步,节点表面看起来一切正常,编译时却总是用初始值,排查了很久才发现是没触发重建。
cpp复制#if WITH_EDITOR
void UDemoScoreNode::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
const FName Name = PropertyChangedEvent.GetPropertyName();
if (Name == GET_MEMBER_NAME_CHECKED(UDemoScoreNode, LowThreshold) ||
Name == GET_MEMBER_NAME_CHECKED(UDemoScoreNode, HighThreshold))
{
ReconstructNode();
}
}
#endif
ReconstructNode会重新执行AllocateDefaultPins,顺带刷新引脚的默认值和外观。有了这一步,节点才真正变成“写一份逻辑,画图的人只调参数”的理想形态。
4. 多逻辑引脚:执行流的分叉,才是自定义节点的灵魂
4.1 为什么exec引脚在蓝图里如此特殊
exec引脚不属于任何数据类型,它不携带“值”,携带的是“控制权”。数据引脚连接定义的是依赖关系:下游用到这个值,上游必须先算出来。exec引脚连接定义的是顺序关系:上游执行完,控制权才轮到下游。这是两种完全不同的语义,我在第一次写节点时把它们混为一谈,结果创建了一堆数据引脚导致的错误。
理解了这一点,你就能明白为什么自定义节点的重点往往是exec引脚。对一个返回单一值的函数节点来说,exec只是“调用一次”和“调用完往下走”的固定结构;但一旦节点自身要分流,比如“如果分数大于80走这条线,否则走另一条”,exec引脚的数量和连接方式就决定了一个节点能表达多复杂的逻辑。
4.2 创建多输入/多输出的Exec引脚:约束与习惯
创建多个输出Exec引脚没有任何魔法,就是在AllocateDefaultPins里多调几次CreatePin:
cpp复制CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, TEXT("In"));
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TEXT("High"));
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TEXT("Mid"));
CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, TEXT("Low"));
命名上要遵循节点社区里的大致习惯:输入Exec通常叫In、Exec、Execute,输出Exec往往用能说明分支含义的短语,比如OnSuccess、OnFailure、ThenA、ThenB。这样别人看图的时候不用点开节点就能猜到每条线的语义。
需要注意,多个输入Exec引脚虽然技术上可以创建,但在编排执行语义上要非常小心。蓝图默认情况下,一个节点只有一个真正的主输入Exec,其他输入Exec的语义完全由你自定义编译逻辑决定,编译器不会自动合并多个输入流。多数情况我只保留一个输入Exec,避免让使用者产生“两条线都能进来”的误解。
4.3 一个极实用的多分支样例:让自定义节点像Branch一样分派执行流
达到这个目标的办法不是自己在UK2Node的Compile里硬写中间指令,而是把节点展开成引擎自带的标准节点组合。这是UE5中最推荐的自定义K2节点实现方式,核心就在ExpandNode。
我设计的评分分流逻辑是:先把分数传给一个C++静态函数算出等级枚举,再把这个枚举接到引擎自带的SwitchEnum节点,由它把执行流切到三条输出Exec。这样一来,真正在运行时执行的其实是一段非常标准、可被引擎追踪调试的逻辑,而不是一个无法内视的黑盒。
首先定义枚举和计算函数:
cpp复制UENUM(BlueprintType)
enum class EDemoScoreLevel : uint8
{
High,
Mid,
Low
};
UFUNCTION(BlueprintCallable)
static EDemoScoreLevel EvaluateScore(float Score, int32 LowThreshold, int32 HighThreshold);
然后在ExpandNode里搭建中间图:
cpp复制bool UDemoScoreNode::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
// 1. 创建内部函数调用节点,真正执行评分逻辑
UK2Node_CallFunction* CallNode = NewObject<UK2Node_CallFunction>(SourceGraph);
CallNode->FunctionReference.SetExternalMember(
GET_FUNCTION_NAME_CHECKED(UDemoLibrary, EvaluateScore), UDemoLibrary::StaticClass());
SourceGraph->AddNode(CallNode, false, false);
CallNode->AllocateDefaultPins();
// 2. 创建引擎内置的 SwitchEnum 节点,负责分流
UK2Node_SwitchEnum* SwitchNode = NewObject<UK2Node_SwitchEnum>(SourceGraph);
SwitchNode->Enum = FindObject<UEnum>(nullptr, TEXT("/Script/Demo.EDemoScoreLevel"));
SourceGraph->AddNode(SwitchNode, false, false);
SwitchNode->AllocateDefaultPins();
// 3. 把原节点引脚上的外部连接迁移到中间节点
CompilerContext.MovePinLinksToIntermediate(*FindPin(TEXT("In")),
*CallNode->FindPin(UEdGraphSchema_K2::PN_Execute));
CompilerContext.MovePinLinksToIntermediate(*FindPin(TEXT("Score")),
*CallNode->FindPin(TEXT("Score")));
// 4. 把函数输出接到Switch的输入选择引脚
UEdGraphPin* LevelResultPin = CallNode->FindPin(TEXT("ReturnValue"));
UEdGraphPin* SwitchInputPin = SwitchNode->FindPin(TEXT("Selection"));
if (LevelResultPin && SwitchInputPin)
{
LevelResultPin->MakeLinkTo(SwitchInputPin);
}
// 5. 将三条输出Exec的对外连接分别迁移到Switch对应的输出Exec
UEdGraphPin* HighPin = SwitchNode->FindPin(TEXT("High"));
UEdGraphPin* MidPin = SwitchNode->FindPin(TEXT("Mid"));
UEdGraphPin* LowPin = SwitchNode->FindPin(TEXT("Low"));
CompilerContext.MovePinLinksToIntermediate(*FindPin(TEXT("High")), *HighPin);
CompilerContext.MovePinLinksToIntermediate(*FindPin(TEXT("Mid")), *MidPin);
CompilerContext.MovePinLinksToIntermediate(*FindPin(TEXT("Low")), *LowPin);
// 6. 断掉原节点所有外部连接,本节点不再直接参与编译
BreakAllNodeLinks();
return true;
}
第5步里,如果还想把Report字符串输出也传递出去,可以再用一个中间变量节点或直接调用另一个输出函数把它接出去;思路完全一样,都是“迁移连接、建立新连接”。这段代码有几个关键点:创建中间节点后必须调用AllocateDefaultPins,否则引脚不存在;连接迁移用的是CompilerContext.MovePinLinksToIntermediate,它会保留原有连接上的激活状态和高级连线;最后必须BreakAllNodeLinks并返回true,编译器看到返回值后就不会再把原节点当成一个独立编译单元。
5. 编译内核:自定义节点如何被Kismet编译器“翻译”成可执行逻辑
5.1 ExpandNode展开法:把自定义节点拆成标准节点图
UE5的Kismet编译器在编译蓝图时,会先让每个节点决定自己如何参与编译。节点可以不实现ExpandNode,那编译器就直接处理它;如果实现了,编译器会先调用它,用返回的中间图替换原节点,再继续编译。
这也是我认为自定义节点最需要理解的一点:你不一定要让编译器认识你的节点,你只需要让它认识你搭建出来的那一堆标准节点。引擎自带的CallFunction、SwitchEnum、Branch等节点,编译器处理得非常成熟,错误提示和调试支持都很完善。把自己的逻辑拆散成它们的组合,几乎等于免费获得了整条编译链路的稳定性。
展开法唯一的代价是调试时多一层跳转:你在源码蓝图里看不到中间节点,因为它们只存在于编译器的临时图里。但引擎的错误信息仍然会指向具体函数,所以实际排查并不困难。
5.2 FNodeHandlingFunctor映射法:直接翻译到中间语言指令
还有一条更底层的路,是自己写一个FNodeHandlingFunctor,覆写Compile方法,为节点生成编译指令。这条路适合那些引擎自带节点组合表达不了的流程控制,比如自定义循环、状态机跳转、堆栈操作,需要直接操作FKismetCompilerContext的语句生成接口。
普通业务项目我不建议碰这条路,原因很简单:展开法是用引擎已经验证过的砖块盖房子,而映射法是直接烧砖。前者出了问题,错误定位可以落到标准节点上;后者出了问题,你可能要通读编译器的中间指令说明才能找到问题。我只有在做一个类似自定义状态机框架的试验项目时尝试过一次,收益确实很大,但投入时间也是几何级数增长。
5.3 编译错误与节点图标红色警告的排查链路
自定义节点最常见的编译失败现象,是蓝图编辑器里节点变成红色/黄色,编译器输出窗口给出一句模糊提示。我踩坑之后整理出了一套排查链路,基本够用:
第一步,检查ExpandNode是否完整。遗漏任何一个MovePinLinksToIntermediate,或者忘记BreakAllNodeLinks,编译器都可能报“Failed to expand”或直接说“Pin has no connections”。
第二步,检查引脚类型是否精确。UE5里浮点类型尤其容易出问题,PC_Float和PC_Real + PC_Double混用,会导致编译时无法匹配网络类型。排查方法很简单:把节点的输出引脚默认下拉菜单打开,看它显示的类型是不是和预期一致。
第三步,覆写ValidateNodeDuringCompilation,把自定义错误提前暴露出来:
cpp复制virtual EDataValidationResult ValidateNodeDuringCompilation(
FDataValidationContext& Context) const override;
在这里面检查引脚名、默认值、枚举指针是否有效,一发现问题就Context.AddError(...),比事后去翻编译输出直观得多。
第四步,也是我最常用的“最小化”方案:复制节点到一个空蓝图里,只保留一个输入Exec和一个输出数据引脚,去掉所有多分支逻辑,看能不能编译通过。如果能,再逐层加回输出Exec和中间Switch。这种二分法能非常迅速地把问题隔离到具体某个引脚或某段展开逻辑上。
6. 打磨节点体验:标题、分类、搜索词与踩坑清单
6.1 节点在菜单里的位置、标题和关键词怎么控制
节点的可用性大部分来自菜单体验。一个搜不到、看不懂标题的节点,逻辑再强也没人愿意用。几个必备的重写方法:
cpp复制FText UDemoScoreNode::GetMenuCategory() const
{
return INVTEXT("Demo|Rating");
}
FText UDemoScoreNode::GetKeywords() const
{
return INVTEXT("score level branch");
}
FText UDemoScoreNode::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
return INVTEXT("Score Level Branch");
}
FLinearColor UDemoScoreNode::GetNodeTitleColor() const
{
return FLinearColor(0.1f, 0.7f, 0.3f);
}
GetMenuCategory决定节点面板里的路径,用|可以创建子菜单;GetKeywords决定被搜索到的概率,多写几个同义词,策划找不着节点的时候会回来谢谢你;GetNodeTitleColor给节点一个视觉区分点,方便它在密密麻麻的蓝图网里一眼被认出。
6.2 那些文档没写但你必须知道的细节
有几个坑是几乎每个自定义节点作者都会撞上的,我列成一张速查表,省得你再走一遍弯路:
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 蓝图编辑器里搜不到自定义节点 | 模块没加载或没实现GetMenuActions | 确认模块在Build.cs和Target里已经加载,并实现标准的NodeSpawner注册 |
| 节点显示红色但编译日志没细节 | ExpandNode返回false或内部引脚连接缺失 | 用二分法逐个删引脚,看哪个引发了编译失败 |
| 浮点输入只能接整数 | 用了PC_Float,与蓝图默认的PC_Real + PC_Double不一致 | 统一改成PC_Real + PC_Double |
| 细节面板改阈值,编译结果不变 | 没有在PostEditChangeProperty里调用ReconstructNode | 补上重建逻辑,让引脚和默认值跟着刷新 |
| 字符串输出默认值显示乱码 | 字符串默认值没有转义 | 字符串默认值要写成\"...\"形式,不能裸写 |
| 节点升级后旧蓝图破线 | 引脚名称改过但没做旧引脚兼容 | 在ReconstructNode中处理Orphaned pin,或保留原FName并新增引脚采用不同命名 |
| 多输出Exec被允许多连 | 默认exec输出可以连多个下游 | 需要自己限制时,重写CanCreateConnection或连接回调 |
6.3 我的实测建议:一套能少踩很多坑的实践清单
最后是我个人在实际项目里沉淀下来的一套做法,照做能省掉大量无谓的调试时间:
- 第一版只做数据引脚。先把输入输出参数跑通,确认编辑器能搜到节点,编译不报错,再往上加多个Exec引脚。一步到位写全功能,报错时你根本分不清是引脚问题还是展开问题。
- 先定函数,再定节点。把最终要执行的C++函数先独立写成BlueprintCallable静态函数,在普通函数库里测试返回值正确,然后再写节点去包装它。节点只是壳,数学和业务逻辑越简单越好。
- 小心缓存。修改了节点类之后,编辑器偶尔会出现节点菜单不刷新、引脚下拉列表还是旧类型的情况。先关编辑器、增量编译、再打开,大多数假象都会消失。
- 给输出Exec引脚命名时,多用“状态”而不是“动作”。
Success、Failure、HighLevel比DoA、DoB更通用,因为同一个节点的输出可能被不同蓝图接去完全不同的动作。 - 用版本控制留底稿。展开逻辑从无到有时改动很大,建议每跑通一个阶段就提交一次,方便二分回滚定位问题。我一开始把整个节点写完才提交,结果一次重构就把所有对外连接全弄丢了。
UE5自定义蓝图节点的路并不长,最难的一步永远是你决定不再“拼蓝图”,而是动手去写一个自己的节点当天。只要把输入输出引脚和多逻辑执行这两块骨架打好,剩下的扩展空间,会比你能想到的还大。
