FYI: This is good for stuff which is derived from a base struct type and you want to filter it.
So there is going to be a lot of code here, and a few overrides of the engine, plus a custom module which is UncookedOnly for the K2Node and its stuff to live in.
The Basics
We will define a struct which will be our base, and 2 derived structs. We will hide the base struct from being selected as it should contain no properties.
USTRUCT(BlueprintType)
struct FMyBaseInstancedStruct
{
GENERATED_BODY()
};
USTRUCT(BlueprintType)
struct FMyChildAInstancedStruct : public FMyBaseInstancedStruct
{
GENERATED_BODY();
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
FText SomeText;
};
USTRUCT(BlueprintType)
struct FMyChildBInstancedStruct : public FMyBaseInstancedStruct
{
GENERATED_BODY();
public:
UPROPERTY(EditAnywhere, BlueprintReadOnly)
bool SomeBool;
};
With these structs created and in your main module, lets make a DataAsset that will hold our TArray of FInstancedStruct.
UENUM()
enum class EMyFindInstanceStructResult : uint8
{
Valid,
NotValid,
};
UCLASS()
class UMyDataAsset : public UDataAsset
{
GENERATED_BODY()
protected:
// Here we define the Array of FInstancedStructs we want, and we use two meta
// properties to define the type that can be used in this array, and exclude the base
// struct from being used.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Data, meta = (BaseStruct = "/Script/MyGameModule.MyBaseInstancedStruct", ExcludeBaseStruct))
TArray<FInstancedStruct> MyData;
private:
// Function called from our custom K2 Node. Uses a custom thunk.
// The Thunk will use the InstancedStructType to find the instanced struct
// in the array and set the value of Value. This will get converted to the
// the correct custom type (not here but in our K2Node), but the data in Value
/// will be valid if the Instanced Struct was found.
UFUNCTION(BlueprintCallable, CustomThunk, Category = "MyData", meta = (DisplayName = "GetMyData", CustomStructureParam = "Value", ExpandEnumAsExecs = "FindResult", BlueprintInternalUseOnly="true"))
void GetMyDataBP(EMyFindInstanceStructResult& FindResult, UScriptStruct* InstancedStructType, int32& Value);
//Needed to declare our custom thunk
DECLARE_FUNCTION(execGetMyDataBP);
// So our K2Node can access this private function, we don't want the function above
// called by anything BUT the K2Node.
friend class UK2Node_GetMyData;
};
Now we need to implement our CustomThunk in the cpp file
void UMyDataAsset ::GetItemDataBP(EMyFindInstanceStructResult& FindResult, UScriptStruct* InstancedStructType, int32& Value)
{
// We should never hit this! stubs to avoid NoExport on the class.
checkNoEntry();
}
DEFINE_FUNCTION(UMyDataAsset::execGetMyDataBP)
{
//Get the result enum (out ref)
P_GET_ENUM_REF(EMyFindInstanceStructResult, FindResult);
// Get the struct type we want to match
P_GET_OBJECT_REF(UScriptStruct, InstancedStructType);
// Read wildcard Value input.
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FStructProperty>(nullptr);
const FStructProperty* ValueProp = CastField<FStructProperty>(Stack.MostRecentProperty);
void* ValuePtr = Stack.MostRecentPropertyAddress;
P_FINISH;
//Set the result as Not Valid for starters
FindResult = EMyFindInstanceStructResult::NotValid;
P_NATIVE_BEGIN;
for (auto& Item : P_THIS->MyData) //Loop through our array on structs
{
//If its valid and its the correct type
if (Item.IsValid() && Item.GetScriptStruct() == ItemDataType)
{
//Copy the memory (data) to the Value out param
ValueProp->Struct->CopyScriptStruct(ValuePtr, Item.GetMemory());
// Set result as valid
FindResult = EMyFindInstanceStructResult::Valid;
//No more need to loop. break out.
break;
}
}
P_NATIVE_END;
}
The K2Node
You first need to make an UncookedOnly module in your plugin or project. You can refer to https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-engine-modules?application_version=4.27 if you need info on modules.
Create 2 classes in your uncooked modules, one called
MyDataGraphPin and another called: K2Node_GetMyData.
MyDataGraphPin will define some filters for our K2Node pin and the K2Node will contain a very small amount of code to glue it all together.
MyDataGraphPin
#include "Framework/SlateDelegates.h"
#include "Input/Reply.h"
#include "Internationalization/Text.h"
#include "KismetPins/SGraphPinObject.h"
#include "Templates/SharedPointer.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
class SWidget;
class UEdGraphPin;
class UScriptStruct;
/////////////////////////////////////////////////////
// SGraphPinStruct
class SMyDataGraphPin : public SGraphPinObject
{
public:
SLATE_BEGIN_ARGS(SMyDataGraphPin ) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj);
protected:
// Called when a new struct was picked via the asset picker
void OnPickedNewStruct(const UScriptStruct* ChosenStruct);
//~ Begin SGraphPinObject Interface
virtual FReply OnClickUse() override;
virtual bool AllowSelfPinWidget() const override { return false; }
virtual TSharedRef<SWidget> GenerateAssetPicker() override;
virtual FText GetDefaultComboText() const override;
virtual FOnClicked GetOnUseButtonDelegate() override;
//~ End SGraphPinObject Interface
};
CPP File
#include "Containers/UnrealString.h"
#include "Delegates/Delegate.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraph/EdGraphSchema.h"
#include "Editor.h"
#include "InstancedStructDetails.h"
#include "Editor/EditorEngine.h"
#include "Engine/UserDefinedStruct.h"
#include "Internationalization/Internationalization.h"
#include "Layout/Margin.h"
#include "Misc/Attribute.h"
#include "Modules/ModuleManager.h"
#include "SGraphPin.h"
#include "ScopedTransaction.h"
#include "Selection.h"
#include "SlotBase.h"
#include "StructViewerFilter.h"
#include "StructViewerModule.h"
#include "Styling/AppStyle.h"
#include "Types/SlateStructs.h"
#include "UObject/Class.h"
#include "UObject/NameTypes.h"
#include "Widgets/Input/SMenuAnchor.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
class SWidget;
class UObject;
#define LOCTEXT_NAMESPACE "SMyDataGraphPin "
/////////////////////////////////////////////////////
// SMyDataGraphPin
void SMyDataGraphPin::Construct(const FArguments& InArgs, UEdGraphPin* InGraphPinObj)
{
SGraphPin::Construct(SGraphPin::FArguments(), InGraphPinObj);
}
FReply SMyDataGraphPin::OnClickUse()
{
FEditorDelegates::LoadSelectedAssetsIfNeeded.Broadcast();
UObject* SelectedObject = GEditor->GetSelectedObjects()->GetTop(UScriptStruct::StaticClass());
if (SelectedObject)
{
const FScopedTransaction Transaction(NSLOCTEXT("GraphEditor", "ChangeStructPinValue", "Change Struct Pin Value"));
GraphPinObj->Modify();
GraphPinObj->GetSchema()->TrySetDefaultObject(*GraphPinObj, SelectedObject);
}
return FReply::Handled();
}
TSharedRef<SWidget> SMyDataGraphPin::GenerateAssetPicker()
{
FStructViewerModule& StructViewerModule = FModuleManager::LoadModuleChecked<FStructViewerModule>("StructViewer");
// Fill in options
FStructViewerInitializationOptions Options;
Options.Mode = EStructViewerMode::StructPicker;
Options.bShowNoneOption = true;
// Set your instanced struct here!
const UScriptStruct* MetaStruct = FMyBaseInstancedStruct::StaticStruct();
//We use FInstancedStructFilter cause its convienient
TSharedRef<FInstancedStructFilter> StructFilter = MakeShared<FInstancedStructFilter>();
Options.StructFilter = StructFilter;
StructFilter->BaseStruct = MetaStruct;
StructFilter->bAllowBaseStruct = false;
return
SNew(SBox)
.WidthOverride(280)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.MaxHeight(500)
[
SNew(SBorder)
.Padding(4)
.BorderImage( FAppStyle::GetBrush("ToolPanel.GroupBorder") )
[
StructViewerModule.CreateStructViewer(Options, FOnStructPicked::CreateSP(this, &SMyDataGraphPin::OnPickedNewStruct))
]
]
];
}
FOnClicked SMyDataGraphPin::GetOnUseButtonDelegate()
{
return FOnClicked::CreateSP(this, &SMyDataGraphPin::OnClickUse);
}
void SMyDataGraphPin::OnPickedNewStruct(const UScriptStruct* ChosenStruct)
{
if(GraphPinObj->IsPendingKill())
{
return;
}
FString NewPath;
if (ChosenStruct)
{
NewPath = ChosenStruct->GetPathName();
}
if (GraphPinObj->GetDefaultAsString() != NewPath)
{
const FScopedTransaction Transaction( NSLOCTEXT("GraphEditor", "ChangeStructPinValue", "Change Struct Pin Value" ) );
GraphPinObj->Modify();
AssetPickerAnchor->SetIsOpen(false);
GraphPinObj->GetSchema()->TrySetDefaultObject(*GraphPinObj, const_cast<UScriptStruct*>(ChosenStruct));
}
}
FText SMyDataGraphPin::GetDefaultComboText() const
{
return LOCTEXT("DefaultComboText", "Select Struct");
}
#undef LOCTEXT_NAMESPACE
I won’t go over every detail in the above, but inside the function GenerateAssetPicker you will see the struct defined here.
K2Node_GetMyData
struct FMyInstancedStructPinFactory : public FGraphPanelPinFactory
{
public:
virtual TSharedPtr<class SGraphPin> CreatePin(class UEdGraphPin* Pin) const override;
};
/**
*
*/
UCLASS()
class UK2Node_GetMyData : public UK2Node_CallFunction
{
GENERATED_BODY()
public:
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override;
virtual void PostReconstructNode() override;
virtual void ClearCachedBlueprintData(UBlueprint* Blueprint) override;
void RefreshOutputStructType();
};
The CPP file
TSharedPtr<class SGraphPin> FMyInstancedStructPinFactory::CreatePin(class UEdGraphPin* Pin) const
{
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Object)
{
//Only if the node is our special custom K2Node.
if (UK2Node_GetMyData* Node = Cast<UK2Node_GetMyData>(Pin->GetOwningNode()))
{
//Only use this custom graph slate if its the pin we want to use it on
//this must be the same name as your UScriptStruct* pointer name in your
//custom thunk!
if (Pin->PinName == "InstancedStructType")
{
//Return our custom GraphPin we made earlier.
return SNew(SMyDataGraphPin, Pin);
}
}
}
//Let other filters try we dont want it.
return nullptr;
}
void UK2Node_GetMyData::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
{
Super::GetMenuActions(ActionRegistrar);
UClass* Action = GetClass();
if (ActionRegistrar.IsOpenForRegistration(Action))
{
auto CustomizeLambda = [](UEdGraphNode* NewNode, bool bIsTemplateNode, const FName FunctionName)
{
UK2Node_GetMyData* Node = CastChecked<UK2Node_GetMyData>(NewNode);
UFunction* Function = UMyData::StaticClass()->FindFunctionByName(FunctionName);
check(Function);
Node->SetFromFunction(Function);
};
// Our custom thunk
UBlueprintNodeSpawner* GetNodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
check(GetNodeSpawner != nullptr);
GetNodeSpawner->CustomizeNodeDelegate = UBlueprintNodeSpawner::FCustomizeNodeDelegate::CreateStatic(CustomizeLambda, GET_FUNCTION_NAME_CHECKED(UMyData, GetMyDataBP));
ActionRegistrar.AddBlueprintAction(Action, GetNodeSpawner);
}
}
void UK2Node_GetMyData::PinDefaultValueChanged(UEdGraphPin* ChangedPin)
{
Super::PinDefaultValueChanged(ChangedPin);
//Refresh our wildcard pin if the default value is changed
if (ChangedPin->PinName == "InstancedStructType")
{
if (ChangedPin->LinkedTo.Num() == 0)
{
RefreshOutputStructType();
}
}
}
void UK2Node_GetMyData::PostReconstructNode()
{
Super::PostReconstructNode();
//Refresh our wildcard pin if the node has been rebuilt
RefreshOutputStructType();
}
void UK2Node_GetMyData::ClearCachedBlueprintData(UBlueprint* Blueprint)
{
Super::ClearCachedBlueprintData(Blueprint);
//Refresh our wildcard pin if the cached data has been cleared
RefreshOutputStructType();
}
void UK2Node_GetMyData::RefreshOutputStructType()
{
auto GetPinForMe = [this] (FName PinName) {
UEdGraphPin* Pin = FindPinChecked(PinName);
return Pin;
};
//Grab the value pin (the return pin with our data
UEdGraphPin* ValuePin = GetPinForMe("Value");
//Our type struct pin
UEdGraphPin* StructTypePin = GetPinForMe("InstancedStructType");
if (StructTypePin->DefaultObject != ValuePin->PinType.PinSubCategoryObject)
{
if (ValuePin->SubPins.Num() > 0)
{ //If the pin has been broken (split), recombine it.
GetSchema()->RecombinePin(ValuePin);
}
//Set the value of our value pin to your selected InstancedStructType
ValuePin->PinType.PinSubCategoryObject = StructTypePin->DefaultObject;
ValuePin->PinType.PinCategory = (StructTypePin->DefaultObject == nullptr) ? UEdGraphSchema_K2::PC_Wildcard : UEdGraphSchema_K2::PC_Struct;
}
}
One last step is to register our custom pin factory, in your uncooked only modules StartupModule and ShutdownModule
void FMyUncookedOnlyModule::StartupModule()
{
MyInstancedStructPinFactory = MakeShared<FMyInstancedStructPinFactory>();
FEdGraphUtilities::RegisterVisualPinFactory(MyInstancedStructPinFactory );
}
void FMyUncookedOnlyModule::ShutdownModule()
{
FEdGraphUtilities::UnregisterVisualPinFactory(MyInstancedStructPinFactory);
}
//Place the following in the header:
TSharedPtr<FMyInstancedStructPinFactory> MyInstancedStructPinFactory;
Well that was a lot but, that should be everything to make it work.
I will provide some screenshots in a bit of it in action 🙂