Abstract
In the GDC 2023, Epic games just launched it’s PCG(Procedural Content Generation) framework in Ureal Engine 5.2, along with a new unreal pcg example Eletric Dream Env. The new PCG framework has an in-house data driven workflow, it’s easy-extended and hight efficient. It also has datatables like Houdini, and it’s debug friendly. Many companies are considering to intergrate it into their own PCG workflow, or, is already ding that.
In this writting we’ll have a glance of the basic schema of Unreal’s PCG framework. Discuss the basic module structure of the PCG plugin and introduce some basic objects in the PCG framework. Our discussion is based on the 5.2 edition of Unreal Engine.
Introduction
Just like Houdini, Gaea, Blender or other DCC(Digital Content Creator), Unreal had provide a graphical programming platform for developers to describe their custom PCG process, just like Blueprint. However, when open a PCG Graph editor, you can’t find any “Compile” button in the editor UI. That’s because the PCG Graph is pure-data object. Unreal also did not originally provide nodes with conditional or loop semantics in PCG Graph (you can implement those functions with custom node with tricks, but doing that may cause problems and is not recommended)(In Unreal Engine 5.3, Branch and select is originally provided). The PCG Graph is a graph that describe the “process” or “dataflow” of a PCG tool, it shouldn’t contains any detailed logic. The detail logic and algorithms is in the blueprint or C++ code behind those node.
Quick Start
Before the discussion of the implementation of PCG framework, let’s have a short practice of how we use the PCG graph in level. We always stat a new chapter for practice part in this serious, but this time let’s just put it here.
To using PCG frame work, first make sure the PCG related plugins are activated:
Notice that the PCG framework is still a experimental function, so using it in real production is not recommended.
Then drag a PCGVolume
into the level, adjust it into the size you desired.
The idea of using volume is very appropriate and natural. We could have differnt PCG process for different and it fits the World Partition open world system.
Then drag the graph you make to the Graph
property of the PCGComponent
Click the generate to se se generation result. Notice that the PCG process only execute when input or PCG Graph change.
You can also use debug function or data inspector to show the PCG data of a node.
And you can set the debug mesh, default is black and gray cubes.
Basic PCG Objects
Before we looking at those objects in the PCG Frame work, let’s have a quick discuss the moduel structure of the PCG plugin, it looks like this:
─PCG
│ ├─Private
│ │ ├─Data
│ │ ├─Elements
│ │ │ └─Metadata
│ │ ├─Graph
│ │ ├─Grid
│ │ ├─Helpers
│ │ ├─InstancePackers
│ │ ├─MatchAndSet
│ │ ├─MeshSelectors
│ │ ├─Metadata
│ │ │ └─Accessors
│ │ ├─Tests
│ │ │ ├─Accessors
│ │ │ ├─Cache
│ │ │ ├─Data
│ │ │ ├─Determinism
│ │ │ └─Elements
│ │ │ └─Metadata
│ │ └─Utils
│ └─Public
│ ├─Data
│ ├─Elements
│ │ └─Metadata
│ ├─Grid
│ ├─Helpers
│ ├─InstancePackers
│ ├─MatchAndSet
│ ├─MeshSelectors
│ ├─Metadata
│ │ └─Accessors
│ ├─Tests
│ │ ├─Accessor
│ │ └─Determinism
│ └─Utils
└─PCGEditor
├─Private
│ ├─AssetTypeActions
│ └─Details
└─Public
└─Details
From the file structure, we could see that the code that implement PCG algorithms, functions and objects, was in the PCG
module, which is a runtime module. The Editor module PCGEditor
only contains editor related codes like detail pannel layouts and asset actions. It’s a typical structure for module organization, and it means that the function code and objects for PCG could be compiled or packaged into our game package, we could even implement runtime PCG with this framework.
IPCGElement
IPCGElement
is the a interface that all executable PCG object(called PCG element) shall implement. It has data and logic of execute, and is the uint-callable object for PCG.
/**
* Base class for the processing bit of a PCG node/settings
*/
class PCG_API IPCGElement
{
public:
virtual ~IPCGElement() = default;
/** Creates a custom context object paired to this element */
virtual FPCGContext* Initialize(const FPCGDataCollection& InputData,
TWeakObjectPtr<UPCGComponent> SourceComponent, const UPCGNode* Node) = 0;
......
protected:
......
/** This function will be called once and once only, at the beginning of an execution */
void PreExecute(FPCGContext* Context) const;
/** The prepare data phase is one where it is more likely to be able to multithread */
virtual bool PrepareDataInternal(FPCGContext* Context) const;
/** Core execution method for the given element. Will be called until it returns true. */
virtual bool ExecuteInternal(FPCGContext* Context) const = 0;
/** This function will be called once and once only, at the end of an execution */
void PostExecute(FPCGContext* Context) const;
virtual bool IsCacheable(const UPCGSettings* InSettings) const { return true; }
};
Here we listed some important functins or interfaces of `IPCGElement
, It’s all about the execution of a PCG unit. It has the Initialize
interface which will create a context for element excution, it has IsCacheable
function which define is the result of element cachable. After that is functions for prepare data, previous and post execute, which will all be called in the Execute
function. A very critical interface is ExecuteInternal
, in this function elements will implement their algoritms for PCG.
The Execute
Function will finally called in the PCGExecutor Execute
Function. We’ll discuss the execution process more detailed later.
UPCGSetting
UPCGSetting
is a kind of UObject
. It contains everying “setting” infomation of a specific node in PCG process. Including parameters, input-output informations(represent the Pins of node in graph), debug data and custom datas. It also has a CreateElement
interface that will create a shared pointer of the binded element for execute, and a CreateNode
interface that define which class of PCG Node(we’ll discuss later, normally just UPCGNode
) is required.
UCLASS(Abstract, BlueprintType, ClassGroup = (Procedural))
class PCG_API UPCGSettings : public UPCGSettingsInterface
{
......
// Returns an array of all the input pin properties. You should not add manually a
// "params" pin, it is handled automatically by FillOverridableParamsPins
virtual TArray<FPCGPinProperties> InputPinProperties() const;
virtual TArray<FPCGPinProperties> OutputPinProperties() const;
virtual FPCGElementPtr CreateElement() const
PURE_VIRTUAL(UPCGSettings::CreateElement, return nullptr;);
......
}
These are some important interfaces of a PCG Setting. The InputPinProperties
and OutputPinProperties
contains input and output pin information for UPCGNode
to create pins(the public interface is AllInputPinProperties
and AllOutputPinProperties
, which will call this two virtual function inside). The CreateElement
creates element for execution.
We could say that PCG Setting bassically represents the “body” of a PCG Node. If you wants to extened a new PCG function with code(another way is using blueprint element which we’ll discuss later), a new PCG Setting and a new PCG Element is basically all you need.
The PCG Setting is kind of UPCGData
which contains UID counter and basic Crc functions.
Setting Instance
The UPCGSetting
was a derivative of UPCGSettingInterface
, which contains a GetSetting
interface. Another class of PCG setting object that derived form
is the UPCGSettingInterface
UPCGSettingInstance
.
Just like it’s name, the UPCGSettingInstance
is a instanced-object of UPCGSetting
. It contains a pointer of a UPCGSetting
as property(which means, hard reference). The idea of both setting and instance derived form UPCGSettingInterface
is that, external could have a unified call of UPCGSetting
through the GetSetting
interface.
UPCGNode
UPCGNode
neither has data nor has algorithm or logic for execution itself. It’s like a “container” for PCG Setting, or we say, it’s a “topologically“ node on the PCG Graph that form the topological structure of the graph and represents the execution flow.
UCLASS(ClassGroup = (Procedural))
class PCG_API UPCGNode : public UObject
{
......
UFUNCTION(BlueprintCallable, Category = Node)
UPCGNode* AddEdgeTo(FName FromPinLabel, UPCGNode* To, FName ToPinLabel);
/** Removes an edge originating from this node */
UFUNCTION(BlueprintCallable, Category = Node)
bool RemoveEdgeTo(FName FromPinLable, UPCGNode* To, FName ToPinLabel);
......
/** Returns the settings interface (settings or instance of settings) on this node */
UPCGSettingsInterface* GetSettingsInterface() const { return SettingsInterface.Get(); }
/** Changes the default settings in the node */
void SetSettingsInterface(UPCGSettingsInterface* InSettingsInterface, bool bUpdatePins = true);
/** Returns the settings this node holds (either directly or through an instance) */
UFUNCTION(BlueprintCallable, Category = Node)
UPCGSettings* GetSettings() const;
#if WITH_EDITORONLY_DATA
UPROPERTY()
int32 PositionX;
UPROPERTY()
int32 PositionY;
UPROPERTY()
FString NodeComment;
UPROPERTY()
uint8 bCommentBubblePinned : 1;
UPROPERTY()
uint8 bCommentBubbleVisible : 1;
#endif // WITH_EDITORONLY_DATA
protected:
EPCGChangeType UpdatePins();
EPCGChangeType UpdatePins(TFunctionRef<UPCGPin* (UPCGNode*)> PinAllocator, const UPCGNode* FromNode = nullptr);
EPCGChangeType UpdateDynamicPins(const UPCGNode* FromNode = nullptr);
......
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = Node)
TArray<TObjectPtr<UPCGPin>> InputPins;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = Node)
TArray<TObjectPtr<UPCGPin>> OutputPins;
};
As we can see, the node has getter and setter for PCG Setting, it will also update pins in the SetSettingInterface
function.
The UPCGNode
also contains Pin properties, which contains infomations about which nodes this node connects to.
Editor infomations like position on the PCG graph, blueprint comment information, was also in this class and wrapped by WITH_EDITORONLY_DATA
macro.
After discussion of element, setting and node, now we can have a preliminary understanding of the relationship between these three object, which shows on the figure below.
Subgraph
The PCG Graph comes with subgraph feature. To acess this you need the UPCGSubgraphSettings
setting, FPCGSubgraphElement
element, and a special class of node UPCGSubgraphNode
.
Notice that the circular reference detection and avoidance of PGC Graph is currently incomplete, so inappropriate subgraph usage may cause stackoverflow, and the engine will crash immediately.
The implementation of subgraph is complacated. It support both subgraph be static(which means defined in graph) and dynamic. So the implementation involved PGC task dependency, graph schedule and element schedule. We’ll discuss those in the PCG Graph execution chapter later.
Blueprint Element
A very intering “element” type is UPCGBlueprintElement
, from the “U” prefix you can see that it’s not a derivative of IPCGElement
. Them why call it an “element” ? That’s a very intering feature of Unreal PCG Framework, extend by blueprint.
To extend new features though blueprint, you can create blueprint and implement the ExecuteWithContext
or Execute
interface and that’s all.
//In UPCGBlueprintElement
......
UFUNCTION(BlueprintNativeEvent, Category = "PCG|Execution")
void ExecuteWithContext(UPARAM(ref)FPCGContext& InContext, const FPCGDataCollection& Input,
FPCGDataCollection& Output);
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = "PCG|Execution")
void Execute(const FPCGDataCollection& Input, FPCGDataCollection& Output);
Infact, some builtin node in PCG framework plugin was implemented with blueprint. For example the set color node.
To use a blueprint element in PCG Graph, you can just using Execute Blueprint
node, and select your own blueprint type, or just search the name if it’s a builtin node.
The real execution of the blueprint is in the real element FPCGExecuteBlueprintElement
which is a derivative of IPCGElement
. The element will call the ExecuteWithContext
event in it’s ExecuteInrernal
function.
bool FPCGExecuteBlueprintElement::ExecuteInternal(FPCGContext* InContext) const
{
......
Context->BlueprintElementInstance->ExecuteWithContext(*Context, Context->InputData, Context->OutputData);
......
}
The ExecuteWithContext
is a BlueprintNativeEvent
, it will call the Execute
event in it’s C++ implementation. This can make it call the Execute
event when a node doesn’t have a blueprint implementation of ExecuteWithContext
.
void UPCGBlueprintElement::ExecuteWithContext_Implementation(FPCGContext& InContext, const FPCGDataCollection& Input, FPCGDataCollection& Output)
{
Execute(Input, Output);
}
This might means that we somehow could implement a UPCGBlueprintElement
with C++ by overridding the ExecuteWithContext_Implementation
virtual function(not tested yet).
One more thing, the blueprint element is a Instanced
property of UPCGBlueprintSettings
so the setting data will be automatically reflected.
References
[1] Unreal Engine. Procedural Content Generation in UE5 | GDC 2023[EB/OL]. (2023.4.18)[2023.7.3]. https://www.youtube.com/watch?v=aoCGLW53fZg.
[2] Epic Games. Electric Dreams Environment[EB/OL]. (2023.6.20)[2023.7.3]. https://www.unrealengine.com/en-US/electric-dreams-environment
[3] Epic Games. Procedural Content Generation Overview[EB/OL]. [2023.7.6]. https://docs.unrealengine.com/5.2/en-US/procedural-content-generation-overview/