API Reference
Joint API Reference
Here is the place where everything fundamental about Joint is explained at. This documentation will be updated and reinforced through the future updates.
Be careful, we are not aiming to explain those stuffs in tutorial mood, but just throw the things we considered on the development, just like a nerd who tells a stuff that only he knows about. 🤓
Also, the sample assets in Joint Native will also greatly help you to understand the concept of the framework. Please consider checking it out.
Dialogue Manager
Dialogue Manager is an asset type that refers to a single set of conversation on project. Dialogue manager is the object that you store all the nodes in.
Finding Base Node on Dialogue Manager
Dialogue Manager contains all the base nodes in the graph in the Nodes property. You can access it on both C++ and BP on the runtime. You can iterate this array and find the nodes you need, most common method to get the node is simply comparing its GUID. We already support finding nodes with Guid with FindBaseNodeWithGuid(). Also comparing its name to identify it is also possible, but not recommended way to go.
Changing the starting point on the runtime
When you play a dialogue manager, the dialogue instance will pick up the first node on the Start Node list and play the dialogue from there. You can manually change this property on the runtime before you start off the dialogue to change the point where you want to start your dialogue from. Notice the list must take base nodes only.
About The Root Node
The node that you see at first when you first opened the dialogue manager is the root node of that dialogue manager. Root node also can have fragments, and the fragments the root node has are called Manager Fragment.
Manager Fragments are the perfect place to store the data you need throughout the dialogue playback because it is really easy to access on every dialogue nodes on the graph because Dialogue Manager provide a tons of functions to find a manager fragment you need. Things about the Manager Fragments are well described on the Fragment part.
Root node is technically not a dialogue node, because it doesn't have dialogue node instance (UDialogueNodeBase
) inside.
Dialogue Manager itself is not replicated
Actually, Dialogue Manager itself is not replicated through the network. Only the nodes are replicated via Dialogue Actor.
Dialogue Actor
Dialogue Actor is an actor object that actually plays a dialogue with a provided dialogue manager.
Dialogue Actor has DialogueGuid property. You can find dialogue actor with this Guid with FindDialogue()
on Joint Subsystem.
You can get the base node the dialogue actor is currently playing with GetPlayingDialogueNode()
Playback Delegates
Dialogue Actor provides delegates related to the node's playback and dialogue's playback.
- OnDialogueStartedDelegate : Will be executed when the dialogue has been started.
- OnDialogueEndedDelegate : Will be executed when the dialogue has been ended.
- OnDialogueBaseNodePlayedDelegate : Will be executed when the dialogue has played a new base node.
- OnDialogueNodeBeginPlayDelegate : Will be executed whenever a fragment or a base node has been played.
- OnDialogueNodeEndPlayDelegate : Will be executed whenever a fragment or a base node has been ended.
- OnDialogueNodePendingDelegate : Will be executed whenever a fragment or a base node has been marked as pending.
Networking
Dialogue Actor is where all the network related actions including replication happen.
Check out the networking related functions.
GAS Supports
Dialogue Actor has a UAbilitySystemComponent
by default, and due to that, you can use a lot of GAS related things with the dialogue actor.
/**
* A component for the GAS implementation.
* A Dialogue instance actor can have gameplay ability by itself, and it can be used in multiple situations.
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="GAS")
UAbilitySystemComponent* AbilitySystemComponent;
public:
UFUNCTION(BlueprintPure, Category="GAS")
UAbilitySystemComponent* GetAbilitySystemComponent() const;
/**
* Actual code where the node instance use to implement a default AbilitySystemComponent sub object.
* Override this function to implement the AbilitySystemComponent with the subclass you desire.
*/
virtual void ImplementAbilitySystemComponent();
Joint Subsystem
Joint Subsystem is a gameinstance subsystem that provides some helpful features you can utilize.
Finding Dialogue
You can find existing dialogue instance actor with FindDialogue, GetAllDialogues
.
Global Dialogue Start & End Delegate
It also provides delegates for Dialogue begin & end events, OnDialogueBeginDelegate, OnDialogueEndDelegate
.
You can access the subsystem on both c++ and bp, and bind events to the delegate to get the dialogues that has been played on the session.
Also, you can get the dialogue instances that has been played in the same frame with GetDialoguesGuidStartedOnThisFrame, GetDialoguesGuidEndedOnThisFrame
.
Those are useful when you want to check out whether there are any dialogues that have been played & ended just before you assign your events on the delegates above.
Dialogue Node
Dialogue Node is a type of object that has been designed to execute its unique events and contain data for the graph.
Dialogue Fragment, Base Nodes are subclasses of Dialogue node.
You can access the node that the node is attached at with GetParentNode()
, and the highest parent on the hierarchy with GetParentmostNode()
, or all the parent node on hierarchy with GetParentNodesOnHierarchy()
You can get the dialogue manager that owns the dialogue node with GetDialogueManager()
.
You can get the dialogue actor that is hosting the dialogue in the world with GetHostingDialogueInstance()
Dialogue Nodes can have Gameplay Tags. the container property is named NodeTags. This is useful for the identification.
You can get the children sub nodes with those functions :
- FindFragmentWithTag
- FindFragmentsWithTag
- FindFragmentWithAnyTags
- FindFragmentsWithAnyTags
- FindFragmentsWithAllTags
- FindFragmentWithAllTags
- FindFragmentWithGuid
- FindFragmentByClass
- FindFragmentsByClass
- GetAllFragments
You can get the children sub nodes all over the lower hierarchy (it will also search through the sub nodes' sub nodes) with those functions :
- FindFragmentWithTagOnLowerHierarchy
- FindFragmentsWithTagOnLowerHierarchy
- FindFragmentWithAnyTagsOnLowerHierarchy
- FindFragmentsWithAnyTagsOnLowerHierarchy
- FindFragmentWithAllTagsOnLowerHierarchy
- FindFragmentsWithAllTagsOnLowerHierarchy
- FindFragmentWithGuidOnLowerHierarchy
- FindFragmentByClassOnLowerHierarchy
- FindFragmentsByClassOnLowerHierarchy
- GetAllFragmentsOnLowerHierarchy
Playback Delegates
Dialogue node provides delegates related to the node's playback.
- OnDialogueNodeBeginDelegate : Will be executed when the node has been played.
- OnDialogueNodeEndDelegate : Will be executed when the node has been ended.
- OnDialogueNodeMarkedAsPendingDelegate : Will be executed when the node has been marked as pending.
Graph Node Customization
Dialogue Nodes are displayed on the graph while being wrapped by Dialogue Editor Graph Node (UDialogueEdGraphNode
) that supports that type of the dialogue node.
To fully customize the visual representation on the graph, you must define custom Dialogue Editor Graph Node for your fragment, but if that is not possible, you can also use this to enhance the productivity.
If you go to the Class Default of the dialogue node class, then you will see those properties there.
Add new element on the Property Data for Simple Display on Graph Node array and fill out the name of the property you want to show on the graph node.
Then you will see this small section that displays the property you wanted.
Also you can specify the color of the node to use in the graph here. Change the value of Graph Node Iconic Color and set Use Specified Graph Node Iconic Color to true.
Playback and Life-cycle of Dialogue Nodes
Begin Play
When Dialogue Nodes begin played on the graph, it iterates and plays the sub nodes. (fragments) It looks like this on the code.
This action is overridable, but applied on every base nodes and any normal fragments on the system by default.
Due to this, when dialogue node has been played the order of the dialogue nodes begin will be as following.
The number in the bracket is the actual order the node begins played.
Another important thing you must realize on this is that every node will be triggered without waiting the previous node to finish by default. It means that the begin play action of the dialogue nodes is not delayed by the other nodes by default.
To achieve the delay effect on the node execution you override the default action on begin play. This is already implemented through the Sequence Node on Joint Native.
See how we implemented that feature on the sequence node.
void UDF_Sequence::SelectNodeAsPlayingNode(UDialogueNodeBase* SubNode)
{
SubNode->OnDialogueNodeMarkedAsPendingDelegate.AddDynamic(this, &UDF_Sequence::OnSubNodePending);
GetHostingDialogueInstance()->RequestNodeBeginPlay(SubNode);
}
void UDF_Sequence::PlayNextSubNode()
{
if (!GetHostingDialogueInstance().IsValid()) return;
CurrentIndex++;
while (SubNodes.IsValidIndex(CurrentIndex))
{
UDialogueNodeBase* SubNode = SubNodes[CurrentIndex];
if (SubNode != nullptr)
{
SelectNodeAsPlayingNode(SubNode);
break;
}
CurrentIndex++;
}
if (!SubNodes.IsValidIndex(CurrentIndex))
{
GetHostingDialogueInstance()->RequestNodeEndPlay(this);
}
}
void UDF_Sequence::OnNodeBeginPlay_Implementation()
{
//reset
CurrentIndex = INDEX_NONE;
PlayNextSubNode();
}
void UDF_Sequence::OnSubNodePending(UDialogueNodeBase* InNode)
{
if (InNode == nullptr) return;
InNode->OnDialogueNodeMarkedAsPendingDelegate.RemoveDynamic(this, &UDF_Sequence::OnSubNodePending);
PlayNextSubNode();
}
End Play
And End Play of the dialogue nodes are almost identical with the begin play action. It iterates the sub nodes on hierarchy in the same order, and trigger NodeEndPlay().
Pending
Pending means the state of node that has played all the necessary logic to execute, ready to finish the node playback anytime the parent node want, and no longer holding the playback.
This is useful when you want to make a node that must continue its action until the whole node ends, but don't want to halt or hold the playback.
You can mark the node as pending by force by executing MarkNodePendingByForce()
on the runtime.
And nodes will be marked as pending whenever they become end played.
Also, Nodes will be tested whether to be marked as pending whenever any of the sub nodes (fragments) is marked as pending.
void UDialogueNodeBase::MarkNodePending()
{
//Don't end again if once ended before.
if (IsNodePending()) return;
bIsNodePending = true;
//Broadcast OnDialogueNodeMarkedAsPendingDelegate Action.
if (OnDialogueNodeMarkedAsPendingDelegate.IsBound())
{
OnDialogueNodeMarkedAsPendingDelegate.Broadcast(this);
}
GetHostingDialogueInstance()->NotifyNodePending(this);
if (ParentNode) ParentNode->MarkNodePendingIfNeeded();
}
And by default, it will be marked as pending when all the sub nodes are marked as pending, and you can control this behavior by overriding that function.
void UDialogueNodeBase::MarkNodePendingIfNeeded()
{
if (CheckCanMarkNodeAsPending())
{
//Mark pending.
MarkNodePendingByForce();
}
}
...
bool UDialogueNodeBase::CheckCanMarkNodeAsPending_Implementation()
{
for (const UDialogueNodeBase* SubNode : SubNodes)
{
if (!SubNode) continue;
if (!SubNode->IsNodePending())
{
return false;
}
}
return true;
}
Base Node (Foundation Node)
(Important) Unit of Network Replication of Playback Synchronization
This is EXTREMELY important to understand when you are going to use Joint on the multiplayer game.
Fragment's properties can be replicated for sure, but the playback of it is not replicated through the network. All the local clients and the server will play the fragments on its own local playback. This means the node's begin play events and end play, pending events will not be replicated (They are not RPC functions at the first place.), and also the flag variables for them will not be replicated by default.
This let you trigger different fragments for each session. The server can trigger its own unique fragments that must be played only on the server, and clients can trigger the nodes for the clients. Also this let you able to exists the players from multiple localization culture even if the asset they use has been slightly changed from the original asset.
But there is only one type of nodes that the playback is replicated. Base Node. The dialogue actor on the host side (specifically the session that has the authority over the dialogue instance) control all the playback of the base nodes.
Even if the clients finishes the playback of the dialogues, they can not move to the next node in local. And also even if they didn't finish the playback of the base node locally, if the host want to move to the next node, then all the clients will move to the node server specified.
Fragment
Fragment is a type of node that has been designed to be attached on the other nodes to execute its unique events and contain data for the graph and its parent nodes.
Networking
Fragments' properties will be replicated, but only when the bReplicate flag is true. Use SetReplicates()
to change whether to replicate the node on the runtime.
And fragments can have RPC functions, but those must be executed through the external actor that has net authority if the session is not the host of the game session. (In most cases, it will be a player controller.)
Manager Fragments
Best Way to Make Global Storage
Manager Fragments are the most effective methods for data storages that can be used throughout the playback, since their life-cycle will follow the dialogue instance's life-cycle and they are the easiest fragments to find on the graph on the performance wise.
You can find those fragments on the Dialogue Manager with the functions related to the manager fragments all over the graph because all dialogue nodes have GetDialogueManager() function, and dialogue manager has a tons of Manager fragment version of find ~~ fragments functions you can use to get the manager fragments.
Also, Manager fragment is good to be a fragment that must be accessible on the outside of the dialogue graph. You can simply grab the node you want with tags, name, class etc.
For example, if you want to provide a set of players that can interact with the dialogue's contents, then you can create a fragments to contains the player states and you can grab them with some tags before you starts off the dialogue. The player fragment in the image above is great example for this.
Dialogue Editor Graph Node
Dialogue Editor Graph Node (UDialogueEdGraphNode
) is an editor only class for the nodes on the graph. It contains various useful features related to the customization.
You can start creating a new graph node class by deriving UDialogueEdGraphNode
(If you want to make a graph node for a fragment, then you must override UDialogueEdGraphNode_Fragment
).
Especially if you want to implement a dialogue node with custom pins, you must override this class in C++.
We are not planning the make the editor node overridable in the blueprint yet.
But don't be scare even if you are not familiar with the c++! It is very easy to do even if it uses code.
We have a separate document about making a custom editor node for the system. Refer to Making Custom Graph Node for Dialogue Node.
Node instance Related
Dialogue Editor Graph Nodes stores the runtime dialogue node they are referring in NodeInstance property. You can access the runtime node with that property, or you can get the type-casted version of it with GetCastedNodeInstance<>
.
If you want to specify the class of the dialogue node that your editor node supports, you can override SupportedNodeClass
and return the class of the dialogue node.
This is important to provide the exact class if you want to make the system automatically use this class when you placed a new node, because the editor will pick up the editor node that provides the exact match class when you are placing a new node on the graph.
TSubclassOf<UDialogueNodeBase> UDialogueEdFragment_Select::SupportedNodeClass()
{
return UDF_Select::StaticClass();
}
You can override GetNodeTitleColor
to specify the iconic color of the nodes on the graph.
FLinearColor UDialogueEdFragment_Select::GetNodeTitleColor() const
{
return FLinearColor(0.4f, 0.2f, 0.5f);
}
You can override OnNodeInstancePropertyChanged
to attach a function that will be triggered whenever any of the properties the node instance has become changed. This is useful when you have to update some of the graph node visuals for the node instance properties
We don't recommend to attach some pin related events on there. Try using ReallocatePins
instead.
void UDialogueEdFragment_LevelSequence::OnNodeInstancePropertyChanged(const FPropertyChangedEvent& PropertyChangedEvent,
const FString& PropertyName)
{
Super::OnNodeInstancePropertyChanged(PropertyChangedEvent, PropertyName);
UpdateThumbnail();
}
Sub Node Related
You can override CanAttachSubNodeOnThis
to tell the system whether it can have the sub node (fragment) whenever a new node is trying to be attached on the node.
FPinConnectionResponse UDialogueEdFragment_Select::CanAttachSubNodeOnThis(const UDialogueEdGraphNode* InSubNode) const
{
if (UDF_Select* Context = InSubNode->GetCastedNodeInstance<UDF_Select>())
{
return FPinConnectionResponse(CONNECT_RESPONSE_DISALLOW,
LOCTEXT("AllowedAttachmentMessage",
"Context node can not have another context node as child."));
}
return Super::CanAttachSubNodeOnThis(InSubNode);
}
Pin Related
You can override AllocateDefaultPins
to implement the default pins for the node on the graph.
UDialogueEdGraphNode provide a property that helps you to implement a pin on the node easily, PinData. You can use this array to implement pins for the node.
void UDialogueEdFragment_Select::AllocateDefaultPins()
{
PinData.Empty();
PinData.Add(FDialogueEdPinData("Out", EEdGraphPinDirection::EGPD_Output));
}
You can override ReallocatePins
to add or remove the pins on the node instance & graph node's property change.
Notice that we are trying to save the pin data that has actual implemented pin on the graph, because if we remove it then any connects with that pin will break.
void UDialogueEdFragment_Branch::ReallocatePins()
{
UDF_Branch* CastedNodeInstance = GetCastedNodeInstance<UDF_Branch>();
if (CastedNodeInstance == nullptr) return;
//Collect all the False pins by whether they are actually implemented or not.
TArray<FDialogueEdPinData> NotImplementedFalsePinData;
TArray<FDialogueEdPinData> FalsePinData;
for (const FDialogueEdPinData& Data : PinData)
{
if (Data.PinName == "False")
{
if (Data.ImplementedPinId == FGuid())
{
NotImplementedFalsePinData.Add(Data);
}
else
{
FalsePinData.Add(Data);
}
}
}
//attach the NotImplementedFalsePinData at the tail so we can pop out those first.
FalsePinData.Append(NotImplementedFalsePinData);
if (!CastedNodeInstance->bUseFalse)
{
//Remove the false pin.
for (FDialogueEdPinData DialogueEdPinData : FalsePinData)
{
PinData.Remove(DialogueEdPinData);
}
}
else
{
//Restore the false pin.
//Pop out if we have too many of them.
while (FalsePinData.Num() > 1)
{
PinData.Remove(FalsePinData.Last());
FalsePinData.Pop();
}
if (FalsePinData.IsEmpty())
{
PinData.Add(FDialogueEdPinData("False", EEdGraphPinDirection::EGPD_Output));
}
}
}
We have NodeConnectionListChanged
that will be triggered whenever any of the pins' connection list has been changed. You must override NodeConnectionListChanged
to collect the nodes that has been attached on the pins.
Check out the UDialogueEdFragment_Select::NodeConnectionListChanged from Joint Native and see how we are getting the connected nodes on the pin
void UDialogueEdFragment_Select::NodeConnectionListChanged()
{
Super::NodeConnectionListChanged();
UDF_Select* CastedNode = GetCastedNodeInstance<UDF_Select>();
if (CastedNode == nullptr) return;
//Clear the next nodes array first because will are going to reallocate them.
CastedNode->NextNodes.Empty();
//Iterate through the pins this graph node has.
for (UEdGraphPin* Pin : Pins)
{
if (Pin == nullptr) continue;
//Find the replicated pin from the parent-most node.
//This is necessary because the pins on the fragment are not implemented on the graph, instead, the parent-most node of the fragment take all the pins on the fragment and replicate them, and display on the graph.
//So to get the connections of the pins, you must get the replicated pin and get the connection from them.
UEdGraphPin* FoundPin = FindReplicatedSubNodePin(Pin);
if (FoundPin == nullptr) continue;
//Iterate through the connected pins on the replicated pin.
for (const UEdGraphPin* LinkedTo : FoundPin->LinkedTo)
{
if (LinkedTo == nullptr) continue;
//Check the connected node and cast it to UDialogueEdGraphNode.
if (LinkedTo->GetOwningNode() == nullptr) continue;
UEdGraphNode* ConnectedNode = LinkedTo->GetOwningNode();
if (!ConnectedNode) continue;
UDialogueEdGraphNode* CastedGraphNode = Cast<UDialogueEdGraphNode>(ConnectedNode);
if (!CastedGraphNode) continue;
//Get the actual node instances from the connected node and allocate it on the Next Nodes.
//This is necessary because the graph node itself might not have the node instances we have to get. (ex-> Connector node)
//It solves such 'redirections' on the connections.
CastedGraphNode->AllocateReferringNodeInstancesOnConnection(CastedNode->NextNodes);
}
}
}
Graph Node Slate Related
In Joint, you don't have to override an actual slate class to display a custom slate on the editor.
What you have to do is to override ModifyGraphNodeSlate
. You can access the node's graph slate with GetGraphNodeSlate
there, and you can grab the existing layout and attach any slates you want on the layout.
Graph node slate provide those slates for the customization. :
TSharedPtr<SImage> NodeHighlightOverlay;
TSharedPtr<SImage> NodeBackground;
TSharedPtr<SBorder> NodeBody;
TSharedPtr<SHorizontalBox> NameBox;
TSharedPtr<SVerticalBox> CenterWholeBox;
TSharedPtr<SVerticalBox> CenterContentBox;
TSharedPtr<SWrapBox> SubNodeBox;
TSharedPtr<SVerticalBox> NodeTagBox;
TSharedPtr<SVerticalBox> PropertyDisplayBox;
See how our UDialogueEdFragment_LevelSequence attach its custom slates on the graph. :
void UDialogueEdFragment_LevelSequence::ModifyGraphNodeSlate()
{
if (!GetGraphNodeSlate().IsValid()) return;
const TSharedPtr<SDialogueGraphNodeBase> NodeSlate = GetGraphNodeSlate();
if (!GetCastedNodeInstance<UDF_LevelSequence>()) return;
const TAttribute<FString> AssetPath_Attr = TAttribute<FString>::Create(TAttribute<FString>::FGetter::CreateLambda(
[this]
{
if (GetCastedNodeInstance<UDF_LevelSequence>() && GetCastedNodeInstance<UDF_LevelSequence>()->
SequenceToPlay)
{
return GetCastedNodeInstance<UDF_LevelSequence>()->SequenceToPlay->GetPathName();
}
return FString();
}));
AssetThumbnailPool = MakeShareable(new FAssetThumbnailPool(24, true));
NodeSlate->CenterContentBox->AddSlot()
.AutoHeight()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Padding(FJointEditorStyle::Margin_Border)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FJointEditorStyle::Margin_Frame)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(SBorder)
.Visibility(EVisibility::SelfHitTestInvisible)
.Padding(FMargin(0, 0, 4, 4))
.BorderImage(FAppStyle::Get().GetBrush("PropertyEditor.AssetTileItem.DropShadow"))
[
SNew(SOverlay)
+ SOverlay::Slot()
.Padding(1)
[
SNew(SBorder)
.Padding(0)
.BorderImage(FStyleDefaults::GetNoBrush())
[
SAssignNew(AssetBox, SBox)
.WidthOverride(ThumbnailSize.X)
.HeightOverride(ThumbnailSize.Y)
]
]
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(FJointEditorStyle::Margin_Frame)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(SObjectPropertyEntryBox)
.DisplayBrowse(true)
.EnableContentPicker(true)
.AllowClear(true)
.ObjectPath(AssetPath_Attr)
.AllowedClass(ULevelSequence::StaticClass())
.OnObjectChanged_UObject(this, &UDialogueEdFragment_LevelSequence::OnAssetSelectedFromPicker)
]
];
UpdateThumbnail();
}