Why don't "Expose on Spawn" variables show as input nodes on Spawn AI From Class nodes?

5.3.2, and still not fixed!

This is a pretty major flaw to have been ignored for over eight years now!

1 Like

BUMP!

I am curious about “WHY” SpawnAI doesn’t have this feature.

My guess is that Epic doesn’t focus on NPC creation and focuses more about other/bigger features in UE like MetaHuman, UE improvements/optimisation, etc. . That would explain why BehaviorTree is also so underdeveloped… which is all in all realy sad. But that are just guesses, I would like to know the exact reasons behind the missing “spawn on expose” feature in “SpawnAI”.

1 Like

I expent the last 6 hours trying to make this bp,
I am not an expert on cpp right now but I was able to show the exposed variables when class is selected and the pin of BehaviorTree if you refresh the node or copy/paste it.
the image show two times Bheaviour tree because I tried to se the BehaviorTree with other name, but without luck.
if I connect to the flow, it will complain (after you put 0,0,0 as location of course this is mandatory)
COMPILER ERROR: failed building connection with ‘Only exactly matching structures are considered compatible.’ at SpawnAIWithExposedVars BC MyChar

I tried with chatgpt, it might be because of this:

The pin is set to be part of the advanced view (`BehaviorTreePin->bAdvancedView = true;`), which means it might be hidden depending on the pin display setting.

So probably something somewhere is pushing Behavior tree to be hidden also, maybe. My cpp knowledge is basic. If somebody make this happen, pls let me know.

I leave here my classes, I invested a lot of time in this…for nothing, so I will just cast for now.

SGraphNodeSpawnAiWithExposedVarsFromClass.h

// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include "KismetNodes/SGraphNodeK2Default.h"

class SGraphNodeSpawnAiWithExposedVarsFromClass : public SGraphNodeK2Default
{
public:
	// SGraphNode interface
	virtual void CreatePinWidgets() override;
	// End of SGraphNode interface
};

SGraphNodeSpawnAiWithExposedVarsFromClass.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "SGraphNodeSpawnAiWithExposedVarsFromClass.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_SpawnAiWithExposedVarsFromClass.h"
#include "KismetPins/SGraphPinClass.h"
#include "NodeFactory.h"

void SGraphNodeSpawnAiWithExposedVarsFromClass::CreatePinWidgets()
{
	UK2Node_SpawnAiWithExposedVarsFromClass* SpawnAiNode = CastChecked<UK2Node_SpawnAiWithExposedVarsFromClass>(GraphNode);
	UEdGraphPin* ClassPin = SpawnAiNode->GetClassPin();

	for (auto PinIt = GraphNode->Pins.CreateConstIterator(); PinIt; ++PinIt)
	{
		UEdGraphPin* CurrentPin = *PinIt;

		// Force the BehaviorTreePin to be visible
		if (CurrentPin->PinName == FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName)
		{
			CurrentPin->bHidden = false;
		}

		if ((!CurrentPin->bHidden) && (CurrentPin != ClassPin))
		{
			TSharedPtr<SGraphPin> NewPin = FNodeFactory::CreatePinWidget(CurrentPin);
			check(NewPin.IsValid());
			this->AddPin(NewPin.ToSharedRef());
		}
		else if ((ClassPin == CurrentPin) && (!ClassPin->bHidden || (ClassPin->LinkedTo.Num() > 0)))
		{
			TSharedPtr<SGraphPinClass> NewPin = SNew(SGraphPinClass, ClassPin);
			check(NewPin.IsValid());
			this->AddPin(NewPin.ToSharedRef());
		}
	}
}

K2Node_SpawnAiWithExposedVarsFromClass.h

// Copyright Epic Games, Inc. All Rights Reserved.


#pragma once

#include "Containers/Array.h"
#include "CoreMinimal.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphNodeUtils.h"
#include "Internationalization/Text.h"
#include "K2Node.h"
#include "K2Node_ConstructObjectFromClass.h"
#include "K2Node_GenericCreateObject.h"
#include "KismetCompilerMisc.h"
#include "Textures/SlateIcon.h"
#include "UObject/ObjectMacros.h"
#include "UObject/UObjectGlobals.h"

#include "K2Node_SpawnAiWithExposedVarsFromClass.generated.h"

class FBlueprintActionDatabaseRegistrar;
class FString;
class UClass;
class UEdGraph;
class UEdGraphPin;
class UObject;
struct FLinearColor;
template <typename KeyType, typename ValueType> struct TKeyValuePair;

UCLASS()
class TOPDOWN_API UK2Node_SpawnAiWithExposedVarsFromClass : public UK2Node_ConstructObjectFromClass
{
	GENERATED_UCLASS_BODY()

	//~ Begin UEdGraphNode Interface.
	virtual void AllocateDefaultPins() override;
	virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override;
	virtual void GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const override;
	virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override;
	virtual FSlateIcon GetIconAndTint(FLinearColor& OutColor) const override;
	virtual bool IsCompatibleWithGraph(const UEdGraph* TargetGraph) const override;
	virtual void PostPlacedNewNode() override;
	//~ End UEdGraphNode Interface.

	//~ Begin UObject Interface
	virtual void Serialize(FArchive& Ar) override;
	virtual void PostLoad() override;
	//~ End UObject Interface
	
	//~ Begin UK2Node Interface
	virtual bool IsNodeSafeToIgnore() const override { return true; }
	virtual void ReallocatePinsDuringReconstruction(TArray<UEdGraphPin*>& OldPins) override;
	virtual void GetNodeAttributes( TArray<TKeyValuePair<FString, FString>>& OutNodeAttributes ) const override;
	virtual class FNodeHandlingFunctor* CreateNodeHandler(class FKismetCompilerContext& CompilerContext) const override;
	//~ End UK2Node Interface

	//~ Begin UK2Node_ConstructObjectFromClass Interface
	virtual UClass* GetClassPinBaseClass() const;
	virtual bool IsSpawnVarPin(UEdGraphPin* Pin) const override;
	//~ End UK2Node_ConstructObjectFromClass Interface

	
private:
	void FixupScaleMethodPin();
	
	/** Get the spawn transform input pin */	
	UEdGraphPin* GetSpawnTransformPin() const;
	/** Get the collision handling method input pin */
	UEdGraphPin* GetCollisionHandlingOverridePin() const;
	/** Get the collision handling method input pin */
	UEdGraphPin* GetScaleMethodPin() const;
	UEdGraphPin* TryGetScaleMethodPin() const;
	/** Get the actor owner pin */
	UEdGraphPin* GetOwnerPin() const;

	void MaybeUpdateCollisionPin(TArray<UEdGraphPin*>& OldPins);
};

// Add this struct definition at the end of the file, or after the necessary includes.
struct FK2Node_SpawnAiWithExposedVarsFromClassHelper
{
	static const FName SpawnTransformPinName;
	static const FName SpawnEvenIfCollidingPinName;
	static const FName NoCollisionFailPinName;
	static const FName CollisionHandlingOverridePinName;
	static const FName TransformScaleMethodPinName;
	static const FName OwnerPinName;
	static const FName BehaviorTreePinName;
};

K2Node_SpawnAiWithExposedVarsFromClass.cpp

// Copyright Epic Games, Inc. All Rights Reserved.

#include "K2Node_SpawnAiWithExposedVarsFromClass.h"

#include "BehaviorTree/BehaviorTree.h"  // Include for UBehaviorTree
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "Containers/EnumAsByte.h"
#include "Containers/UnrealString.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNodeUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "Engine/Blueprint.h"
#include "Engine/EngineTypes.h"
#include "Engine/MemberReference.h"
#include "GameFramework/Actor.h"
#include "HAL/PlatformCrt.h"
#include "HAL/PlatformMath.h"
#include "Internationalization/Internationalization.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_EnumLiteral.h"
#include "K2Node_Select.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/CompilerResultsLog.h"
#include "KismetCompiler.h"
#include "KismetCompilerMisc.h"
#include "Math/Transform.h"
#include "Misc/AssertionMacros.h"
#include "Styling/AppStyle.h"
#include "Templates/Casts.h"
#include "Templates/SubclassOf.h"
#include "Templates/UnrealTemplate.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "UObject/Object.h"
#include "UObject/ObjectPtr.h"
#include "UObject/UE5MainStreamObjectVersion.h"

struct FLinearColor;

const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnTransformPinName(TEXT("SpawnTransform"));
const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnEvenIfCollidingPinName(TEXT("SpawnEvenIfColliding"));		// deprecated pin, name kept for backwards compat
const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::NoCollisionFailPinName(TEXT("bNoCollisionFail"));		// deprecated pin, name kept for backwards compat
const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::CollisionHandlingOverridePinName(TEXT("CollisionHandlingOverride"));
const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::TransformScaleMethodPinName(TEXT("TransformScaleMethod"));
const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::OwnerPinName(TEXT("Owner"));
const FName FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName(TEXT("BehaviorTree"));

#define LOCTEXT_NAMESPACE "K2Node_SpawnAiWithExposedVarsFromClass"

UK2Node_SpawnAiWithExposedVarsFromClass::UK2Node_SpawnAiWithExposedVarsFromClass(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	NodeTooltip = LOCTEXT("NodeTooltip", "Attempts to spawn a new AI with the specified transform");
}

UClass* UK2Node_SpawnAiWithExposedVarsFromClass::GetClassPinBaseClass() const
{
	return AActor::StaticClass();
}

void UK2Node_SpawnAiWithExposedVarsFromClass::AllocateDefaultPins()
{
	Super::AllocateDefaultPins();

	// Transform pin
	UScriptStruct* TransformStruct = TBaseStructure<FTransform>::Get();
	UEdGraphPin* TransformPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, TransformStruct, FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnTransformPinName);

	// Collision handling method pin
	UEnum* const CollisionMethodEnum = StaticEnum<ESpawnActorCollisionHandlingMethod>();
	UEdGraphPin* const CollisionHandlingOverridePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, CollisionMethodEnum, FK2Node_SpawnAiWithExposedVarsFromClassHelper::CollisionHandlingOverridePinName);
	CollisionHandlingOverridePin->DefaultValue = CollisionMethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorCollisionHandlingMethod::Undefined));

	// Pin to set transform scaling behavior
	UEnum* const ScaleMethodEnum = StaticEnum<ESpawnActorScaleMethod>();
	UEdGraphPin* const ScaleMethodPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, ScaleMethodEnum, FK2Node_SpawnAiWithExposedVarsFromClassHelper::TransformScaleMethodPinName);
	ScaleMethodPin->DefaultValue = ScaleMethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorScaleMethod::SelectDefaultAtRuntime));
	ScaleMethodPin->bAdvancedView = true;

	// Owner pin
	UEdGraphPin* OwnerPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, AActor::StaticClass(), FK2Node_SpawnAiWithExposedVarsFromClassHelper::OwnerPinName);
	OwnerPin->bAdvancedView = true;

	// Create the original BehaviorTreePin
	UEdGraphPin* BehaviorTreePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UBehaviorTree::StaticClass(), FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName);
	BehaviorTreePin->bAdvancedView = true;

	// Create a duplicate BehaviorTreePin with a different name to prevent it from being hidden
	UEdGraphPin* BehaviorTreePinDuplicate = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UBehaviorTree::StaticClass(), TEXT("VisibleBehaviorTreePin"));
	BehaviorTreePinDuplicate->bAdvancedView = false;  // This pin should always be visible

	if (ENodeAdvancedPins::NoPins == AdvancedPinDisplay)
	{
		AdvancedPinDisplay = ENodeAdvancedPins::Hidden;
	}

	if (ENodeAdvancedPins::NoPins == AdvancedPinDisplay)
	{
		AdvancedPinDisplay = ENodeAdvancedPins::Hidden;
	}
}


void UK2Node_SpawnAiWithExposedVarsFromClass::MaybeUpdateCollisionPin(TArray<UEdGraphPin*>& OldPins)
{
	// see if there's a bNoCollisionFail pin
	for (UEdGraphPin* Pin : OldPins)
	{
		if (Pin->PinName == FK2Node_SpawnAiWithExposedVarsFromClassHelper::NoCollisionFailPinName || Pin->PinName == FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnEvenIfCollidingPinName)
		{
			bool bHadOldCollisionPin = true;
			if (Pin->LinkedTo.Num() == 0)
			{
				// no links, use the default value of the pin
				bool const bOldCollisionPinValue = (Pin->DefaultValue == FString(TEXT("true")));

				UEdGraphPin* const CollisionHandlingOverridePin = GetCollisionHandlingOverridePin();
				if (CollisionHandlingOverridePin)
				{
					UEnum const* const MethodEnum = FindObjectChecked<UEnum>(nullptr, TEXT("/Script/Engine.ESpawnActorCollisionHandlingMethod"), true);
					CollisionHandlingOverridePin->DefaultValue =
						bOldCollisionPinValue
						? MethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorCollisionHandlingMethod::AlwaysSpawn))
						: MethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding));
				}
			}
			else
			{
				// something was linked. we will just move the links to the new pin
				// #note: this will be an invalid linkage and the BP compiler will complain, and that's intentional
				// so that users will be able to see and fix issues
				UEdGraphPin* const CollisionHandlingOverridePin = GetCollisionHandlingOverridePin();
				check(CollisionHandlingOverridePin);

				UEnum* const MethodEnum = FindObjectChecked<UEnum>(nullptr, TEXT("/Script/Engine.ESpawnActorCollisionHandlingMethod"), true);
				
				FGraphNodeCreator<UK2Node_EnumLiteral> AlwaysSpawnLiteralCreator(*GetGraph());
				UK2Node_EnumLiteral* const AlwaysSpawnLiteralNode = AlwaysSpawnLiteralCreator.CreateNode();
				AlwaysSpawnLiteralNode->Enum = MethodEnum;
				AlwaysSpawnLiteralNode->NodePosX = NodePosX;
				AlwaysSpawnLiteralNode->NodePosY = NodePosY;
				AlwaysSpawnLiteralCreator.Finalize();
			
				FGraphNodeCreator<UK2Node_EnumLiteral> AdjustIfNecessaryLiteralCreator(*GetGraph());
				UK2Node_EnumLiteral* const AdjustIfNecessaryLiteralNode = AdjustIfNecessaryLiteralCreator.CreateNode();
				AdjustIfNecessaryLiteralNode->Enum = MethodEnum;
				AdjustIfNecessaryLiteralNode->NodePosX = NodePosX;
				AdjustIfNecessaryLiteralNode->NodePosY = NodePosY;
				AdjustIfNecessaryLiteralCreator.Finalize();

				FGraphNodeCreator<UK2Node_Select> SelectCreator(*GetGraph());
				UK2Node_Select* const SelectNode = SelectCreator.CreateNode();
				SelectNode->NodePosX = NodePosX;
				SelectNode->NodePosY = NodePosY;
				SelectCreator.Finalize();

				// find pins we want to set and link up
				auto FindEnumInputPin = [](UK2Node_EnumLiteral const* Node)
				{
					for (UEdGraphPin* NodePin : Node->Pins)
					{
						if (NodePin->PinName == Node->GetEnumInputPinName())
						{
							return NodePin;
						}
					}
					return (UEdGraphPin*)nullptr;
				};

				UEdGraphPin* const AlwaysSpawnLiteralNodeInputPin = FindEnumInputPin(AlwaysSpawnLiteralNode);
				UEdGraphPin* const AdjustIfNecessaryLiteralInputPin = FindEnumInputPin(AdjustIfNecessaryLiteralNode);

				TArray<UEdGraphPin*> SelectOptionPins;
				SelectNode->GetOptionPins(SelectOptionPins);
				UEdGraphPin* const SelectIndexPin = SelectNode->GetIndexPin();

				auto FindResultPin = [](UK2Node const* Node)
				{
					for (UEdGraphPin* NodePin : Node->Pins)
					{
						if (EEdGraphPinDirection::EGPD_Output == NodePin->Direction)
						{
							return NodePin;
						}
					}
					return (UEdGraphPin*)nullptr;
				};
				UEdGraphPin* const AlwaysSpawnLiteralNodeResultPin = FindResultPin(AlwaysSpawnLiteralNode);
				check(AlwaysSpawnLiteralNodeResultPin);
				UEdGraphPin* const AdjustIfNecessaryLiteralResultPin = FindResultPin(AdjustIfNecessaryLiteralNode);
				check(AdjustIfNecessaryLiteralResultPin);

				UEdGraphPin* const OldBoolPin = Pin->LinkedTo[0];
				check(OldBoolPin);

				//
				// now set data and links that we want to set
				//

				AlwaysSpawnLiteralNodeInputPin->DefaultValue = MethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
				AdjustIfNecessaryLiteralInputPin->DefaultValue = MethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding));

				OldBoolPin->BreakLinkTo(Pin);
				OldBoolPin->MakeLinkTo(SelectIndexPin);

				AlwaysSpawnLiteralNodeResultPin->MakeLinkTo(SelectOptionPins[0]);
				AdjustIfNecessaryLiteralResultPin->MakeLinkTo(SelectOptionPins[1]);
				
				UEdGraphPin* const SelectOutputPin = SelectNode->GetReturnValuePin();
				check(SelectOutputPin);
				SelectOutputPin->MakeLinkTo(CollisionHandlingOverridePin);

				// tell select node to update its wildcard status
				SelectNode->NotifyPinConnectionListChanged(SelectIndexPin);
				SelectNode->NotifyPinConnectionListChanged(SelectOptionPins[0]);
				SelectNode->NotifyPinConnectionListChanged(SelectOptionPins[1]);
				SelectNode->NotifyPinConnectionListChanged(SelectOutputPin);

			}
		}
	}
}

void UK2Node_SpawnAiWithExposedVarsFromClass::ReallocatePinsDuringReconstruction(TArray<UEdGraphPin*>& OldPins)
{
	Super::ReallocatePinsDuringReconstruction(OldPins);

	// Ensure the original and duplicate BehaviorTree pins are present
	UEdGraphPin* BehaviorTreePin = FindPin(FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName);
	if (!BehaviorTreePin)
	{
		BehaviorTreePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UBehaviorTree::StaticClass(), FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName);
		BehaviorTreePin->bAdvancedView = true;
	}

	UEdGraphPin* BehaviorTreePinDuplicate = FindPin(TEXT("VisibleBehaviorTreePin"));
	if (!BehaviorTreePinDuplicate)
	{
		BehaviorTreePinDuplicate = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UBehaviorTree::StaticClass(), TEXT("VisibleBehaviorTreePin"));
		BehaviorTreePinDuplicate->bAdvancedView = false;
	}

	// Move existing connections from old pins to the new pins
	for (UEdGraphPin* OldPin : OldPins)
	{
		if (OldPin->PinName == FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName ||
			OldPin->PinName == TEXT("VisibleBehaviorTreePin"))
		{
			if (OldPin->LinkedTo.Num() > 0)
			{
				for (UEdGraphPin* LinkedPin : OldPin->LinkedTo)
				{
					LinkedPin->Modify();  // Mark the linked pin as modified
					LinkedPin->BreakLinkTo(OldPin);
					LinkedPin->MakeLinkTo(BehaviorTreePinDuplicate);  // Connect to the duplicate pin
				}
			}
		}
	}
}

bool UK2Node_SpawnAiWithExposedVarsFromClass::IsSpawnVarPin(UEdGraphPin* Pin) const
{
	UEdGraphPin* ParentPin = Pin->ParentPin;
	while (ParentPin)
	{
		if (ParentPin->PinName == FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnTransformPinName)
		{
			return false;
		}
		ParentPin = ParentPin->ParentPin;
	}

	return(	Super::IsSpawnVarPin(Pin) &&
			Pin->PinName != FK2Node_SpawnAiWithExposedVarsFromClassHelper::TransformScaleMethodPinName &&
			Pin->PinName != FK2Node_SpawnAiWithExposedVarsFromClassHelper::CollisionHandlingOverridePinName &&
			Pin->PinName != FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnTransformPinName && 
			Pin->PinName != FK2Node_SpawnAiWithExposedVarsFromClassHelper::OwnerPinName );
}

void UK2Node_SpawnAiWithExposedVarsFromClass::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const
{
	const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();

	if (UEdGraphPin* TransformPin = GetSpawnTransformPin())
	{
		K2Schema->ConstructBasicPinTooltip(*TransformPin, LOCTEXT("TransformPinDescription", "The transform to spawn the Actor with"), TransformPin->PinToolTip);
	}
	if (UEdGraphPin* CollisionHandlingOverridePin = GetCollisionHandlingOverridePin())
	{
		K2Schema->ConstructBasicPinTooltip(*CollisionHandlingOverridePin, LOCTEXT("CollisionHandlingOverridePinDescription", "Specifies how to handle collisions at the spawn point. If undefined, uses actor class settings."), CollisionHandlingOverridePin->PinToolTip);
	}
	if (UEdGraphPin* OwnerPin = GetOwnerPin())
	{
		K2Schema->ConstructBasicPinTooltip(*OwnerPin, LOCTEXT("OwnerPinDescription", "Can be left empty; primarily used for replication (bNetUseOwnerRelevancy and bOnlyRelevantToOwner), or visibility (PrimitiveComponent's bOwnerNoSee/bOnlyOwnerSee)"), OwnerPin->PinToolTip);
	}

	return Super::GetPinHoverText(Pin, HoverTextOut);
}

FSlateIcon UK2Node_SpawnAiWithExposedVarsFromClass::GetIconAndTint(FLinearColor& OutColor) const
{
	static FSlateIcon Icon(FAppStyle::GetAppStyleSetName(), "GraphEditor.SpawnActor_16x");
	return Icon;
}

UEdGraphPin* UK2Node_SpawnAiWithExposedVarsFromClass::GetSpawnTransformPin() const
{
	UEdGraphPin* Pin = FindPinChecked(FK2Node_SpawnAiWithExposedVarsFromClassHelper::SpawnTransformPinName);
	check(Pin->Direction == EGPD_Input);
	return Pin;
}

UEdGraphPin* UK2Node_SpawnAiWithExposedVarsFromClass::GetCollisionHandlingOverridePin() const
{
	UEdGraphPin* const Pin = FindPinChecked(FK2Node_SpawnAiWithExposedVarsFromClassHelper::CollisionHandlingOverridePinName);
	check(Pin->Direction == EGPD_Input);
	return Pin;
}

UEdGraphPin* UK2Node_SpawnAiWithExposedVarsFromClass::GetScaleMethodPin() const
{
	UEdGraphPin* const Pin = FindPinChecked(FK2Node_SpawnAiWithExposedVarsFromClassHelper::TransformScaleMethodPinName);
	check(Pin->Direction == EGPD_Input);
	return Pin;
}

UEdGraphPin* UK2Node_SpawnAiWithExposedVarsFromClass::TryGetScaleMethodPin() const
{
	return FindPin(FK2Node_SpawnAiWithExposedVarsFromClassHelper::TransformScaleMethodPinName, EGPD_Input);
}

UEdGraphPin* UK2Node_SpawnAiWithExposedVarsFromClass::GetOwnerPin() const
{
	UEdGraphPin* Pin = FindPin(FK2Node_SpawnAiWithExposedVarsFromClassHelper::OwnerPinName);
	check(Pin == nullptr || Pin->Direction == EGPD_Input);
	return Pin;
}

FText UK2Node_SpawnAiWithExposedVarsFromClass::GetNodeTitle(ENodeTitleType::Type TitleType) const
{
	FText NodeTitle = NSLOCTEXT("K2Node", "SpawnAiWithExposedVars_BaseTitle", "Spawn AI with Exposed Vars from Class");
	if (TitleType != ENodeTitleType::MenuTitle)
	{
		if (UEdGraphPin* ClassPin = GetClassPin())
		{
			if (ClassPin->LinkedTo.Num() > 0)
			{
				// Blueprint will be determined dynamically, so we don't have the name in this case
				NodeTitle = NSLOCTEXT("K2Node", "SpawnAiWithExposedVars_Title_Unknown", "SpawnAIWithExposedVars");
			}
			else if (ClassPin->DefaultObject == nullptr)
			{
				NodeTitle = NSLOCTEXT("K2Node", "SpawnAiWithExposedVars_Title_NONE", "SpawnAIWithExposedVars NONE");
			}
			else
			{
				if (CachedNodeTitle.IsOutOfDate(this))
				{
					FText ClassName;
					if (UClass* PickedClass = Cast<UClass>(ClassPin->DefaultObject))
					{
						ClassName = PickedClass->GetDisplayNameText();
					}

					FFormatNamedArguments Args;
					Args.Add(TEXT("ClassName"), ClassName);

					// FText::Format() is slow, so we cache this to save on performance
					CachedNodeTitle.SetCachedText(FText::Format(NSLOCTEXT("K2Node", "SpawnAiWithExposedVars_Title_Class", "SpawnAIWithExposedVars {ClassName}"), Args), this);
				}
				NodeTitle = CachedNodeTitle;
			} 
		}
		else
		{
			NodeTitle = NSLOCTEXT("K2Node", "SpawnAiWithExposedVars_Title_NONE", "SpawnAIWithExposedVars NONE");
		}
	}
	return NodeTitle;
}

bool UK2Node_SpawnAiWithExposedVarsFromClass::IsCompatibleWithGraph(const UEdGraph* TargetGraph) const 
{
	UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(TargetGraph);
	return Super::IsCompatibleWithGraph(TargetGraph) && (!Blueprint || (FBlueprintEditorUtils::FindUserConstructionScript(Blueprint) != TargetGraph && Blueprint->GeneratedClass->GetDefaultObject()->ImplementsGetWorld()));
}

void UK2Node_SpawnAiWithExposedVarsFromClass::PostPlacedNewNode()
{
	UEdGraphPin* const ScaleMethodPin = GetScaleMethodPin();
	const UEnum* const ScaleMethodEnum = StaticEnum<ESpawnActorScaleMethod>();
	ScaleMethodPin->DefaultValue = ScaleMethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorScaleMethod::MultiplyWithRoot));
}

void UK2Node_SpawnAiWithExposedVarsFromClass::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);
	Ar.UsingCustomVersion(FUE5MainStreamObjectVersion::GUID);
}

void UK2Node_SpawnAiWithExposedVarsFromClass::PostLoad()
{
	FixupScaleMethodPin();
	Super::PostLoad();
}

void UK2Node_SpawnAiWithExposedVarsFromClass::FixupScaleMethodPin()
{
	// if this node is being diffed, don't fix up anything. Keep the legacy pins
	const UPackage* Package = GetPackage();
	if (Package && Package->HasAnyPackageFlags(PKG_ForDiffing))
	{
		return;
	}

	// For the sake of cook determinism, leave the scale method as ESpawnActorScaleMethod::SelectDefaultAtRuntime when cooking.
	// This shouldn't impact runtime behavior.
	if (IsRunningCookCommandlet())
	{
		return;
	}

	if (GetLinkerCustomVersion(FUE5MainStreamObjectVersion::GUID) < FUE5MainStreamObjectVersion::SpawnActorFromClassTransformScaleMethod)
	{
        if (UEdGraphPin* const ScaleMethodPin = TryGetScaleMethodPin())
        {
			const UEdGraphPin* const ClassPin = FindPin(TEXT( "Class" ));
	        const UEnum* const ScaleMethodEnum = StaticEnum<ESpawnActorScaleMethod>();
            if (const UClass* Class = Cast<UClass>(ClassPin->DefaultObject))
            {
            	if (const AActor* ActorCDO = Cast<AActor>(Class->ClassDefaultObject))
                {
                    if (const USceneComponent* Root = ActorCDO->GetRootComponent()) // native root component
                    {
                        ScaleMethodPin->DefaultValue = ScaleMethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorScaleMethod::MultiplyWithRoot));
                    }
                    else
                    {
                        ScaleMethodPin->DefaultValue = ScaleMethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorScaleMethod::OverrideRootScale));
                    }
                }
            }
            else
            {
            	// if the class can't be determined during compile time, defer the default value to runtime
            	ScaleMethodPin->DefaultValue = ScaleMethodEnum->GetNameStringByValue(static_cast<int>(ESpawnActorScaleMethod::SelectDefaultAtRuntime));
            }
        }
        else
        {
	        UE_LOG(LogBlueprint, Warning, TEXT("Blueprint Node '%s' is missing '%s' pin. Proceeding as if it's default is SelectDefaultAtRuntime"),
	        	*GetNodeTitle(ENodeTitleType::FullTitle).ToString(),
	        	*FK2Node_SpawnAiWithExposedVarsFromClassHelper::TransformScaleMethodPinName.ToString()
	        )
        }
	}
}

void UK2Node_SpawnAiWithExposedVarsFromClass::GetNodeAttributes( TArray<TKeyValuePair<FString, FString>>& OutNodeAttributes ) const
{
	UClass* ClassToSpawn = GetClassToSpawn();
	const FString ClassToSpawnStr = ClassToSpawn ? ClassToSpawn->GetName() : TEXT( "InvalidClass" );
	OutNodeAttributes.Add( TKeyValuePair<FString, FString>( TEXT( "Type" ), TEXT( "SpawnAiWithExposedVarsFromClass" ) ));
	OutNodeAttributes.Add( TKeyValuePair<FString, FString>( TEXT( "Class" ), GetClass()->GetName() ));
	OutNodeAttributes.Add( TKeyValuePair<FString, FString>( TEXT( "Name" ), GetName() ));
	OutNodeAttributes.Add( TKeyValuePair<FString, FString>( TEXT( "ActorClass" ), ClassToSpawnStr ));
}

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

void UK2Node_SpawnAiWithExposedVarsFromClass::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph)
{
    Super::ExpandNode(CompilerContext, SourceGraph);

    // Fixup the Scale Method Pin
    FixupScaleMethodPin();

    // Define the function names used
    static const FName SpawnAIFromClassFuncName = GET_FUNCTION_NAME_CHECKED(UAIBlueprintHelperLibrary, SpawnAIFromClass);
    static const FName WorldContextParamName(TEXT("WorldContextObject"));
    static const FName ActorClassParamName(TEXT("PawnClass"));
    static const FName BehaviorTreeParamName(TEXT("BehaviorTree"));
    static const FName LocationParamName(TEXT("Location"));
    static const FName RotationParamName(TEXT("Rotation"));
    static const FName CollisionHandlingOverrideParamName(TEXT("bNoCollisionFail"));
    static const FName OwnerParamName(TEXT("Owner"));

    // Retrieve pins
    UEdGraphPin* SpawnNodeExec = GetExecPin();
    UEdGraphPin* SpawnNodeTransform = GetSpawnTransformPin();
    UEdGraphPin* SpawnNodeCollisionHandlingOverride = GetCollisionHandlingOverridePin();
    UEdGraphPin* SpawnWorldContextPin = GetWorldContextPin();
    UEdGraphPin* SpawnClassPin = GetClassPin();
    UEdGraphPin* SpawnNodeOwnerPin = GetOwnerPin();
    UEdGraphPin* SpawnNodeThen = GetThenPin();
    UEdGraphPin* SpawnNodeResult = GetResultPin();
    UEdGraphPin* SpawnNodeBehaviorTreePin = FindPin(FK2Node_SpawnAiWithExposedVarsFromClassHelper::BehaviorTreePinName);

    // Ensure all necessary pins are found
    if (!SpawnNodeExec || !SpawnNodeTransform || !SpawnNodeResult || !SpawnClassPin)
    {
        CompilerContext.MessageLog.Error(*LOCTEXT("NullPinError", "Spawn AI node has missing pins").ToString(), this);
        BreakAllNodeLinks();
        return;
    }

    // Create the 'spawn AI' node
    UK2Node_CallFunction* CallSpawnAINode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
    CallSpawnAINode->FunctionReference.SetExternalMember(SpawnAIFromClassFuncName, UAIBlueprintHelperLibrary::StaticClass());
    CallSpawnAINode->AllocateDefaultPins();

    // Link the Exec pin
    CompilerContext.MovePinLinksToIntermediate(*SpawnNodeExec, *CallSpawnAINode->GetExecPin());

    // Link the world context, class, collision handling, and owner pins
    if (SpawnWorldContextPin && CallSpawnAINode->FindPin(WorldContextParamName))
    {
        CompilerContext.MovePinLinksToIntermediate(*SpawnWorldContextPin, *CallSpawnAINode->FindPinChecked(WorldContextParamName));
    }

    CompilerContext.MovePinLinksToIntermediate(*SpawnClassPin, *CallSpawnAINode->FindPinChecked(ActorClassParamName));
    CompilerContext.MovePinLinksToIntermediate(*SpawnNodeCollisionHandlingOverride, *CallSpawnAINode->FindPinChecked(CollisionHandlingOverrideParamName));
    CompilerContext.MovePinLinksToIntermediate(*SpawnNodeOwnerPin, *CallSpawnAINode->FindPinChecked(OwnerParamName));

    // Link the optional behavior tree pin if connected
    if (SpawnNodeBehaviorTreePin && SpawnNodeBehaviorTreePin->LinkedTo.Num() > 0)
    {
        CompilerContext.MovePinLinksToIntermediate(*SpawnNodeBehaviorTreePin, *CallSpawnAINode->FindPinChecked(BehaviorTreeParamName));
    }

    // Handle the transform pin: ensure it contains both location and rotation
    if (SpawnNodeTransform->LinkedTo.Num() > 0)
    {
        UEdGraphPin* CallBeginLocationPin = CallSpawnAINode->FindPinChecked(LocationParamName);
        UEdGraphPin* CallBeginRotationPin = CallSpawnAINode->FindPinChecked(RotationParamName);

        CompilerContext.MovePinLinksToIntermediate(*SpawnNodeTransform, *CallBeginLocationPin);
        CompilerContext.MovePinLinksToIntermediate(*SpawnNodeTransform, *CallBeginRotationPin);
    }
    else
    {
        CompilerContext.MessageLog.Error(*LOCTEXT("SpawnTransformError", "The current value of the 'Spawn Transform' pin is invalid: 'Spawn Transform' must have an input wired into it.").ToString(), this);
        BreakAllNodeLinks();
        return;
    }

    // Link the result pin (spawned AI pawn)
    CompilerContext.MovePinLinksToIntermediate(*SpawnNodeResult, *CallSpawnAINode->GetReturnValuePin());

    // Link the 'then' pin
    CompilerContext.MovePinLinksToIntermediate(*SpawnNodeThen, *CallSpawnAINode->GetThenPin());

    // Break any links to the expanded node
    BreakAllNodeLinks();
}

#undef LOCTEXT_NAMESPACE

1 Like

Wuoah dude, that’s interestening! I wouldn’t say you wasted any time for that. Will look into what you did in detail some day.

But still, what bugs me is why the UE developer didn’t implement exposed variables for “SpawnAI”-Function themself. Like, is there any reasoning behind it?