虽然蓝图节点可以很方便地通过UFunction进行扩展,但是其生成的节点拥有统一的结构,即单线的执行引脚(三角形引脚),并且输入与输出引脚由函数的参数与返回值决定:
如果想要扩展更个性化的节点,则需要以更底层的方式进行。即,定义新的UK2Node
类。
由UFunction创建的蓝图节点就是UK2Node
的子类UK2Node_CallFunction
(可以在这里下断点证明)
而基础的蓝图节点Branch,C++定义是UK2Node
的子类UK2Node_IfThenElse
。
它应该是最简单的UK2Node
子类,因此我想通过观察它的代码,来对K2Node代码的结构有个初步的了解。
观察 K2Node_IfThenElse.h/cpp 的代码,来对K2Node代码的最基础结构有最初步的了解。
观察 .h 文件中的定义,可以看到函数被分为三个部分:
这一部分描述了它作为图表节点所具备的特征,即:
主要关注的是默认引脚方面的逻辑:
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())
填入参数分别是:
EGPD_Input
,“输出”是EGPD_Output
)UEdGraphSchema_K2
中定义好了,以PC_
为前缀)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");
}
接下来的三个函数其目的很简单,就是为了方便找到引脚,而方式就是通过引脚名:
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;
}
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节点的核心逻辑。
FKCHandler_Branch::Compile
的外层主要是找到需要的引脚,并对其进行验证,包括:
而【核心逻辑】被包裹在了最里层:
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);
}
}
}
【核心逻辑】如下:
// 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);
}
对于其中的意思我并不敢直接解释,因为其中的概念我现在并不了解,我担心自己的理解并不准确。
我只能逐个观察每个概念点,尝试翻译下其注释。
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) */
翻译:图表中的一个端点(字面意思,或者变量引用)
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中的(执行类输出)引脚所连接的第一个语句。
SkipIfGoto
和GotoThen
类型都是FBlueprintCompiledStatement
FBlueprintCompiledStatement
字面翻译:蓝图编译语句
它的成员Type
指定了其类型,由枚举EKismetCompiledStatementType
表示(附录贴上了完整定义)。
LHS
和RHS
全称应该是 left hand side 和 right 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
指定的,其中牵扯到一些我还不太熟悉的概念,需要之后通过观察其他节点以及实践来进一步学习。
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,
};