Introduction To PCG
If you’re within the Unreal Engine 5 ecosystem, you’ve no doubt heard of the Procedural Content Generation framework, otherwise known as PCG.
This is a particularly powerful, albeit complex, framework for creating procedural tools within a graph-based context.

My Experience
In the past I’ve developed several procedural frameworks entirely within Unreal Engine 4 / 5, heavily inspired by the work I did with Toni Seifert on Sackboy: a Big Adventure, see his incredible breakdown below:
On the project I was responsible for maintaining and creating tools using Houdini, while Toni was creating his own bespoke framework utilising Blueprints – making it much easier (and more cost effective) to bridge the gap between tools and content creation.
When Epic announced PCG, I was incredibly excited to get my hands on the framework, but was immediately disappointed to find a complex framework that seemed to push itself more as a framework just for processing.. points?
I saw the potential in PCG as something that could help bridge that gap between Technical Artists making reusable tools, and Artists & Designers using those tools.. using graphs!
So I wanted to push this further than just processing points, as one of Houdini’s biggest strengths is in its interop between data types – I experimented with creating custom nodes for expanding its potential, such as spline data and dynamic mesh (which Epic then added some native support for in 5.5).
Creating Custom Nodes
To summarize, PCG is a framework with a lot of potential, however, I am impatient and don’t like to wait for these features and improvements to trickle down, so I create my own nodes to expand PCG in both Blueprint and C++.
I will explain the basics of how to create and setup your own PCG nodes so you can expand its functionality.
Although please be aware the API for PCG is constantly changing and is quite complex, so some things in this tutorial may not apply pre-5.5, and may even change past 5.5.
Using Blueprints
The easiest solution is to create nodes using Blueprints, which thankfully is pretty straight forward thanks to the Execute Blueprint node.
This PCG node essentially wraps around your Blueprint subclass, allowing you to easily extend PCG in Blueprints. You can either search for your nodes in the context menu, or use the Execute Blueprint node directly and give it your class.

I will forewarn that not everything is exposed to Blueprints, so you may need to write some C++ helpers, depending on what you’re trying to achieve.
Class Setup
To setup your custom Blueprint node, you need to subclass PCGBlueprintElement.

Node Properties
You can override quite a few methods within the class, which is also how you supply a node label and set the colour.

Pins
It’s quite easy to setup your pins from the class defaults tab – I recommend setting the input and output pins you want, and make sure you name them.

Behaviour
Writing your custom node behaviour involves overriding ExecuteWithContext.

It can look a bit intimidating at first, so here’s a quick breakdown:
In Context– the context the node is being executed in, you probably won’t need to deal with this at firstInput– node input dataOutput– the output data for the node
Input Data
We can sample the input data quite easily using a few of the helpers provided and supplying a class type which will automatically cast Return Value to the data type for you to loop over. This is because PCG always assumes you can have multiple data, and it’s up to you to handle it.

Output Data
Handling outputs is a bit more complicated, but easy once you get the hang of it. Most of the confusion I think revolves around the naming of the API.
I recommend creating a local variable of PCGData Collection type and outputting that.
Below is an example that will create empty point data, add it to the data collection (output pin essentially), and return it.
Remember to specify In Pin Label!

It should be noted that PCG discourages you from mutating inputs, and instead prefers you to do copies.
Settings
Defining settings is as easy as making a public Instance Editable variable in your Blueprint. This will appear in the details for the node in your PCG graph.

Example
Here is a quick example which will transform each point by making a copy of the input, mutating the points, and copying it back onto a new PCGPointData.

Please note this definitely will not be efficient for handling many points and is purely for demonstration purposes.
Using C++
Creating PCG nodes in C++ will generally be the more flexible (and in many cases, performant) solution as you won’t be limited by the Blueprint API or the performance of the VM (which can get out of hand once you start looping on large amounts of data).

Build.cs Config
To start with, ensure your module contains PCG in either its public or private dependency module names. Here’s the Build.cs of our PCG plugin for an example.

Class Setup
You actually need to subclass two classes here:
UPCGSettings– what defines your node, and stores any settingsIPCGElement– an interface which defines the behaviour of your node
I’ve provided a basic setup to get you going here, but I implore you to explore the source code yourself where needed.
.h
#pragma once
#include "PCGSettings.h"
UCLASS()
class UPCGCustomElement : public UPCGSettings
{
GENERATED_BODY()
public:
// Begin UPCGSettings interface
public:
#if WITH_EDITOR
virtual FName GetDefaultNodeName() const override { return TEXT("CustomPCGNode"); }
virtual FText GetDefaultNodeTitle() const override;
virtual FText GetNodeTooltipText() const override;
virtual EPCGSettingsType GetType() const override { return EPCGSettingsType::Spatial; }
#endif
virtual bool HasDynamicPins() const override { return true; }
virtual EPCGDataType GetCurrentPinTypes(const UPCGPin* InPin) const override;
protected:
virtual TArray<FPCGPinProperties> InputPinProperties() const override;
virtual TArray<FPCGPinProperties> OutputPinProperties() const override;
virtual FPCGElementPtr CreateElement() const override;
// End UPCGSettings interface
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Settings", meta = (PCG_Overridable))
bool bExampleSetting = false;
};
class FPCGCustomElement: public IPCGElement
{
protected:
// Begin IPCGElement interface
virtual bool ExecuteInternal(FPCGContext* Context) const override;
// End IPCGElement interface
};
.cpp
#include "PCGContext.h"
#define LOCTEXT_NAMESPACE "PCGCustomElement"
// Begin UPCGSettings interface
#if WITH_EDITOR
FText UPCGCustomElement::GetDefaultNodeTitle() const
{
return LOCTEXT("NodeTitle", "Custom PCG Node");
}
FText UPCGCustomElement::GetNodeTooltipText() const
{
return LOCTEXT("NodeTooltip", "Enter your tooltip here.");
}
#endif
EPCGDataType UPCGCustomElement::GetCurrentPinTypes(const UPCGPin* InPin) const
{
// Define the data type for each pin
// I am not 100% sure why this exists when InputPinProperties/OutputPinProperties outputs the type
const FName PinName = InPin->GetFName();
// In/Out = Spline Data
if (PinName == PCGPinConstants::DefaultInputLabel)
{
return EPCGDataType::Spatial;
}
return Super::GetCurrentPinTypes(InPin);
}
TArray<FPCGPinProperties> UPCGCustomElement::InputPinProperties() const
{
// Init pins - increase the number to add more pins
TArray<FPCGPinProperties> PinProperties;
PinProperties.SetNum(1);
PinProperties[0].Label = PCGPinConstants::DefaultInputLabel; // Can give it your own FName here, but PCG provides some constants for you
PinProperties[0].Usage = EPCGPinUsage::Normal;
PinProperties[0].AllowedTypes = EPCGDataType::Spatial; // Spatial includes points etc. but you can change this to whatever you like within the enum
PinProperties[1].bAllowMultipleData = true; // Force single input or allow multiple inputs?
return PinProperties;
}
TArray<FPCGPinProperties> UPCGCustomElement::OutputPinProperties() const
{
// See InputPinProperties as it's essentially the same, but for output pins
TArray<FPCGPinProperties> PinProperties;
PinProperties.SetNum(1);
PinProperties[0].Label = PCGPinConstants::DefaultOutputLabel;
PinProperties[0].Usage = EPCGPinUsage::Normal;
PinProperties[0].AllowedTypes = EPCGDataType::Spatial;
PinProperties[0].bAllowMultipleData = true;
return PinProperties;
}
FPCGElementPtr UPCGCustomElement::CreateElement() const
{
// Return your class which is the actual node behaviour
// Use MakeShared<T> for this
return MakeShared<FPCGCustomElement>();
}
// End UPCGSettings interface
// Begin IPCGElement interface
bool FPCGCustomElement::ExecuteInternal(FPCGContext* Context) const
{
// Example implementation
// This is the behaviour of your node
// You can grab data from your pins and your settings directly from the Context arg
// Add some basic profiler support
TRACE_CPUPROFILER_EVENT_SCOPE(FPCGCustomElement::Execute);
// Double check settings
check(Context != nullptr);
// Resolve settings
const UPCGCustomElement* const Settings = Context->GetInputSettings<UPCGCustomElement>();
check(Settings != nullptr);
// Get input pin with the default label
TArray<FPCGTaggedData> Sources = Context->InputData.GetInputsByPin(PCGPinConstants::DefaultInputLabel);
// No data, so early out
if (Sources.Num() == 0)
{
return true;
}
// Example - will just spit out the data coming in
// You would add your own outputs here
Context->OutputData.TaggedData.Append(Sources);
return true;
}
// End IPCGElement interface
#undef LOCTEXT_NAMESPACE
Pins
To define your pins you must override UPCGSettings::InputPinProperties and UPCGSettings::OutputPinProperties – both must return TArray<FPCGPinProperties>.
You also need to override UPCGSettings::GetCurrentPinTypes (although I don’t fully understand why this is repeated).
Example
EPCGDataType UPCGCustomElement::GetCurrentPinTypes(const UPCGPin* InPin) const
{
const FName PinName = InPin->GetFName();
// In/Out = Spline Data
if (PinName == PCGPinConstants::DefaultInputLabel || PinName == PCGPinConstants::DefaultOutputLabel)
{
return EPCGDataType::PolyLine;
}
// Projection Target = Spatial
if (PinName == ProjectionTargetPinName)
{
return EPCGDataType::Concrete;
}
return Super::GetCurrentPinTypes(InPin);
}
TArray<FPCGPinProperties> UPCGCustomElement::InputPinProperties() const
{
TArray<FPCGPinProperties> PinProperties;
PinProperties.SetNum(2);
// In
PinProperties[0].Label = PCGPinConstants::DefaultInputLabel;
PinProperties[0].Usage = EPCGPinUsage::Normal;
PinProperties[0].AllowedTypes = EPCGDataType::PolyLine;
PinProperties[1].bAllowMultipleData = true;
// Projection Target
PinProperties[1].Label = ProjectionTargetPinName;
PinProperties[1].Usage = EPCGPinUsage::Normal;
PinProperties[1].AllowedTypes = EPCGDataType::Concrete;
PinProperties[1].bAllowMultipleData = false;
return PinProperties;
}
TArray<FPCGPinProperties> UPCGCustomElement::OutputPinProperties() const
{
TArray<FPCGPinProperties> PinProperties;
PinProperties.SetNum(1);
PinProperties[0].Label = PCGPinConstants::DefaultOutputLabel;
PinProperties[0].Usage = EPCGPinUsage::Normal;
PinProperties[0].AllowedTypes = EPCGDataType::PolyLine;
PinProperties[0].bAllowMultipleData = true;
return PinProperties;
}
Input / Output Data
Data is passed around using an intermediate, polymorphic UObject class, for example splines are passed around using UPCGSplineData.
To sample input data simply query the Context arg inside ExecuteInternal, i.e. TArray<FPCGTaggedData> Sources = Context->InputData.GetInputsByPin(TEXT("MyPinName"));
You can then cast this data to specific types from FPCGTaggedData::Data, i.e. const UPCGSpatialData* const MyData = Cast<UPCGSpatialData>(Sources[0].Data);
(make sure you remember your pointer guard!)
To output data, you just add it to FPCGContext::OutputData (you can specify the Pin name in the element), i.e. Context->OutputData.TaggedData.Emplace(FPCGTaggedData());
There is a hierarchy of data types you can deal with, most of which inherit from UPCGSpatialData – you can look into these yourself.
Summary
To summarize, it’s possible to extend PCG using both Blueprints and C++ by creating custom nodes, and even custom data types (though Epic’s got it mostly covered).
If you are able to write C++, then I recommend writing C++ nodes where performance or maintainability matters more.
Another thing to note is Epic’s intention (at least long term) is to be able to do as much as possible, directly inside PCG, so it may be difficult to beat the performance of many nodes as a result. For example they introduced HLSL nodes (which I presume run compute shaders), and more support for multithreading. So be aware of this as it may be faster to do certain things natively in PCG – I recommend custom nodes more as a solution for expanding working with data types, with a specific example coming to mind of using Geometry Script functions directly inside PCG.


Leave a Reply