Unreal Engine - Customized AssetType and Editor

Unreal Engine - Customized AssetType and Editor

Created
Oct 25, 2023 11:13 AM
Tags

Intro

In this article, you will learn how to create a graph editor for your customized asset type in Unreal Engine. The code provided is based on version 4.26.2. Please note that some of the parameter names and functions are specific to my own requirements. Be sure to replace them with appropriate names when following this tutorial.
 

Customized Asset Type

In the context of custom asset development, we encounter three key considerations: the process of asset creation, the behavior when double-clicking the asset in the editor, and the comprehensive definition of the asset.
 

Defining the Asset

Let's declare our custom asset class. All asset types in Unreal Engine, which are visible in the Content Browser, inherit from UObject.
notion image
We'll set aside the implementation of PostInitProperties for future use. At this point, our focus is on initializing the EdGraph pointer within the constructor.
 

Creating the Editor Window

Next, we need to implement the functionality to generate an editor panel for this asset class. This class should inherit from FAssetTypeActions_Base.
notion image
In the constructor, you'll pass a selected category as a parameter—EAssetTypeCategories . This category corresponds to the labels seen in the right-click menu for various asset types, like “FX” and “Sound”.
notion image
The implementations of these functions are relatively straightforward. However, the most critical function is OpenAssetEditor. Its purpose is to open an editor window when an asset is opened. For now, we will inherit the base class's method and return to complete this function later.
notion image
notion image
To ensure that our double-click behavior is handled correctly, we must register this action with Unreal Engine's events. In your Module class CPP file, add the following code to the StartupModule function. This code registers the necessary definitions and types. When the Module is loaded, Unreal Engine will register the action for double-clicking our asset. This coding pattern will be repeatedly used in subsequent steps.
notion image

Mapping the creation action

Unreal Engine assembles data through different types of “factories”. For instance, a class responsible for constructing vertex data for the render thread is named VertexFactory. In our case, the class used to assemble the data required for our assets should inherit from UFactory. It's important to note that, despite the 'factory' in its name, VertexFactory inherits from FRenderResource, which categorizes it as a type of rendering resource. The crucial function here is FactoryCreateNew. As the name suggests, this function determines the behavior when creating our custom asset.
notion image
In the constructor, it's crucial to set bCreateNew to true. This enables the creation of our custom asset through the context menu in the Content Browser. Unreal Engine provides several functions and member variables that allow for defining more intricate behaviors. You can find comprehensive details about these in the UFactory class.
In addition to this, we must specify our asset class as one supported by this factory. This ensures that our factory is capable of generating instances of our custom asset.
notion image
Within the FactoryCreateNew function, we are responsible for creating a new instance of our custom UObject. To support Undo/Redo functionality for our asset operations, it's essential to explicitly include the RF_Transactional flag.
notion image
Once these steps are completed, our asset will become visible in the Content Browser's context menu. Double-clicking on this asset opens a default dialog with a dock tab that displays the details of our LayerEditorData.
notion image
notion image

Node Graph Editor

Next, we'll enhance our asset to enable the following capabilities:
  • Serialization and deserialization of node editor data, allowing users to save and edit it repeatedly.
  • Opening a node graph editor window when double-clicking the asset.
  • Implementing custom behaviors for each node, including the ability to create new nodes.
 

ToolKit

We've already set our EdGraph data member in the asset class as a UPROPERTY, allowing Unreal Engine to handle serialization for us. To enable graph editing, we'll create a Toolkit class that derives from FAssetEditorToolkit. The purpose of FAssetEditorToolkit is explained below.
notion image
You can see the Toolkit defines the asset editing behaviours. You can treat it as the asset property asset ifself, as some modules have done this. There are four functions we MUST implement. Otherwise it won’t compile.
notion image
notion image
Let’s copy those down and I will elaborate on them one by one.
notion image
Next, we need to customize this tab to show our editor. We will now declare and define the SpawnDetailTab() function. This function is responsible for creating a detail panel on the right side of our graph editor tab.
notion image
notion image
Next, we will implement two sets of the most complex functions. The first group is Register/Unregister TabSpawners, as shown below. These two functions determine the layout of the customized tab for your asset.
notion image
Let’s define RegisterTabSpawners first. You can follow this code explanation step by step, with a special focus on the InEvents parameter. As mentioned earlier, Unreal Engine often relies on event registration to enable custom functionalities. In this case, we're registering our functions to define the behavior when interacting with the editor. This approach helps keep the base class code clean and facilitates easy extension of interfaces.
notion image
The IDE may complain about missing ViewModel, just write the code for now and we will address this later. Similar to the event registration system mentioned in the comment line above AdditionalCommands, these commands allow us to incorporate additional customized operations, like node copying and deletion. We will revisit this when we write your ViewModel later. We now need to register two tabs, one is the detail panel to show those UPROPERTY defined in our asset object, and the other is the node graph editor.
notion image
Please pay attention to this Group since it is mandatory for us to fill a group title. We can see this in the Window section shown below. Skipping this will not cause compilation errors or crashes, but it will generate unwanted results. So I do recommend you treat it as a required field. And that’s all for RegisterTabSpawners.
notion image
The reason.
If you skip the group name, Unreal Engine will assign a default group during the compilation. The funny part is Unreal Engine will still treat tabs with the default group name as uncategorized tabs. Therefore, when displaying tabs in the dialog in the Window menu shown below, it will display those tabs twice. First, they are listed with the tabs under the default group name, and then again with the uncategorized tabs. Although this oversight has not caused any significant issues, I still believe that leaving the group name field blank is not the best practice.
 
 
For UnregisterTabSpanwers, we just unregister all tabs we registered in RegisterTabSpawners.
notion image
So, how do we inform Unreal Engine about our custom window layout? This is where the second set of complex functions comes into play.
Recalling that we called the base-class function when writing OpenAssetEditor above. Now we modify the code to use our toolkit to initialize the correct asset editor.
notion image
We now need to implement InitializeAssetEditor. Again you can copy the code first.
notion image
Next, you'll notice the use of the →Split operator, which resembles Slate syntax. It acts as a separator, and the direction of separation is determined by the Orientation specified. In my case, I've chosen Orient_Vertical, resulting in a vertical split into two tabs.
notion image
Now, let's arrange the tabs within this layout. We'll start by placing the Toolbar for future expansion. Then, we'll position the GraphEditor and Detail tabs horizontally on the left and right sides of the lower section. Take note of the SizeCoefficient, which determines the default size percentage each tab occupies when opening windows. In my example, I've set it to 0.25 for the PropertyEditorTab (the detail panel), meaning it will initially occupy 25% of the lower section. The diagram below illustrates this layout.
notion image
notion image

ViewModel

The concept of ViewModel originates from the Model-View-ViewModel (MVVM) design pattern. The ViewModel defines the behaviour when operating on nodes.
notion image
Now let's implement this FLayerWeightEdGraphViewModel. It seems this ViewModel includes some implementations of commands related to graph editing.
notion image
notion image
You can see that the ViewModel contains several implementations of commands related to graph editing. Let's start implementing them one by one. First, let's take a look at the constructor and destructor functions.
notion image
notion image
The constructor and deconstructor are responsible for registering and unregistering our ViewModel in the editor. The set and get Graph is for setting and getting the node graph that this ViewModel operates on.
notion image
We also need to define some behaviors for after undo and redo. After an undo, we need to notify the graph to refresh after undo/redo.
notion image
notion image
We will setup the commands we need in the Initialize function.
notion image
We will bind our customized commands in SetupCommands. For instance, after pressing the delete key, we are responsible for telling the engine to delete selected nodes in our editor graph. The function we use to bind actions is shown below. There are more actions you can customize, we will only register actions in the image below in this tutorial.
notion image
Let’s go through each action function. For SelectAll, we can just use the default one from its parent class.
notion image
We also need a helper function to get the currently selected nodes. Many operations, like copying, rely on operating on selected nodes via keyboard shortcuts. Therefore, we need a convenient way to retrieve the currently selected nodes.
notion image
In DeleteSelectedNodes, the key aspect to note is FScopedTransaction. This function marks the transactions after this line in the same scope as this function call will be recorded in the undo/redo system of Unreal Engine. There is another version which allows you to specify the section that you would want undo/redo stack to cache. Finally, do not forget to refresh the UI manually since we have modified the graph.
notion image
In some cases, certain nodes should not be deletable by users. For instance, in the Niagara system editor, the system node must not be deletable. To ensure this, we perform a check in the CanDeleteNodes function. To control the ability to delete nodes of our own node types, we override the CanUserDeleteNode function during the definition of these nodes.
notion image
When it comes to copying, we can follow Unreal Engine's approach, which involves exporting the objects to strings and then placing them in the clipboard. Unreal Engine uses this method because the clipboard can only handle text strings. Subsequently, Unreal Engine reconstructs the objects based on these stored strings. This process aligns with Unreal Engine's standard procedure for copying objects. To ensure nodes are eligible for duplication, we rely on Unreal Engine's UEdGraphNode function interface CanDuplicateNode.
notion image
For handling the paste operation, I've organized the core logic in a dedicated function, much like the approach used in AIGraph within Unreal Engine. Alternatively, you can opt to integrate it directly within the PasteNodes function, following the convention demonstrated in NiagaraGraph.
notion image
In the PasteNodeHere function, we initially clear the node selection to prepare for highlighting the newly pasted nodes. We then utilize Unreal Engine's built-in function, ClipboardPaste, to retrieve the object strings that were previously copied. Afterward, we make use of Unreal Engine's ImportNodesFromText function to convert these strings back into node objects.
notion image
In Unreal Engine, when pasting a group of nodes, it selects the location where your mouse cursor is placed as the center of the selection box. It then arranges the pasted nodes around this central point, maintaining their relative positions. Do not forget to notify the graph to update at the end.
notion image
Additionally, you will need to dirty the outer package of this graph as well.
notion image
Duplication, achieved by holding the ALT key while dragging nodes, follows a similar process to copy-paste.
notion image
Cutting involves a two-step process: first, copying the nodes, and then deleting them. As a result, we need to assess whether the nodes can be both copied and deleted.
notion image
In order to facilitate the cut operation, we require two sets for node selection. The first set is for nodes that "can be copied and deleted," while the second set is designated for nodes that "cannot be copied or deleted." Our intention is to cut the nodes that meet both criteria properly, while preserving the selection state of nodes that satisfy at least one criterion. To achieve this, we will begin by categorizing the nodes into these two sets.
notion image
Then, we'll use SetNodeSelection to delete and copy the valid nodes for cutting. We'll also highlight the invalid nodes to provide visual feedback to users. Again, at the end, you need to update the graph.
notion image
Renaming can be a complex operation. We will need to modify the node class to cooperate with this operation. The code in ViewModel is relatively straightforward, and is shown below, and we will implement the code in following section.
notion image

Node

Next comes the highlight of this section, which is the implementation of our node class. First, we create a widget class for our nodes to support renaming. As you can see in the code shown below, our widget class includes a pointer to the node it represents. This design choice enables us to modify the widget's visual representation based on the node's internal state and properties.
notion image
If you delve into the code of the SGraphNode class, you'll see the Unreal Engine has a established renaming mechanism, including functionality like highlighting and focusing the node currently selected when users press F2 to initiate a standard rename transaction. These actions are achieved by setting the RenamePending flag. Consequently, during our implementation, the key is to accurately manage this status flag.
notion image
Here, we're defining the base class for all our node types. If you want something like the Niagara system, which allows you to nest multiple layers of graphs, then you will need to write the same amount of the base node classes.
notion image
In the PostLoad function, we need to ensure that the RF_Transactional flag is set for our nodes to support Undo/Redo.
notion image
AutowireNewNode is responsible for automatically connecting the new node when a wire is dragged from an ancestor's pin. We don’t have an EdGraphSchema yet and compiler may complain about it. We will complete that later.
notion image
Now we will write two kinds of nodes: "Start," serving as the initiation point of the graph, and "InOut," which features both an input and an output. Our initial focus will be on the "StartNode.”
We will write the “Start” node first. The functions in the red frame are interfaces inherited from UEdGraphNode. Notably, functions like CanDuplicateNode, are present. Since the "StartNode" functions as the entry point in our graph, I've set CanDuplicateNode and CanUserDeleteNode to return false. This ensures that users cannot duplicate or delete the "StartNode," aligning with its intended role.
notion image
Now, let's delve into the implementation details. The property bCanRenameNode deternmines whether users can rename this node. If there is no need for users to modify the name, such as in basic computational operator nodes, you can set bCanRenameNode to false. AllocateDefaultPin manages the default pin generation when the node is initially created. If there are additional pin modifications based on certain operations, they won't be handled in this section.
notion image
The following code is straight-forward. You can copy them straight away. Ensure that you utilize the SWidget we defined earlier as the visual widget.
notion image
The implementation for the "InOut" node is relatively straightforward in this tutorial, featuring a simple node structure with one input pin and one output pin.
notion image
notion image

EdGraphSchema

Now, with our nodes in place, we can define the behaviour of these nodes, such as creating new nodes and establishing connections. In Unreal Engine, this is handled by a class called UEdGraphSchema. We will declare our schema which is derived from UEdGraphSchema. Please follow the naming convention here, appending your name after an underscore.
notion image
notion image
GetContextMenuActions will be called when right-clicking the nodes in the editor.
notion image
CanCreateConnection is responsible for determining whether a connection can be established. The two parameter pins will be compared and return a proper response. For now, we can return OK for all types of pins.
notion image
GetGraphContextAction, in comparison to GetContextMenuActions, will be called when right-clicking the empty space in the editor. It will also be called when we drag out a wire from a pin.
notion image
Within this function, you can see there is a SchemaAction class. We will store all node creation actions in an array. This way, when we extend our node types in the future, we can simply add new node creation actions to this array without modifying other code.
notion image
All node creation operations are derived from one base class, which enables us to put all actions into one function.
notion image
We can see there is a NodeTemplate. It is the template of the node we want to create.
notion image
Now, let’s define the AddNewNodeAction function. This function is responsible for adding the previously defined action to the NewAction array mentioned earlier. Additionally, you will notice the NodeDistance variable. When creating a new node, there may not be enough space to place it exactly where the user clicked. Therefore, we introduce a slight offset to ensure the node is placed at a more suitable location.
notion image
Now it is time for us to define this SchemaAction_NewNode.
notion image
notion image
We will acknowledge the GC system and the existence of the NodeTemplate.
notion image
Then we will implement our two PerformAction functions. The reason for that is one function accepts the reference of an array, the other function will only take one single pointer to the pin. This follows the convention of the Unreal Engine.
notion image
We will convert the NodeTemplate to our base class of nodes.
notion image
We will wrap the section with Undo/Redo. We also do not want nodes to overlap, so we will push the node away after creation.
notion image
Now we can create the desired nodes through the context menu. Given that our previous node template takes an “InOut”, we can generate an “InOut” node. If we add another action for a template that takes a “Start” node to the existing actions, we can generate a “Start” node as well. This approach eliminates the need to add actions for nodes with the same creation behavior multiple times. Furthermore, if we have other node classes that require additional creation behaviors, we can directly modify the PerformAction function, the actual implementation of the behavior, instead of repeatedly modifying the code in the Actions section.
Open up your engine, we will see the graph editor looks like this.
notion image
 

Miscellaneous

ConnectionDrawingPolicy

You might have noticed the presence of a peculiar arrow at the end of the wire. This occurs because we haven't provided specific instructions on how to render our graphs.
notion image
To remove the arrow, we set both the arrow image and radius to null values in the constructor. We can further customize the wire's appearance by specifying its thickness and color in the DetermineWiringStyle method. Depending on the pin's data type, we can even draw wires in different colors.
notion image
Finally, we apply this wire drawing strategy in our previously implemented graph schema.
notion image

Starting with an existing node.

Suppose you wish for your graph asset to automatically contain a node when it's opened. This functionality can be achieved within the PostInitProperties function, as previously mentioned during the asset definition.
 
The critical part here is to check whether the asset has any flags related to the Class Default Object (CDO) or requires loading. This ensures that the operation to add a default node only occurs during the asset's initial creation or when the CDO is loaded. Inside this block of code, we initiate the creation of the EdGraph and apply the defined Schema. Additionally, we utilize the NodeCreator to create the desired default node. As a result, when you create and open your asset, you'll find it already equipped with a "StartNode." Furthermore, thanks to our prior settings, users won't be able to delete or duplicate this node, fulfilling the requirement of making it an unalterable starting point for your graph.
notion image

ModuleDependency

The image below displays the essential dependency modules required for this project. While "CustomPrimitive" is a module I've created, all the others are standard components provided by the official Unreal Engine.
notion image
 
 
 
 
 
 
 
Â