Making A New Editor Node
What Is Editor Node?
Editor Nodes (a.k.a Graph Node) are the type of nodes that are used as interfaces between user interaction on the graph and the node object (Node instance) itself.
What Is The Differences Between Editor Node And Node Instance?
In Joint, we name the actual node object as Node instance, and the graph node as Editor Node.
Node instances
are the basic objects that will contain the data that will be utilized on runtime and also some logic runs on the runtime to manipulate the dialogue itself.
On the other hand, Editor Nodes
are the objects that will only available on the graph and editor module in the development.
So if you want to make some properties available on the runtime Editor nodes are the objects that rely on the editor graph instance, and they represent the node instances they have in the graph.
Editor nodes will handle some of the important data and process related to the editor feature including slate population of the graph node, and pin data control for the graph, and any addtional process to manipulate and pass over the data for the node instances that the editor node represent.
You don't need to make unique editor nodes for each node instance classes on the system since the default editor node that joint will assign to the node instances (fragments, base nodes) already handles most of the logic with quite elegant and decent ways, and also the node instances now have some particular ways to manipulate the data in the editor node (ex, custom pin system).
Yet manipulating editor nodes in node instances has some limitations thus knowing how to work with Editor nodes (UDialogueEdGraphNode) is important for such conditions:
- Slate control.
- Complex editing features and data control including pin data editing, better editor transaction and communication with the editor modules.
Still, making a whole new editor node class for a specific node might not fit well for your project's scale and budget. and even if your product's budget allows you to do so, it might be still a frustrating decision to make. So check out the later part of this document first and reconsider about it with the PM.
Especially in Joint 2.3.0 update, Pin data can be now fully controlled on the node instance side, so basically, you don't need to work with the editor node to accomplish such actions. We are going to describe about how to control the pin in editor nodes in this tutorial, but still controlling the pins on the node instance side is much more easier to go in most cases.
Making a New Editor Node Class
We only support implmenenting a new editor node class via C++, if you are working on the BP only project, then you are out of luck.
You can force to make a new editor node class in BP, but it will not work as intended. Please don't do that.
Start making a new editor node class by overriding UDialogueEdGraphNode
in C++.
For fragment editor node class, Please use UDialogueEdGraphNode_Fragment
.
Specifying Node Instance Class for Editor Node Class.
Let's start this out by overriding SupportedNodeClass
function.
SupportedNodeClass
is a function where you can set and tell the system about which class of the node instance object your editor node is designed to be used with. It can be C++ class or BP class as needed.
TSubclassOf<UDialogueNodeBase> UDialogueEdFragment_Sequence::SupportedNodeClass()
{
return UDF_Sequence::StaticClass();
}
'''note
For BP classes, you can use some convenient function that joint native provide to grab the BP class by its name, GetBPClassWithName
in UJointNativeFunctionLibrary
but please be careful with it since by any changes on its name can break your code.
'''
Once you overrided this class properly, now the editor will automatically pick and use your new editor node class for the specified node instance type when you newly create a node on the graph editor.
'''warning If multiple editor nodes specify the same node as its supported node class, then it will pick the first one on the class cache in the system singleton, which mean, you can not control or specify it on the editor side as you want. Just avoid doing it so. '''
Basically, it's the only mandatory stuff you need to do when you are implementing a new editor node class to the system. The following instructions are all optional, so please read and use them as you need.
Populating and Manipulating Custom Slates (Widgets)
In Joint, unlike SDS1, now every custom slates that you need for your node class can be created (populated) and controlled via editor node class, and you don't need to make a whole new slate class for the custom widget (still you can if you need) and just patch up the slate fragment on the graph slate's layout.
(인덴테이션 제거 바람)
/**
* Use this function to populate any default widgets on the graph node body.
* Access the slate and change the slate as you want.
*
* If you must change the slate after it is populated, you can access the slate with GetGraphNodeSlate(). Use this function on further customization and instancing.
*
* Note for SDS1 users : You don't need to create new node slate class and mess around the editor to assign the slate anymore because now we have this function.
* See how the new native editor fragments utilize this function.
*/
virtual void ModifyGraphNodeSlate();
Let's start making our custom slates by overriding this function, and check out how the nodes in the Joint Native is using this function to populate its own unique slates.
void UDialogueEdFragment_Participant::ModifyGraphNodeSlate()
{
//Abort if we can not get the graph node slate instance.
if (!GetGraphNodeSlate().IsValid()) return;
const TSharedPtr<SDialogueGraphNodeBase> NodeSlate = GetGraphNodeSlate();
ParticipantBox = SNew(SVerticalBox);
NodeSlate->CenterContentBox->AddSlot()
.HAlign(HAlign_Fill)
//.VAlign(VAlign_Fill)
.Padding(FJointEditorStyle::Margin_Frame)
[
ParticipantBox.ToSharedRef()
];
UpdateSlate();
}
The actual result is as following:
(사진 추가)
In ModifyGraphNodeSlate(), you can populate the initial slates you can use to decorate your graph node. One thing that you must know is that this function will not be triggered again after the graph refreshing request. thus you can not use this function to update your slate, but only initializing.
if you need a slate that doesn't change over time and change on the other propertie's modifications, it will do anything, but if you need to make your slate be updated according to some property modification, then you have to manually update your slate on somewhere else.
/**
* Callback for the node instance's property change event. Override and use this function to implement actions for the property change.
* By default, it is used to call UpdatePins, RequestPopulationOfNodeTagWidgets and NodeConnectionListChanged.
*/
virtual void OnNodeInstancePropertyChanged(const FPropertyChangedEvent& PropertyChangedEvent, const FString& PropertyName);
OnNodeInstancePropertyChanged() is one of the best places you can update your slate as you need. This function will be executed whenever any properties on the node instance have been changed, so you can do whatever you need for your slate on here.
For example, In Joint Native, UDialogueEdFragment_SpeakerAndListener
updates its slate on OnNodeInstancePropertyChanged like this.
void UDialogueEdFragment_SpeakerAndListener::OnNodeInstancePropertyChanged(
const FPropertyChangedEvent& PropertyChangedEvent,
const FString& PropertyName)
{
Super::OnNodeInstancePropertyChanged(PropertyChangedEvent, PropertyName);
UpdateSlate();
}
void UDialogueEdFragment_SpeakerAndListener::UpdateSlate()
{
if (!GetGraphNodeSlate().IsValid()) return;
const TSharedPtr<SDialogueGraphNodeBase> NodeSlate = GetGraphNodeSlate();
SpeakersBox->ClearChildren();
ListenersBox->ClearChildren();
UDF_SpeakerAndListener* SpeakerAndListener = GetCastedNodeInstance<UDF_SpeakerAndListener>();
if (SpeakerAndListener == nullptr) return;
for (int i = 0; i < SpeakerAndListener->Listeners.Num(); ++i)
{
if (!SpeakerAndListener->Listeners.IsValidIndex(i)) continue;
FDialogueNodePointer& Listener = SpeakerAndListener->Listeners[i];
const TAttribute<FText> DisplayText_Attr = TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateLambda(
[Listener]
{
if (Listener.Node == nullptr) return LOCTEXT("NoParticipant", "No Participant Specified");
if (const UDF_Participant* CastedNode = Cast<UDF_Participant>(Listener.Node.Get()))
return FText::FromString(
CastedNode->ParticipantTag.ToString());
return FText::GetEmpty();
}));
ListenersBox->AddSlot()
[
SNew(SDialogueNodePointerSlate)
.GraphContextObject(this)
.DisplayName(DisplayText_Attr)
.PointerToStructure(&Listener)
.bShouldShowDisplayName(true)
.bShouldShowNodeName(true)
];
}
for (int i = 0; i < SpeakerAndListener->Speakers.Num(); ++i)
{
if (!SpeakerAndListener->Speakers.IsValidIndex(i)) continue;
FDialogueNodePointer& Speaker = SpeakerAndListener->Speakers[i];
const TAttribute<FText> DisplayText_Attr = TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateLambda(
[Speaker]
{
if (Speaker.Node == nullptr) return LOCTEXT("NoParticipant", "No Participant Specified");
if (const UDF_Participant* CastedNode = Cast<UDF_Participant>(Speaker.Node.Get()))
return FText::FromString(
CastedNode->ParticipantTag.ToString());
return FText::GetEmpty();
}));
SpeakersBox->AddSlot()
[
SNew(SDialogueNodePointerSlate)
.GraphContextObject(this)
.DisplayName(DisplayText_Attr)
.PointerToStructure(&Speaker)
.bShouldShowDisplayName(true)
.bShouldShowNodeName(true)
];
}
}
In UDialogueEdFragment_SpeakerAndListener, it populates two custom vertical box on its layout and use that box to shows the widget that indicates each speakers and listeners that are assigend to the node, and update the content of the box in OnNodeInstancePropertyChanged() with a function named UpdateSlate().
You can see that we are accessing the graph node slate via GetGraphNodeSlate
and populating our slates on a predefined layout on the slate named CenterContentBox
.
SDialogueGraphNodeBase
has following layout slates :
TSharedPtr<SBorder> NodeBody;
TSharedPtr<SHorizontalBox> NameBox;
TSharedPtr<SVerticalBox> CenterWholeBox;
TSharedPtr<SVerticalBox> CenterContentBox;
TSharedPtr<SWrapBox> SubNodeBox;
TSharedPtr<SVerticalBox> NodeTagBox;
TSharedPtr<SVerticalBox> PropertyDisplayBox;
They are all guaranteed to be available after the graph node slate initalization stage, which means the editor initialization.
You can grab a layout slate as you want and attach a slate from there.
'''note We are not going to go through all the steps related to the slate system here. Check out the other materials for your need.
And if you need, you can check out some of the editor nodes in Joint Native to start off how to make your own editor node slate with. '''
Using Volt on Your Node Slates
Joint provide a strong slate animation library called Volt and every animation effects on the editor and graph are made with and powered by Volt,
You can also use that in your graph node slates too! In fact, Volt is licensed under MIT license, so if you need, then it is okay to fork it and use it on your own product. (See the official GitHub repository. (https://github.com/GGgRain/Unreal-Volt))
'''note Volt is completely independent module from the Joint and JointEditor Module. You need to include the Volt, VoltCore module to your module if you are working on the other module than Joint, JointNative, JointEditor modules.
Here is the brief explanation for each modules, please check out and use these as you need:
VoltCore : The framework itself. it includes all the classes to animate your slates. This is necessary if you want to work with Volt. Volt : The content module for the animations. It only includes the animation modules and animations themselves. Use this module when you need templete classes for Volt and slate animations. This module is dependent on VoltCore. '''
We are not going to take a look at all the details you have to know about Volt to use it, instead we will learn how to quick-start it from the beginning. Check out our official documentation about Volt on the official Github repository.
Let's assume that you are going to use Volt on your slates on your custom graph nodes. GraphNode slates will have its own UVoltAnimationManager inside, so as long as your slate sticks with the node slate, you don't need to implement a whole new VoltAnimationManager.
Try accessing the existing animation manager via SDialogueGraphNodeBase::GetAnimationManager()
.
/**
* Get the Volt animation manager that handles the slate animations on the node.
* @return The Volt animation manager of the graph node slate.
*/
UVoltAnimationManager* GetAnimationManager() const;
It is guaranteed to be accessible on almost every stage of the lifecycle of the slate, and even if it is not present yet, it will try to create a new instance and handle you the reference of that newly created instance once you try to access that manager via SDialogueGraphNodeBase::GetAnimationManager()
.
Please let us know if it was not accessible via this function, we still can not guarantee it 10/10 because there might be some unfortunate occasions
Once you grab that instance, then now you can animate your slates with that instance.
if (UVolt_ASA_Expand* Animation = VOLT_GET_ANIMATION<UVolt_ASA_Expand>(UVolt_ASA_Expand::StaticClass()))
{
VOLT_PLAY_ANIM(GetAnimationManager(), NodeBody, Animation); // In header; TSharedPtr<SBorder> NodeBody;
}
Here is an example that you can find in Joint Editor, which is actually the part of the code that we animates the graph node slate with expanding animation when the drag-drop operation moves onto the slate.
Joint's Built-In Graph Node Slates
JointEditor module involves various helpful slate classes that you can use on your fragment to boost your productivity.
SDialogueNodePointerSlate
SDialogueNodePointerSlate is a slate class that can be used to indicate and edit a single node pointer structure (FDialogueNodePointer
).
//In DialogueEdFragment_SpeakerAndListener.cpp...
SNew(SDialogueNodePointerSlate)
.GraphContextObject(this)
.DisplayName(DisplayText_Attr)
.PointerToStructure(&Speaker)
.bShouldShowDisplayName(true)
.bShouldShowNodeName(true)
You can populate it on the layout and feed structure's pointer to the slate to make the slate display that structure.
Check out Joint native's DialogueEdFragment_SpeakerAndListener.cpp for the further details.
SContextTextEditor (WYSIWYG editor for text properties)
Joint provides simple WYSIWYG editor for the text editing.
SAssignNew(ContextTextEditor, SContextTextEditor)
.Text(ContextText_Attr)
.TableToEdit(TableToEdit_Attr)
.bUseStyling(bUseStyler_Attr)
.OnTextChanged_UObject(this, &UDialogueEdFragment_Text::OnTextChange)
.OnTextCommitted_UObject(this, &UDialogueEdFragment_Text::OnTextCommitted)
It will take the text attribute for the text being displayed on the editor, and take another attribute that refers to the textstyle table for the styling.
It will not work as a WYSIWYG editor if you don't set bUseStyling true even if you provided a valid textstyle table to the slate, if it is false, it will work just same as a simple text box for the text editing.
Please notice that the update on the text on the slate will not be replicated back to the original property! You must manually update it via ``OnTextChanged, OnTextCommitted` instead.
Check how UDialogueEdFragment_Text
is handling the update on the slate.
It is also utilizing editor transactions here to notify the modifications on the objects to the editor and record them. It will be a good, solid example for the transactions on the editor nodes either.
void UDialogueEdFragment_Text::OnTextChange(const FText& Text)
{
UDF_Text* CastedNode = GetCastedNodeInstance<UDF_Text>();
if (!bHasTransaction)
{
GEditor->BeginTransaction(FText::FromString("Modify Context Text"));
bHasTransaction = true;
}
CastedNode->Modify();
const FText SavedText = CastedNode->Text;
FString OutKey, OutNamespace;
const FString Namespace = FTextInspector::GetNamespace(SavedText).Get(FString());
const FString Key = FTextInspector::GetKey(SavedText).Get(FString());
//Holds new text and let engine caches this text in.
FJointEdUtils::DialogueText_StaticStableTextIdWithObj(
this,
IEditableTextProperty::ETextPropertyEditAction::EditedSource,
Text.ToString(),
Namespace,
Key,
OutNamespace,
OutKey
);
CastedNode->Text = FText::ChangeKey(FTextKey(OutNamespace),FTextKey(OutKey),Text);
}
void UDialogueEdFragment_Text::OnTextCommitted(const FText& Text, ETextCommit::Type Arg)
{
UDF_Text* CastedNode = GetCastedNodeInstance<UDF_Text>();
if (!bHasTransaction)
{
GEditor->BeginTransaction(FText::FromString("Modify Context Text"));
bHasTransaction = true;
}
CastedNode->Modify();
const FText SavedText = CastedNode->Text;
FString OutKey, OutNamespace;
const FString Namespace = FTextInspector::GetNamespace(SavedText).Get(FString());
const FString Key = FTextInspector::GetKey(SavedText).Get(FString());
//Holds new text and let engine caches this text in. This is fundamental when it comes to the
FJointEdUtils::DialogueText_StaticStableTextIdWithObj(
this,
IEditableTextProperty::ETextPropertyEditAction::EditedSource,
Text.ToString(),
Namespace,
Key,
OutNamespace,
OutKey
);
CastedNode->Text = FText::ChangeKey(FTextKey(OutNamespace),FTextKey(OutKey),Text);
if (bHasTransaction)
{
GEditor->EndTransaction();
bHasTransaction = false;
}
}
Manipulating Pins
Pins are the objects that will be displayed right next to the node (left and right) allowing you to getter any possible connections with other nodes.
Using custom pins can boost your process and help you implement unique fragments that fit to your needs.
For example, if your dialogue fragment need to control the flow of the dialogue by the conditional switching, you can provide multiple pins for each branches your fragment needs and getter the nodes attached to those pins and use that as you want, or if you want to play some additional dialogue lines while following the main flow, you can grab some of the nodes through those additional pins and play them in transient dialogue instances your fragment creates. (This is how we implemented this scene in the official showcase video)
'''note About making a custom pin, node instance's easy custom pin system will let you create new custom pins and control them without implementing a new editor node. but still, making it with custom editor node class will work in more clearer way in many other cases. Please check out this article. '''
Complex Editing Features
Unreal always have those stuff only can be controlled via C++, and sometimes only in the editor modules, not present in runtime modules.
In fact, One of the most fundamental reasons of having explict editor nodes for each node instances on the graph comes out from here. There are some useful editor features that surely will improve your productivity, and we are going to look at those one by one.