Node Execution Chain & Node Lifecycle
This document covers the core principles of Joint Framework. It is highly recommended to read this document carefully before creating your own custom nodes or fragments.
Joint's fragment based architecture was possible because of this unique design on the lifecycle of nodes.
Understanding the lifecycle of nodes is extremely important to utilize Joint Framework, as it will help you understand how Joint works under the hood, and also help you to create your own custom nodes and fragments that works perfectly with Joint system.
Video Explanation
The whole concept is also explained in the following video. This will greatly help you understand the concept, so please make sure to check it out!
Chain of Node Execution
First of all, let's see how the node execution flows in Joint Framework.
Manager Fragments are Played First, And End Last
Manager fragments are special fragments that are attached to the Joint Manager instead of the base nodes. (accomplished through attaching fragments on the root node of the Joint Manager)
One of the biggest traits of Manager fragment is that they will be played when the Joint Actor instance begins played, and will be ends when the Joint Actor instance ends played.
In other words, Manager fragments will keep being active throughout the Joint.
Thus, You can use manager fragments to implement some logics that should constantly work in the whole Joint, such as calculating the Joint widget's location etc.
Plus, Manager Fragments are the most effective methods for data storages that can be used throughout the playback, since their life-cycle will follow the Joint instance's life-cycle and they are the easiest fragments to find on the graph on the performance wise.
Base Nodes Plays Fragments
After playing the manager fragments, Joint Actor will begin to play the start base node that is connected to the Joint Manager's root node.
This base node will be played as the first base node of the Joint instance, and from here the playback will continue according to the playback flow of the nodes.
Please note that only one base node can be played at the same time in a Joint Actor instance. (unlike fragments, which can be played multiple at the same time)
When the Joint Actor instance ends played in the middle of the playback, it will end play all the base nodes that are currently being played.
Base nodes also have a full lifecycle that consists of Begin Play, End Play, and Pending states, just like fragments (explained later), and what we have to see is how the PostNodeBeginPlay works on the base nodes.
On PostNodeBeginPlay of the base nodes, it will check whether it has any sub nodes (fragments) to play, and if it has, it will play the sub nodes with SubNodesBeginPlay. If not, it will end play itself immediately to prevent hanging nodes.
void UJointNodeBase::PostNodeBeginPlay_Implementation()
{
SubNodes.Remove(nullptr);
if (!SubNodes.IsEmpty())
{
//Play sub nodes if it has.
SubNodesBeginPlay();
}
else
{
//End play if there is no action specified. To prevent this, you must override this function and implement the features you want for this node type.
RequestNodeEndPlay();
}
}
The SubNodesBeginPlay function simply iterates the sub nodes and request begin play for each of them.
void UJointNodeBase::SubNodesBeginPlay()
{
//Propagate BeginPlay action to the children nodes. if it is not necessary or need some other actions for the node class, replace it with something else.
for (UJointNodeBase* SubNode : SubNodes)
{
if (SubNode == nullptr) continue;
SubNode->RequestNodeBeginPlay(GetHostingJointInstance());
}
}
So the important thing we have to see is that, when the base node begins played, it will also begin play all the fragments that are currently being played under the base node.
Vice versa, when the base node ends played, it will also end play all the fragments that are currently being played under the base node. See the code below:
void UJointNodeBase::PostNodeEndPlay_Implementation()
{
SubNodesEndPlay();
}
Fragments Will Play Their Sub Fragments
So, after the base node begins played and plays its sub nodes (fragments), those fragments will also begin play their own sub fragments in the same way by the default behavior of PostNodeBeginPlay.
(Brought it here again to make it easy to see)
void UJointNodeBase::PostNodeBeginPlay_Implementation()
{
SubNodes.Remove(nullptr);
if (!SubNodes.IsEmpty())
{
//Play sub nodes if it has.
SubNodesBeginPlay();
}
else
{
//End play if there is no action specified. To prevent this, you must override this function and implement the features you want for this node type.
RequestNodeEndPlay();
}
}
That default behavior of PostNodeBeginPlay is applied to every node type by default, and it creates a pattern of node execution order like this:

If you're familiar with tree structures, you can see that this is a typical depth-first traversal of tree structure.
But the important point of Joint is that you can override this default behavior of PostNodeBeginPlay, and put your own logic to control how the sub nodes are played.
Node's Life Cycle Stages
We're going to take a closer look at each stage of the node lifecycle now - and learn how each stage works, and how you can control the behavior of each stage by overriding the default behavior.
Pre & Post Begin Play
When a node has been executed, It enters Begin Play state.
The Begin Play action consists of two stages: PreNodeBeginPlay and PostNodeBeginPlay.
Here is the actual code of NodeBeginPlay function, you can see that it first calls PreNodeBeginPlay, then broadcasts the delegate, notifies the hosting Joint instance, and finally calls PostNodeBeginPlay.
The reason why it has 2 stages is to provide a way to implement logic that should be executed before and after the node begin play action respectively.
void UJointNodeBase::NodeBeginPlay()
{
//Don't play again if once played before.
if (IsNodeBegunPlay()) return;
bIsNodeBegunPlay = true;
PreNodeBeginPlay();
if (OnJointNodeBeginDelegate.IsBound())
{
OnJointNodeBeginDelegate.Broadcast(this);
}
GetHostingJointInstance()->NotifyNodeBeginPlay(this);
PostNodeBeginPlay();
}
And here is the default implementation of PreNodeBeginPlay and PostNodeBeginPlay.
void UJointNodeBase::PreNodeBeginPlay_Implementation()
{
}
void UJointNodeBase::PostNodeBeginPlay_Implementation()
{
SubNodes.Remove(nullptr);
if (!SubNodes.IsEmpty())
{
//Play sub nodes if it has.
SubNodesBeginPlay();
}
else
{
//End play if there is no action specified. To prevent this, you must override this function and implement the features you want for this node type.
RequestNodeEndPlay();
}
}
Here is one example of overriding the PostNodeBeginPlay to implement a sequence node behavior:
Joint's node will be triggered without waiting the previous node to finish by default. It means that the begin play action of the Joint 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(UJointNodeBase* SubNode)
{
SubNode->OnJointNodeMarkedAsPendingDelegate.AddDynamic(this, &UDF_Sequence::OnSubNodePending);
GetHostingJointInstance()->RequestNodeBeginPlay(SubNode);
}
void UDF_Sequence::PlayNextSubNode()
{
if (!GetHostingJointInstance().IsValid()) return;
CurrentIndex++;
while (SubNodes.IsValidIndex(CurrentIndex))
{
UJointNodeBase* SubNode = SubNodes[CurrentIndex];
if (SubNode != nullptr)
{
SelectNodeAsPlayingNode(SubNode);
break;
}
CurrentIndex++;
}
if (!SubNodes.IsValidIndex(CurrentIndex))
{
GetHostingJointInstance()->RequestNodeEndPlay(this);
}
}
void UDF_Sequence::OnNodeBeginPlay_Implementation()
{
//reset
CurrentIndex = INDEX_NONE;
PlayNextSubNode();
}
void UDF_Sequence::OnSubNodePending(UJointNodeBase* InNode)
{
if (InNode == nullptr) return;
InNode->OnJointNodeMarkedAsPendingDelegate.RemoveDynamic(this, &UDF_Sequence::OnSubNodePending);
PlayNextSubNode();
}
Pre & Post End Play
When a node has finished its action, It enters End Play state.
And End Play of the Joint Nodes are almost identical with the begin play action. It iterates the sub nodes on hierarchy in the same order, and trigger NodeEndPlay().
Here is the PreNodeEndPlay and PostNodeEndPlay default implementation.
void UJointNodeBase::PreNodeEndPlay_Implementation()
{
}
void UJointNodeBase::PostNodeEndPlay_Implementation()
{
SubNodesEndPlay();
}
Pre & Post 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.
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 MarkNodePendingIfNeeded function.
void UJointNodeBase::MarkNodePendingIfNeeded()
{
if (CheckCanMarkNodeAsPending())
{
//Mark pending.
MarkNodePendingByForce();
}
}
...
bool UJointNodeBase::CheckCanMarkNodeAsPending_Implementation()
{
for (const UJointNodeBase* SubNode : SubNodes)
{
if (!SubNode) continue;
if (!SubNode->IsNodePending())
{
return false;
}
}
return true;
}