This demo shows how Edge conditions and loops work in the graph node system.
Run the Template
- Go to Assets/InworldRuntime/Scenes/Nodesand play theEdgeLoopDemoscene.  
- Whenever you type something, it responds with the input prefixed by *characters on the left.
 
Understanding the Graph
NodeConnectionCanvas contains an InworldGraphExecutor.
 The graph contains three nodes:
The graph contains three nodes:
- TextCombiner: A custom node that adds- *in front of sentences.
- NodeFinal: A custom conversation‑endpoint node that prints the text.
- FilterInput: The start node, a custom node that filters out mismatched input types.
There are three edges:
- FilterInputto- TextCombiner
- TextCombinerto- FilterInput: This is a customized LoopEdge, with- IsLooptoggled.
- FilterInputto- NodeFinal.
FilterInput is the StartNode and NodeFinal is the EndNode.
 You can also see this connection in the Graph Editor. It’s clearer.
You can also see this connection in the Graph Editor. It’s clearer.
 
CustomNode details
TextCombiner
In its overridden ProcessBaseData(), it adds * to the left of the input text and sets it as the output.
public class TextCombinerNodeAsset : CustomNodeAsset
{
    public override string NodeTypeName => "TextCombinerNode";
    
    public string currentText = "";
    protected override InworldBaseData ProcessBaseData(InworldVector<InworldBaseData> inputs)
    {
        if (inputs.Size == 0)
        {
            return new InworldError("No input data", StatusCode.DataLoss);
        }
        InworldBaseData inputData = inputs[0];
        InworldText textResult = new InworldText(inputData);
        if (textResult.IsValid)
            currentText =  $"* {textResult.Text}";
        return new InworldText(currentText);
    }
    
    void OnEnable()
    {
        currentText = "";
    }
}
InworldBaseData that are neither InworldText nor InworldAudio.
public class FilterInputNodeAsset : CustomNodeAsset
    {
        public override string NodeTypeName => "FilterInputNode";
        protected override InworldBaseData ProcessBaseData(InworldVector<InworldBaseData> inputs)
        {
            if (inputs.Size == 0)
            {
                return new InworldError("No input data", StatusCode.DataLoss);
            }
            InworldBaseData inputData = inputs[0]; // YAN: Let's only process the last single input.
            InworldText textResult = new InworldText(inputData);
            if (textResult.IsValid)
                return textResult;
            InworldAudio audioResult = new InworldAudio(inputData);
            if (audioResult.IsValid)
                return audioResult;
            
            return new InworldError($"Unsupported data type: {inputData.GetType()}", StatusCode.Unimplemented);
        }
    }
NodeFinal
NodeFinal uses the custom node ConversationEndpointNodeAsset.
During CreateRuntime(), it stores the speaker’s name and later returns output text containing both the speaker’s name and the result.
It is typically used as the end node in Character Interaction.
ConversationEndpointNodeAsset.cs
public override bool CreateRuntime(InworldGraphAsset graphAsset)
{
    if (graphAsset is CharacterInteractionGraphAsset charGraph)
        m_SpeakerName = m_IsPlayer ? InworldFrameworkUtil.PlayerName : charGraph.characters[0].characterName;
    return base.CreateRuntime(graphAsset);
}
protected override InworldBaseData ProcessBaseData(InworldVector<InworldBaseData> inputs)
{
    if (inputs.Size <= 0)
        return inputs[0];
    
    InworldText text = new InworldText(inputs[0]);
    if (text.IsValid)
    {
        return new InworldText($"{m_SpeakerName}: {text.Text}");
    }
    return inputs[0];
}
Edge
The loop edge from TextCombiner to FilterInput is a customized edge with IsLoop toggled.
 In its overridden checking function
In its overridden checking function MeetsCondition():
if the current loop count exceeds the limit, the edge blocks passage;
otherwise, it allows passage (sending control back to the loop start FilterInput).
Because each iteration prefixes another * before passing the result back to FilterInput, you will see the number of * increase with each loop.
public class LoopEdgeAsset : InworldEdgeAsset
{
    public int echoTimes = 3;
    int m_CurrentLoop = 0;
    public override string EdgeTypeName => "LoopEdge";
    
    protected override bool MeetsCondition(InworldBaseData inputData)
    {
        Debug.Log($"Current Loop: {m_CurrentLoop} -> {echoTimes}");
        m_CurrentLoop++;
        if (m_CurrentLoop < echoTimes) 
            return m_AllowedPassByDefault;
        m_CurrentLoop = 0;
        return !m_AllowedPassByDefault;
    }
    void OnEnable()
    {
        m_CurrentLoop = 0;
    }
}
Be mindful of memory allocation when using Loop Edges.Because the graph node system executes inside C++, each iteration may allocate new memory.
InworldController
The InworldController contains no primitive modules.
Workflow
- When the game starts, InworldControllerinitializes immediately because there are no primitives.
- Next, InworldGraphExecutorinitializes its graph asset by calling each component’sCreateRuntime().
For howCreateRuntime works on custom nodes, see the CustomNode Demo.
For edges, during CreateRuntime() the system calls SetEdgeCondition() and registers OnConditionCheck as the function pointer for the condition.
Inside OnConditionCheck, the system calls the overridden MeetsCondition() virtual function implemented by each edge subclass.
public bool CreateRuntime(EdgeWrapper wrapper)
{
    if (wrapper == null || !wrapper.IsValid)
        return false;
    m_RuntimeWrapper = wrapper;
    if (IsLoop)
        m_RuntimeWrapper.SetToLoop();
    if (!IsRequired)
        m_RuntimeWrapper.SetToOptional();
    SetEdgeCondition(); // <==
    m_RuntimeWrapper.SetCondition(m_Executor); 
    m_RuntimeWrapper.Build();
    return true;
}
protected void SetEdgeCondition(string customEdgeName = "")
{
    if (!m_IsMultiThread)
    {
        EdgeConditionExecutor executor = new EdgeConditionExecutor(OnConditionCheck, this);
        if (!string.IsNullOrEmpty(customEdgeName))
            InworldComponentManager.RegisterCustomEdgeCondition(customEdgeName, executor);
        
        m_Executor = executor;
    }
    else
    {
        EdgeConditionThreadedExecutor executor = new EdgeConditionThreadedExecutor(OnConditionCheck);
        if (!string.IsNullOrEmpty(customEdgeName))
            InworldComponentManager.RegisterCustomEdgeCondition(customEdgeName, executor);
        m_Executor = executor;
    }
}
static void OnConditionCheck(IntPtr data)
{
    InworldEdgeAsset edgeAsset = GCHandle.FromIntPtr(data).Target as InworldEdgeAsset;
    if (edgeAsset == null)
        return;
    InworldBaseData inputData = new InworldBaseData(InworldInterop.inworld_EdgeConditionExecutor_GetLastInput());
    InworldInterop.inworld_EdgeConditionExecutor_SetNextOutput(edgeAsset.MeetsCondition(inputData));
}
- 
After initialization, the graph calls Compile()and returns the executor handle.
- 
After compilation, the OnGraphCompiledevent is invoked.
In this demo,NodeConnectionTemplate subscribes to it and enables the UI components.
Users can then interact with the graph system.
protected override void OnGraphCompiled(InworldGraphAsset obj)
{
    foreach (InworldUIElement element in m_UIElements)
        element.Interactable = true;
}
- 
After the UI is initialized, send the input text to the graph.
- 
Calling ExecuteGraphAsync()causes the graph to loop, sending theTextCombinerresult back toFilterInputuntil the loop count configured byLoopEdgeis reached.
It then produces a result and invokesOnGraphResult(), which NodeConnectionTemplate subscribes to in order to receive the data.
protected override void OnGraphResult(InworldBaseData obj)
{
    InworldText response = new InworldText(obj);
    if (response.IsValid)
    {
        string message = response.Text;
        InsertBubble(m_BubbleLeft, Role.User.ToString(), message);
    }
}