Creating a new graph pin visualizer for Blueprint

2021/01 02 13:01

Within the Blueprint system, we can use instances of our MyCustomAsset class as variables, provided we mark that class as a BlueprintType in its UCLASS macro. However, by default, our new asset is simply treated as a UObject, and we can’t access any of its members:

For some types of assets, we might wish to enable in-line editing of literal values in the same way that classes such as FVector support the following:

To enable this, we need to use a Graph Pin visualizer. This recipe will show you how to enable in-line editing of an arbitrary type using a custom widget defined by you.

How to do it…

  1. First, we will update the MyCustomAsset class to be editable in Blueprints and reflect what we’ll be doing in this recipe. Go to MyCustomAsset.h and update it to the following code:
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "MyCustomAsset.generated.h"


UCLASS(BlueprintType, EditInlineNew)
class CHAPTER_10_API UMyCustomAsset : public UObject
{
GENERATED_BODY()

public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Custom Asset")
FString ColorName;

};
  1. From the Chapter_10Editor folder, create a new file called MyCustomAssetPinFactory.h .
  2. Inside the header, add the following code:
#pragma once
#include "EdGraphUtilities.h"
#include "MyCustomAsset.h"
#include "SGraphPinCustomAsset.h"


struct CHAPTER_10EDITOR_API FMyCustomAssetPinFactory : public FGraphPanelPinFactory
{
public:
virtual TSharedPtr<class SGraphPin> CreatePin(class UEdGraphPin* Pin) const override
{
if (Pin->PinType.PinSubCategoryObject == UMyCustomAsset::StaticClass())
{
return SNew(SGraphPinCustomAsset, Pin);
}
else
{
return nullptr;
}
};
};


  1. Create another header file called SGraphPinCustomAsset.h:
#pragma once
#include "SGraphPin.h"


class CHAPTER_10EDITOR_API SGraphPinCustomAsset : public SGraphPin
{
SLATE_BEGIN_ARGS(SGraphPinCustomAsset) {}
SLATE_END_ARGS()

void Construct(const FArguments& InArgs, UEdGraphPin* InPin);
protected:
virtual FSlateColor GetPinColor() const override { return FSlateColor(FColor::Black); };

virtual TSharedRef<SWidget> GetDefaultValueWidget() override;

void ColorPicked(FLinearColor SelectedColor);
};

  1. Implement SGraphPinCustomAsset by creating the .cpp file:
#include "SGraphPinCustomAsset.h"
#include "Chapter_10Editor.h"
#include "SColorPicker.h"
#include "MyCustomAsset.h"

void SGraphPinCustomAsset::Construct(const FArguments& InArgs, UEdGraphPin* InPin)
{
SGraphPin::Construct(SGraphPin::FArguments(), InPin);
}

TSharedRef<SWidget> SGraphPinCustomAsset::GetDefaultValueWidget()
{
return SNew(SColorPicker)
.OnColorCommitted(this, &SGraphPinCustomAsset::ColorPicked);

}

void SGraphPinCustomAsset::ColorPicked(FLinearColor SelectedColor)
{
UMyCustomAsset* NewValue = NewObject<UMyCustomAsset>();
NewValue->ColorName = SelectedColor.ToFColor(false).ToHex();
GraphPinObj->GetSchema()->TrySetDefaultObject(*GraphPinObj, NewValue);
}
  1. Regenerate your Visual Studio project.
  2. Add #include “MyCustomAssetPinFactory.h” to the Chapter_10Editor.h module implementation file.
  3. Add the following member to the editor module class (FChapter_10EditorModule):
TSharedPtr<FMyCustomAssetPinFactory> PinFactory; 
  1. Open Chapter_10Editor.cpp and then add the following to StartupModule():
PinFactory = MakeShareable(new FMyCustomAssetPinFactory()); 
FEdGraphUtilities::RegisterVisualPinFactory(PinFactory); 
  1. Also add the following code to ShutdownModule():
FEdGraphUtilities::UnregisterVisualPinFactory(PinFactory); PinFactory.Reset(); 
  1. Compile your code and launch the editor.
  2. Create a new Function inside of the Level Blueprint by clicking on the plus symbol beside Functions within the My Blueprint panel:
  1. Add an input parameter:
  1. Set its type to MyCustomAsset (Object Reference):
  1. In the Level Blueprint’s Event Graph, place an instance of your new function and verify that the input pin now has a custom visualizer in the form of a color picker:

Newly added color picker visualizer

How it works…

Customizing how objects appear as literal values on Blueprint pins is done using the FGraphPanelPinFactory class.

This class defines a single virtual function:

virtual TSharedPtr<class SGraphPin> CreatePin(class 
UEdGraphPin* Pin) const

The function of CreatePin, as the name implies, is to create a new visual representation of the graph pin.

It receives a UEdGraphPin instance. UEdGraphPin contains information about the object that the pin represents so that our factory class can make an informed decision regarding which visual representation we should be displaying.

Within our implementation of the function, we check that the pin’s type is our custom class.

We do this by looking at the PinSubCategoryObject property, which contains a UClass, and comparing it to the UClass associated with our custom asset class.

If the pin’s type meets our conditions, we return a new shared pointer to a Slate Widget, which is the visual representation of our object.

If the pin is of the wrong type, we return a null pointer to indicate a failed state.

The next class, SGraphPinCustomAsset, is the Slate Widget class, which is a visual representation of our object as a literal.

It inherits from SGraphPin, the base class for all graph pins.

The SGraphPinCustomAsset class has a Construct function, which is called when the widget is created.

It also implements some functions from the parent class: GetPinColor() and GetDefaultValueWidget().

The last function that is defined is ColorPicked, a handler for when a user selects a color in our custom pin.

In the implementation of our custom class, we initialize our custom pin by calling the default implementation of Construct.

The role of GetDefaultValueWidget is to actually create the widget that is the custom representation of our class, and return it to the engine code.

In our implementation, it creates a new SColorPicker instance – we want the user to be able to select a color and store the hex-based representation of that color inside the FString property in our custom class.

This SColorPicker instance has a property called OnColorCommitted – this is a slate event that can be assigned to a function on an object instance.

Before returning our new SColorPicker, we link OnColorCommitted to the ColorPicked function on this current object so that it will be called if the user selects a new color.

The ColorPicked function receives the selected color as an input parameter.

Because this widget is used when there’s no object connected to the pin we are associated with, we can’t simply set the property on the associated object to the desired color string.

We need to create a new instance of our custom asset class, and we do that by using the NewObject template function.

This function behaves similarly to the SpawnActor function we discussed in other chapters, and initializes a new instance of the specified class before returning a pointer to it.

With a new instance in hand, we can set its ColorName property. FLinearColors can be converted into FColor objects, which define a ToHex() function that returns an FString with the hexadecimal representation of the color that was selected on the new widget.

Finally, we need to actually place our new object instance into the graph so that it will be referenced when the graph is executed.

To achieve this, we need to access the graph pin object that we represent, and use the GetSchema function. This function returns the Schema for the graph that owns the node that contains our pin.

The Schema contains the actual values that correspond to graph pins, and is a key element during graph evaluation.

Now that we have access to the Schema, we can set the default value for the pin that our widget represents. This value will be used during graph evaluation if the pin isn’t connected to another pin, and acts like a default value that’s provided during a function definition in C++.

As with all the extensions we’ve made in this chapter, there has to be some sort of initialization or registration to tell the engine to defer to our custom implementation before using its default inbuilt representation.

To do this, we need to add a new member to our editor module to store our PinFactory class instance.

During StartupModule, we create a new shared pointer that references an instance of our PinFactory class.

We store it inside the editor module’s member so that it can be unregistered later. Then, we call FEdGraphUtilities::RegisterVisualPinFactory(PinFactory) to tell the engine to use our PinFactory to create the visual representation.

During ShutdownModule, we unregister the pin factory using UnregisterVisualPinFactory.

Finally, we delete our old PinFactory instance by calling Reset() on the shared pointer that contains it.