UK2Node_SwitchClass and K2Node help

I have an issue with a Custom Editor Node which extends UK2Node_Switch, bringing that behaviour to Class types. Surely a very useful node to cut down on bloat when dealing with polymorphic types and other such systems. The node compiles correctly, but only ever outputs the default exec pin. Breakpointing doesn’t seem to yield any useful results.


As you can see, our Selection Input pin is a Class (SubCategoryObject UObject::StaticClass), likewise, we also have an Output pin of type Class, and yet we’ve returned a default result.

Perhaps I’m missing the obvious, as I’m new to editor scripting and K2Nodes. Much of the class is inferred from other UK2Node_Switch types. I hope someone here can help with this issue. My suspicion is in the use of PinType.PinCategory = UEdGraphSchema_K2::PC_Class but as said, I’m new to this. Relevant code is as follows, omitting K2 boilerplate:

UK2Node_SwitchClass .h

class UK2Node_SwitchClass : public UK2Node_Switch
{
	GENERATED_UCLASS_BODY()

	UPROPERTY(EditAnywhere, Category = PinOptions)
	TArray<UClass*> PinClasses;

...

	// UK2Node_Switch Interface
	virtual void AddPinToSwitchNode() override;
	virtual FName GetUniquePinName() override;
	virtual FEdGraphPinType GetPinType() const override;
	virtual FName GetPinNameGivenIndex(int32 Index) const override;
	// End of UK2Node_Switch Interface

protected:
	virtual void CreateSelectionPin() override;
	virtual void CreateCasePins() override;
	virtual void RemovePin(UEdGraphPin* TargetPin) override;

}

UK2Node_SwitchClass .cpp

UK2Node_SwitchClass::UK2Node_SwitchClass(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	FunctionName = TEXT("NotEqual_ClassClass");
	FunctionClass = UKismetMathLibrary::StaticClass();
	OrphanedPinSaveMode = ESaveOrphanPinMode::SaveNone;
}
void UK2Node_SwitchClass::AddPinToSwitchNode()
{
	PinClasses.Add(nullptr);
	const FName PinName = GetUniquePinName();
	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, PinName);
}
FName UK2Node_SwitchClass::GetUniquePinName()
{
	FName NewPinName;
	int32 Index = 0;
	while (true)
	{
		NewPinName = *FString::Printf(TEXT("Case_%d"), Index++);
		if (!FindPin(NewPinName))
		{
			break;
		}
	}
	return NewPinName;
}
FEdGraphPinType UK2Node_SwitchClass::GetPinType() const
{
	FEdGraphPinType PinType;
	PinType.PinCategory = UEdGraphSchema_K2::PC_Class;
	PinType.PinSubCategoryObject = UObject::StaticClass();
	return PinType;
}
FName UK2Node_SwitchClass::GetPinNameGivenIndex(int32 Index) const
{
	return PinClasses.IsValidIndex(Index) ? PinClasses[Index]->GetFName() : "Invalid";
}
void UK2Node_SwitchClass::CreateSelectionPin()
{
	const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
	UEdGraphPin* Pin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Class, UObject::StaticClass(), TEXT("Selection"));
	K2Schema->SetPinAutogeneratedDefaultValueBasedOnType(Pin);
}
void UK2Node_SwitchClass::CreateCasePins()
{
	for (int32 i = 0; i < PinClasses.Num(); ++i)
	{
		FName PinName = PinClasses[i] ? PinClasses[i]->GetFName() : GetUniquePinName();
		UEdGraphPin* NewPin = CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, PinName);
		NewPin->PinFriendlyName = FText::FromName(PinName);
	}
}
void UK2Node_SwitchClass::RemovePin(UEdGraphPin* TargetPin)
{
	checkSlow(TargetPin);
	const FName PinName = TargetPin->PinName;
	for (int32 i = 0; i < PinClasses.Num(); ++i)
	{
		const auto PinClass = PinClasses[i];
		if (PinClass->GetFName() == PinName)
		{
			PinClasses.RemoveAt(i);
			break;
		}
	}
}

UKismetMathLibrary.ini

KISMET_MATH_FORCEINLINE
bool UKismetMathLibrary::NotEqual_ClassClass(class UClass* A, class UClass* B)
{
	return A != B;
}

FKCHandler_Switch is where the node compiles:

virtual void Compile(FKismetFunctionContext& Context, UEdGraphNode* Node) override
	{
		UK2Node_Switch* SwitchNode = CastChecked<UK2Node_Switch>(Node);

		FEdGraphPinType ExpectedExecPinType;
		ExpectedExecPinType.PinCategory = UEdGraphSchema_K2::PC_Exec;

		// Make sure that the input pin is connected and valid for this block
		UEdGraphPin* ExecTriggeringPin = Context.FindRequiredPinByName(SwitchNode, UEdGraphSchema_K2::PN_Execute, EGPD_Input);
		if ((ExecTriggeringPin == NULL) || !Context.ValidatePinType(ExecTriggeringPin, ExpectedExecPinType))
		{
			CompilerContext.MessageLog.Error(*LOCTEXT("NoValidExecutionPinForSwitch_Error", "@@ must have a valid execution pin @@").ToString(), SwitchNode, ExecTriggeringPin);
			return;
		}

		// Make sure that the selection pin is connected and valid for this block
		UEdGraphPin* SelectionPin = SwitchNode->GetSelectionPin();
		if ((SelectionPin == NULL) || !Context.ValidatePinType(SelectionPin, SwitchNode->GetPinType()))
		{
			CompilerContext.MessageLog.Error(*LOCTEXT("NoValidSelectionPinForSwitch_Error", "@@ must have a valid execution pin @@").ToString(), SwitchNode, SelectionPin);
			return;
		}

		// Find the boolean intermediate result term, so we can track whether the compare was successful
		FBPTerminal* BoolTerm = BoolTermMap.FindRef(SwitchNode);

		// Generate the output impulse from this node
		UEdGraphPin* SwitchSelectionNet = FEdGraphUtilities::GetNetFromPin(SelectionPin);
		FBPTerminal* SwitchSelectionTerm = Context.NetMap.FindRef(SwitchSelectionNet);

		if ((BoolTerm != NULL) && (SwitchSelectionTerm != NULL))
		{
			UEdGraphPin* FuncPin = SwitchNode->GetFunctionPin();
			FBPTerminal* FuncContext = Context.NetMap.FindRef(FuncPin);
			UEdGraphPin* DefaultPin = SwitchNode->GetDefaultPin();
			
			// We don't need to generate if checks if there are no connections to it if there is no default pin or if the default pin is not linked 
			// If there is a default pin that is linked then it would fall through to that default if we do not generate the cases
			const bool bCanSkipUnlinkedCase = (DefaultPin == nullptr || DefaultPin->LinkedTo.Num() == 0);

			// Pull out function to use
			UClass* FuncClass = Cast<UClass>(FuncPin->PinType.PinSubCategoryObject.Get());
			UFunction* FunctionPtr = FindUField<UFunction>(FuncClass, FuncPin->PinName);
			check(FunctionPtr);

			// Run thru all the output pins except for the default label
			for (auto PinIt = SwitchNode->Pins.CreateIterator(); PinIt; ++PinIt)
			{
				UEdGraphPin* Pin = *PinIt;

				if ((Pin->Direction == EGPD_Output) && (Pin != DefaultPin) && (!bCanSkipUnlinkedCase || Pin->LinkedTo.Num() > 0))
				{
					// Create a term for the switch case value
					FBPTerminal* CaseValueTerm = new FBPTerminal();
					Context.Literals.Add(CaseValueTerm);
					CaseValueTerm->Name = SwitchNode->GetExportTextForPin(Pin);
					CaseValueTerm->Type = SwitchNode->GetInnerCaseType();
					CaseValueTerm->SourcePin = Pin;
					CaseValueTerm->bIsLiteral = true;

					// Call the comparison function associated with this switch node
					FBlueprintCompiledStatement& Statement = Context.AppendStatementForNode(SwitchNode);
					Statement.Type = KCST_CallFunction;
					Statement.FunctionToCall = FunctionPtr;
					Statement.FunctionContext = FuncContext;
					Statement.bIsParentContext = false;

					Statement.LHS = BoolTerm;
					Statement.RHS.Add(SwitchSelectionTerm);
					Statement.RHS.Add(CaseValueTerm);

					// Jump to output if strings are actually equal
					FBlueprintCompiledStatement& IfFailTest_SucceedAtBeingEqualGoto = Context.AppendStatementForNode(SwitchNode);
					IfFailTest_SucceedAtBeingEqualGoto.Type = KCST_GotoIfNot;
					IfFailTest_SucceedAtBeingEqualGoto.LHS = BoolTerm;

					Context.GotoFixupRequestMap.Add(&IfFailTest_SucceedAtBeingEqualGoto, Pin);
				}
			}

			// Finally output default pin
			GenerateSimpleThenGoto(Context, *SwitchNode, DefaultPin);
		}
		else
		{
			CompilerContext.MessageLog.Error(*LOCTEXT("ResolveTermPassed_Error", "Failed to resolve term passed into @@").ToString(), SelectionPin);
		}
	}

Update:

By replicating UKismetMathLibrary::NotEqual_ClassClass, I was able to breakpoint the function, determining that the RHS (B) is not set.

bool UKismetMathLibrary::NotEqual_ClassClass(class UClass* A, class UClass* B)
{
	return A != B;
}


Still unsure as to where the issue is originating from. Any help would be appreciated.

(post deleted by author)

I’ve been able to have the function pass a reference to the executing Object using PinType.PinSubCategory = UEdGraphSchema_K2::PN_Self. Obviously, this isn’t intended, but it does validate the theory that something is wrong with the Case Pins.


Overriding UK2Node_Switch::GetInnerCaseType allows you to specify the RHS pin type irrespective of the LHS “Selector” pin.

FEdGraphPinType UK2Node_SwitchClass::GetPinType() const
{
	FEdGraphPinType PinType;
	PinType.PinCategory = UEdGraphSchema_K2::PC_Class;
	PinType.PinSubCategoryObject = UObject::StaticClass();
	return PinType;
}

FEdGraphPinType UK2Node_SwitchClass::GetInnerCaseType() const
{
	FEdGraphPinType PinType;
	PinType.PinCategory = UEdGraphSchema_K2::PC_Class;
	PinType.PinSubCategory = UEdGraphSchema_K2::PN_Self;
	//PinType.PinSubCategoryObject = UObject::StaticClass();
	return PinType;
}

no disrespect intended but i dont think this is a good idea.
Its seems like its basically an interface but worse?

ie how would you handle subclasses? an Actor/Character are technically both actors but would require different pins?

Happy to be proven wrong though..

I don’t see how it’s relevant; the questions here are not architectural. This is purely about extending a K2Node to support additional functionality.

But, if you’re curious, this uses UKismetMathLibrary::NotEqual_ClassClass, not ClassIsChildOf. The Classes must be equal to satisfy the switch, and thus, subclasses are ignored by design and intent. An alternative function would have been selected if subclasses were considered valid results.

The use cases are literally to differentiate between Actors, Characters, more inherited types, and even seemingly unassociated Objects, or uninheritable Widget Blueprints, where adding an Interface is unwarranted or requires modification of core types or plugins and breaking forward compatibility. Sure, there might be other ways to do it, but this is intuitive and cuts down on spaghetti nodes with what should be a lightweight extension to switch nodes.

well then my only guess is little guy here

void UK2Node_SwitchClass::AddPinToSwitchNode()
{
	PinClasses.Add(nullptr);
	const FName PinName = GetUniquePinName();
	CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, PinName);
}

you’re adding nullptr?

That’s called when a developer initially adds a new (albeit) invalid pin.


Assignment comes when a developer sets the output pin value.


You can find similar functionality in UGameplayTagsK2Node_SwitchGameplayTag and UGameplayTagsK2Node_SwitchGameplayTagContainer, both of which operate on structs. A key difference is that the type of RHS argument in their target functions are Strings.

UK2Node_Switch is a pretty interesting but very complex K2Node. Hence the current issue.

agreed i’ve made a few of my own and looked into this one for ya, hence my original post :wink:

i still feel this may be the error though, (you could try hardcoding a class just to test) where are you updating it? ie i use PostEditChangeProperty()

i mean we know the pin exists, its just returning nullptr. So maybe option B if you cant find the source is just to compare Pin FNames instead of Classes?

You are never really assigning the UClass* reference to the RHS terminal.

RHS is of type PC_Class, but its associated pin is an Exec pin.

The existing switches (string,name,int,enum) rely on some fallback mechanism implemented in function EmitTermExpr (KismetCompiletVMBackend.cpp), which coerces back the Pin Name into the desired type.
Couple examples :

However, for Object and Class types it doesn’t coerce the Pin Name, it expects the Term’s ObjectLiteral to be set :

So you should be able to fix it by setting the ObjectLiteral, kinda like this :

int32 PinClassIndex = 0;

// Run thru all the output pins except for the default label
for (auto PinIt = SwitchNode->Pins.CreateIterator(); PinIt; ++PinIt)
{
	//...

	CaseValueTerm->ObjectLiteral = PinClasses[PinClassIndex++];

This is not the most elegant, but hopefully should do the job.

2 Likes

This will work. It’s weird though that you need to set ObjectLiteral inside FKCHandler_Switch and not directly inside UK2Node_Switch. This requires creating essentially a copy of FKCHandler_Switch with the slight modification you specified. It seems to me that the developers simply forgot (or never thought) to do this functionality.