初步观察UE蓝图的“Branch节点”,这个最简单的K2Node的代码

发布于:2022-12-14 ⋅ 阅读:(641) ⋅ 点赞:(0)

无用的前言:why?

虽然蓝图节点可以很方便地通过UFunction进行扩展,但是其生成的节点拥有统一的结构,即单线的执行引脚(三角形引脚),并且输入与输出引脚由函数的参数与返回值决定:
在这里插入图片描述
如果想要扩展更个性化的节点,则需要以更底层的方式进行。即,定义新的UK2Node类。
由UFunction创建的蓝图节点就是UK2Node的子类UK2Node_CallFunction

(可以在这里下断点证明)
在这里插入图片描述

而基础的蓝图节点Branch,C++定义是UK2Node的子类UK2Node_IfThenElse
在这里插入图片描述
它应该是最简单的UK2Node子类,因此我想通过观察它的代码,来对K2Node代码的结构有个初步的了解。

目标

观察 K2Node_IfThenElse.h/cpp 的代码,来对K2Node代码的最基础结构有最初步的了解。

0. 总览

观察 .h 文件中的定义,可以看到函数被分为三个部分:
在这里插入图片描述

1. 作为 UEdGraphNode 的行为

这一部分描述了它作为图表节点所具备的特征,即:

  • AllocateDefaultPins:默认的引脚有哪些
  • GetNodeTitleColor:节点标题部分的颜色是什么
  • GetTooltipText:提示文字是什么
  • GetNodeTitle:节点标题的文字是什么
  • GetIconAndTint:图标和颜色是什么

主要关注的是默认引脚方面的逻辑:

void UK2Node_IfThenElse::AllocateDefaultPins()
{
	const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

	CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute);
	UEdGraphPin* ConditionPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Boolean, UEdGraphSchema_K2::PN_Condition);
	K2Schema->SetPinAutogeneratedDefaultValue(ConditionPin, TEXT("true"));

	UEdGraphPin* TruePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then);
	TruePin->PinFriendlyName =LOCTEXT("true", "true");

	UEdGraphPin* FalsePin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Else);
	FalsePin->PinFriendlyName = LOCTEXT("false", "false");

	Super::AllocateDefaultPins();
}

可以看到,它主要是使用CreatePin函数创建引脚

UEdGraphPin* CreatePin(EEdGraphPinDirection Dir, const FName PinCategory, const FName PinName, const FCreatePinParams& PinParams = FCreatePinParams())

填入参数分别是:

  1. 引脚方向(比如“输入”是EGPD_Input,“输出”是EGPD_Output
  2. 分类名(名字均在UEdGraphSchema_K2中定义好了,以PC_为前缀)
  3. 引脚名(一些具备特殊含义的名字均在UEdGraphSchema_K2中定义好了,以PN_为前缀)

剩余的关于图表节点的描述就比较直白了:

FText UK2Node_IfThenElse::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	return LOCTEXT("Branch", "Branch");
}

FLinearColor UK2Node_IfThenElse::GetNodeTitleColor() const
{
	return GetDefault<UGraphEditorSettings>()->ExecBranchNodeTitleColor;
}

FSlateIcon UK2Node_IfThenElse::GetIconAndTint(FLinearColor& OutColor) const
{
	static FSlateIcon Icon("EditorStyle", "GraphEditor.Branch_16x");
	return Icon;
}

FText UK2Node_IfThenElse::GetTooltipText() const
{
	return LOCTEXT("BrancStatement_Tooltip", "Branch Statement\nIf Condition is true, execution goes to True, otherwise it goes to False");
}

2. 为了更方便找到引脚的函数

接下来的三个函数其目的很简单,就是为了方便找到引脚,而方式就是通过引脚名

UEdGraphPin* UK2Node_IfThenElse::GetThenPin() const
{
	UEdGraphPin* Pin = FindPin(UEdGraphSchema_K2::PN_Then);
	check(Pin);
	return Pin;
}

UEdGraphPin* UK2Node_IfThenElse::GetElsePin() const
{
	UEdGraphPin* Pin = FindPin(UEdGraphSchema_K2::PN_Else);
	check(Pin);
	return Pin;
}

UEdGraphPin* UK2Node_IfThenElse::GetConditionPin() const
{
	UEdGraphPin* Pin = FindPin(UEdGraphSchema_K2::PN_Condition);
	check(Pin);
	return Pin;
}

3. 作为 K2Node 的行为

GetMenuCategory指定了节点的分类

FText UK2Node_IfThenElse::GetMenuCategory() const
{
	return FEditorCategoryUtils::GetCommonCategory(FCommonEditorCategory::FlowControl);
}

GetMenuActions 应该和节点的注册有关,虽然包含了一些代码和不少注释,但可以看到其他节点也都是一样的内容。因此暂时应该将它视为“模板”而无需关注。

最值得关注的应该是CreateNodeHandler,这里会是与节点的核心逻辑相关的内容:

FNodeHandlingFunctor* UK2Node_IfThenElse::CreateNodeHandler(FKismetCompilerContext& CompilerContext) const
{
	return new FKCHandler_Branch(CompilerContext);
}

FKCHandler_Branch只有一个Compile函数,显然这里包含了Branch节点的核心逻辑。

4. FKCHandler_Branch::Compile 外层

FKCHandler_Branch::Compile的外层主要是找到需要的引脚,并对其进行验证,包括:

  • 引脚是否存在
  • 引脚方向是否符合预期
  • 引脚类型是否符合预期
  • 引脚连接数目是否大于0

而【核心逻辑】被包裹在了最里层:

virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
{
	// For imperative nodes, make sure the exec function was actually triggered and not just included due to an output data dependency
	FEdGraphPinType ExpectedExecPinType;
	ExpectedExecPinType.PinCategory = UEdGraphSchema_K2::PC_Exec;

	FEdGraphPinType ExpectedBoolPinType;
	ExpectedBoolPinType.PinCategory = UEdGraphSchema_K2::PC_Boolean;


	UEdGraphPin* ExecTriggeringPin = Context.FindRequiredPinByName(Node, UEdGraphSchema_K2::PN_Execute, EGPD_Input);
	if ((ExecTriggeringPin == NULL) || !Context.ValidatePinType(ExecTriggeringPin, ExpectedExecPinType))
	{
		CompilerContext.MessageLog.Error(*LOCTEXT("NoValidExecutionPinForBranch_Error", "@@ must have a valid execution pin @@").ToString(), Node, ExecTriggeringPin);
		return;
	}
	else if (ExecTriggeringPin->LinkedTo.Num() == 0)
	{
		CompilerContext.MessageLog.Warning(*LOCTEXT("NodeNeverExecuted_Warning", "@@ will never be executed").ToString(), Node);
		return;
	}

	// Generate the output impulse from this node
	UEdGraphPin* CondPin = Context.FindRequiredPinByName(Node, UEdGraphSchema_K2::PN_Condition, EGPD_Input);
	UEdGraphPin* ThenPin = Context.FindRequiredPinByName(Node, UEdGraphSchema_K2::PN_Then, EGPD_Output);
	UEdGraphPin* ElsePin = Context.FindRequiredPinByName(Node, UEdGraphSchema_K2::PN_Else, EGPD_Output);
	if (Context.ValidatePinType(ThenPin, ExpectedExecPinType) &&
		Context.ValidatePinType(ElsePin, ExpectedExecPinType) &&
		Context.ValidatePinType(CondPin, ExpectedBoolPinType))
	{
		UEdGraphPin* PinToTry = FEdGraphUtilities::GetNetFromPin(CondPin);
		FBPTerminal** CondTerm = Context.NetMap.Find(PinToTry);

		if (CondTerm != NULL) //
		{
			【核心逻辑】
		}
		else
		{
			CompilerContext.MessageLog.Error(*LOCTEXT("ResolveTermPassed_Error", "Failed to resolve term passed into @@").ToString(), CondPin);
		}
	}
}

5. FKCHandler_Branch::Compile 核心逻辑

【核心逻辑】如下:

// First skip the if, if the term is false
{
	FBlueprintCompiledStatement& SkipIfGoto = Context.AppendStatementForNode(Node);
	SkipIfGoto.Type = KCST_GotoIfNot;
	SkipIfGoto.LHS = *CondTerm;
	Context.GotoFixupRequestMap.Add(&SkipIfGoto, ElsePin);
}

// Now go to the If branch
{
	FBlueprintCompiledStatement& GotoThen = Context.AppendStatementForNode(Node);
	GotoThen.Type = KCST_UnconditionalGoto;
	GotoThen.LHS = *CondTerm;
	Context.GotoFixupRequestMap.Add(&GotoThen, ThenPin);
}

对于其中的意思我并不敢直接解释,因为其中的概念我现在并不了解,我担心自己的理解并不准确。
我只能逐个观察每个概念点,尝试翻译下其注释。


FBPTerminal

CondTerm的类型是FBPTerminal。从逻辑上可以看到它来自于Condition引脚:
在这里插入图片描述

UEdGraphPin* CondPin = Context.FindRequiredPinByName(Node, UEdGraphSchema_K2::PN_Condition, EGPD_Input);
...
UEdGraphPin* PinToTry = FEdGraphUtilities::GetNetFromPin(CondPin);
FBPTerminal** CondTerm = Context.NetMap.Find(PinToTry);

对于FBPTerminal,字面翻译:蓝图端点
注释:

/** A terminal in the graph (literal or variable reference) */

翻译:图表中的一个端点(字面意思,或者变量引用)

FKismetFunctionContext

FKCHandler_Branch::Compile函数中参数Context被传入,它的类型是FKismetFunctionContext

关于“Kismet”这个名字:
官方文档指出,“Kismet”是UE3(UDK)中可视化脚本系统的名字,因此在UE4中应该可以视作指代“蓝图”。(之所以还用这个名字可能是因为代码的历史原因?)

FKismetFunctionContext字面翻译:蓝图函数上下文


它有一个函数AppendStatementForNode
注释:

/** Enqueue a statement to be executed when the specified Node is triggered */

翻译:入队一个语句,它会在指定节点触发时执行


GotoFixupRequestMap是它的一个成员:

// Goto fixup requests (each statement (key) wants to goto the first statement attached to the exec out-pin (value))
TMap< FBlueprintCompiledStatement*, UEdGraphPin* > GotoFixupRequestMap;

翻译:Key中的语句,想要跳转到 Value中的(执行类输出)引脚所连接的第一个语句。

FBlueprintCompiledStatement

SkipIfGotoGotoThen类型都是FBlueprintCompiledStatement
FBlueprintCompiledStatement字面翻译:蓝图编译语句


它的成员Type指定了其类型,由枚举EKismetCompiledStatementType表示(附录贴上了完整定义)。


LHSRHS全称应该是 left hand sideright hand side(语句的左侧和右侧)。
注释:

// Destination of assignment statement or result from function call
struct FBPTerminal* LHS;

// Argument list of function call or source of assignment statement
TArray<struct FBPTerminal*> RHS;

翻译:
LHS:赋值语句的目的地,或函数调用的结果
RHS:函数调用的参数列表,或赋值语句的来源

总结

继承一个K2Node,将可以自定义UEdGraphNode层面的一些行为,包括引脚。
而核心逻辑,是通过FNodeHandlingFunctor::Compile指定的,其中牵扯到一些我还不太熟悉的概念,需要之后通过观察其他节点以及实践来进一步学习。

附录:EKismetCompiledStatementType完整定义

enum EKismetCompiledStatementType
{
	KCST_Nop = 0,
	// [wiring =] TargetObject->FunctionToCall(wiring)
	KCST_CallFunction = 1,
	// TargetObject->TargetProperty = [wiring]
	KCST_Assignment = 2,
	// One of the other types with a compilation error during statement generation
	KCST_CompileError = 3,
	// goto TargetLabel
	KCST_UnconditionalGoto = 4,
	// FlowStack.Push(TargetLabel)
	KCST_PushState = 5,
	// [if (!TargetObject->TargetProperty)] goto TargetLabel
	KCST_GotoIfNot = 6,
	// return TargetObject->TargetProperty
	KCST_Return = 7,
	// if (FlowStack.Num()) { NextState = FlowStack.Pop; } else { return; }
	KCST_EndOfThread = 8,
	// Comment
	KCST_Comment = 9,
	// NextState = LHS;
	KCST_ComputedGoto = 10,
	// [if (!TargetObject->TargetProperty)] { same as KCST_EndOfThread; }
	KCST_EndOfThreadIfNot = 11,
	// NOP with recorded address
	KCST_DebugSite = 12,
	// TargetInterface(TargetObject)
	KCST_CastObjToInterface = 13,
	// Cast<TargetClass>(TargetObject)
	KCST_DynamicCast = 14,
	// (TargetObject != None)
	KCST_ObjectToBool = 15,
	// TargetDelegate->Add(EventDelegate)
	KCST_AddMulticastDelegate = 16,
	// TargetDelegate->Clear()
	KCST_ClearMulticastDelegate = 17,
	// NOP with recorded address (never a step target)
	KCST_WireTraceSite = 18,
	// Creates simple delegate
	KCST_BindDelegate = 19,
	// TargetDelegate->Remove(EventDelegate)
	KCST_RemoveMulticastDelegate = 20,
	// TargetDelegate->Broadcast(...)
	KCST_CallDelegate = 21,
	// Creates and sets an array literal term
	KCST_CreateArray = 22,
	// TargetInterface(Interface)
	KCST_CrossInterfaceCast = 23,
	// Cast<TargetClass>(TargetObject)
	KCST_MetaCast = 24,
	KCST_AssignmentOnPersistentFrame = 25,
	// Cast<TargetClass>(TargetInterface)
	KCST_CastInterfaceToObj = 26,
	// goto ReturnLabel
	KCST_GotoReturn = 27,
	// [if (!TargetObject->TargetProperty)] goto TargetLabel
	KCST_GotoReturnIfNot = 28,
	KCST_SwitchValue = 29,
	
	//~ Kismet instrumentation extensions:

	// Instrumented event
	KCST_InstrumentedEvent,
	// Instrumented event stop
	KCST_InstrumentedEventStop,
	// Instrumented pure node entry
	KCST_InstrumentedPureNodeEntry,
	// Instrumented wiretrace entry
	KCST_InstrumentedWireEntry,
	// Instrumented wiretrace exit
	KCST_InstrumentedWireExit,
	// Instrumented state push
	KCST_InstrumentedStatePush,
	// Instrumented state restore
	KCST_InstrumentedStateRestore,
	// Instrumented state reset
	KCST_InstrumentedStateReset,
	// Instrumented state suspend
	KCST_InstrumentedStateSuspend,
	// Instrumented state pop
	KCST_InstrumentedStatePop,
	// Instrumented tunnel exit
	KCST_InstrumentedTunnelEndOfThread,

	KCST_ArrayGetByRef,
	KCST_CreateSet,
	KCST_CreateMap,
};
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到