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


    无用的前言: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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

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

    填入参数分别是:

    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");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    3. 作为 K2Node 的行为

    GetMenuCategory指定了节点的分类

    FText UK2Node_IfThenElse::GetMenuCategory() const
    {
    	return FEditorCategoryUtils::GetCommonCategory(FCommonEditorCategory::FlowControl);
    }
    
    • 1
    • 2
    • 3
    • 4

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

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

    FNodeHandlingFunctor* UK2Node_IfThenElse::CreateNodeHandler(FKismetCompilerContext& CompilerContext) const
    {
    	return new FKCHandler_Branch(CompilerContext);
    }
    
    • 1
    • 2
    • 3
    • 4

    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);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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


    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);
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    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 */
    
    • 1

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


    GotoFixupRequestMap是它的一个成员:

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

    翻译: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;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    翻译:
    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(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(TargetObject)
    	KCST_MetaCast = 24,
    	KCST_AssignmentOnPersistentFrame = 25,
    	// Cast(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,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
  • 相关阅读:
    物联网ARM开发-4STM32串口通信USART应用
    前端项目如何准确预估个人工时
    Cmake常用命令
    阿里云OSS、用户认证与就诊人
    计算表达式【学习算法】
    压缩文件7-Zip与WinRAR个人免费版在不同压缩等级下的对比
    中国大陆已有IB学校243所
    车载以太网TSN
    maptalks常见操作——图层置顶置底、添加清空图层、添加标注、切换底图、添加缩放工具、事件监听(点击面出弹框)、右键菜单、绘制mark、锁定视角
    【面试系列】Java面试知识篇(七)
  • 原文地址:https://blog.csdn.net/u013412391/article/details/126909792