Skip to main content

Alternative Exit and Parallel Flow

If you have read the Overview - Sequine Flow section, you should've been familiar that each type of Sequine Flow needs an executor in order for the to be executed.

A command executor is responsible to execute each of the command in a certain command execution thread. It can contain multiple threads of command execution to support running parallel threads. A command execution thread is called CommandExecutionFlow, or flow to be short.

1. Alternative Exit

We know that upon executing a command, we need to call the Exit method to continue the flow to the next command. When exiting, the command knows which command from which output index to execute next by using GetNextOutputIndex method.

By default, when we don't override GetNextOutputIndex, it will look for a command at output index 0, or returning -1 if there is not even a single output. Here's the source code of how the default GetNextOutputIndex method looks like.

...
public class Command {
...
public virtual int GetNextOutputIndex() {
if (nextIds.Count > 0) return 0;
else return -1;
}
...
}
...

If we want to have another main out-point as an alternative exit, then we have to tell the GetNextOutputIndex method to return a different output index.

Adding a Main Out-Point

Let's learn how to add an alternative exit by creating a new Command, which randomly chooses which output index it decides to continue flowing.

Create a new command script by opening the Assets menu and choose Create -> Sequine -> C# Command Script. Name the new file to RandomExitCommand (optional).

Random Exit Command

We have learned how to add some outputs from Creating Custom Command Type section. But now instead of adding property out-points, we have to add main out-points.

Here are the steps to add a main out-point:

  • Add a new connection target of nextIds inside Editor_InitOutPoints method.
  • Add a new main out-point using CommandGUI.AddMainOutPoint method inside Editor_InitOutPoints method.
  • Call CommandGUI.DrawOutPoint inside Editor_OnDrawContents method to draw the out-point.
...
public class RandomExitCommand : Command
{
public override void Execute(CommandExecutionFlow _flow)
{
Exit();
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
base.Editor_InitOutPoints();
//nextIds at index 0 is the default exit out-point
if (nextIds.Count < 2) nextIds.Add(new List<ConnectionTarget>());
CommandGUI.AddMainOutPoint();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
base.Editor_OnDrawContents(_absPosition);
CommandGUI.DrawOutPoint(1);
CommandGUI.DrawLabel("Alternative Exit", true);
}
#endif
}

Alternative Main Out-Point

Overriding GetNextOutputIndex Method

Currently, when we execute the Sequine Flow, the RandomExitCommand will always exited from the default main out-point (the first one).

To change that behaviour, we need to override the GetNextOutputIndex method.

...
public class RandomExitCommand : Command
{
public override void Execute(CommandExecutionFlow _flow)
{
Exit();
}

public override int GetNextOutputIndex()
{
return base.GetNextOutputIndex();
}
...
}

Let's change the behaviour so that it randomly selects a value either 0 or 1 for the output index to use.

...
public override int GetNextOutputIndex()
{
int randomIndex = Random.Range(0, 2); //randomly select 0 or 1
return randomIndex;
}
...

Alternatively, we can also store the variable globally so that we don't have to directly decide choose the next output index inside the GetNextOutputIndex. For example, we can decide it inside the Execute method instead.

...
private int randomIndex;

public override void Execute(CommandExecutionFlow _flow)
{
randomIndex = Random.Range(0, 2);
Exit();
}

public override int GetNextOutputIndex()
{
return randomIndex;
}
...

The code above is also a valid way to decide the output index. Since GetNextOutputIndex method is getting called upon exiting the execution, then Execute method is obviously called before that.

Let's try playing it over and over. We'll see that it randomly executes either the default exit or the alternative exit.

Random Exit

note

Exit method chooses only 1 output to continue the flow, therefore, Alternative Exit does not create a new flow.

2. Parallel Flow

There is often a case where we need the command to exit through multiple outputs. In Sequine itself for example, we often have some commands that has a default output for immediate exit, and one more output for on complete which is used when the task actually completed at certain time.

A flow should execute commands linearly in order. Therefore, to create a parallel flow, we have to create a sub flow, which technically just another flow that instead of executing from the Start node, it executes starting from the command that is referred by the output.

Adding a Main Out-Point

Adding a main out-point for a parallel flow is no different than alternative exit. So, we can simply repeat what we've learned before.

Instead of creating a new script, let's just extend our RandomExitCommand.cs. This time, let's make a parallel main out-point that is started after waiting for 5 seconds since execution.

We can use CommandGUI.AddRectHeight method to add some space vertically.

...
#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
base.Editor_InitOutPoints();
//nextIds at index 0 is the default exit out-point
if (nextIds.Count < 2) nextIds.Add(new List<ConnectionTarget>()); // for alternative exit
CommandGUI.AddMainOutPoint();

if (nextIds.Count < 3) nextIds.Add(new List<ConnectionTarget>()); // for parallel flow
CommandGUI.AddMainOutPoint();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
base.Editor_OnDrawContents(_absPosition);
CommandGUI.DrawOutPoint(1); //draw alternative exit output
CommandGUI.DrawLabel("Alternative Exit", true);
CommandGUI.AddRectHeight(5f);
CommandGUI.DrawOutPoint(2); //draw parallel flow output
CommandGUI.DrawLabel("After 5 Seconds", true);
}
#endif
...

Here's how it's going to look like after we added one more main out-point.

Parallel Flow

Calling RunSubFlow Method

Now all that is left is to actually do something with that main out-point. We've decided that we want to start it after 5 seconds since execution. In that case, we need anything that can delay a process.

We can use something like Unity's built-in Coroutine, async Task, or even an external plugin like DOTween for example. For now, let's simply use an async Task.

Before running a sub-flow, we have to create a SubFlowInfo first. A sub-flow info must be created at the same frame when the Execute method is called using _flow.CreateSubFlow(_outputIndex) method, where _outputIndex is the index of the main out-point we want to run. In this case, the output index is 2.

After that, we can call RunSubFlow method in our asynchronous method, passing the sub-flow info with ref keyword.

...
public override void Execute(CommandExecutionFlow _flow)
{
randomIndex = Random.Range(0, 2);

SubFlowInfo subFlow = _flow.CreateSubFlow(2);
WaitFor5Seconds(subFlow);

Exit();
}

private async void WaitFor5Seconds(SubFlowInfo _subFlowInfo)
{
await Task.Delay(5000); //5000 miliseconds
RunSubFlow(ref _subFlowInfo);
}
...

Let's setup something to log for that parallel flow. We'll see that the Log command from the After 5 Seconds output is started after 5 seconds.

Run Sub Flow

note

RunSubFlow creates a new flow context. This enables a command to continue flowing through multiple next commands.

Full Code

RandomExitCommand.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;
using System.Threading.Tasks;

[System.Serializable]
[RegisterCommand("Custom/RandomExitCommand", typeof(SequineFlowCommandData))]
public class RandomExitCommand : Command
{
private int randomIndex;

public override void Execute(CommandExecutionFlow _flow)
{
randomIndex = Random.Range(0, 2);

SubFlowInfo subFlow = _flow.CreateSubFlow(2);
WaitFor5Seconds(subFlow);

Exit();
}

private async void WaitFor5Seconds(SubFlowInfo _subFlowInfo)
{
await Task.Delay(5000); //5000 miliseconds
RunSubFlow(_subFlowInfo);
}

public override int GetNextOutputIndex() {
return randomIndex;
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
base.Editor_InitOutPoints();
//nextIds at index 0 is the default exit out-point
if (nextIds.Count < 2) nextIds.Add(new List<ConnectionTarget>()); // for alternative exit
CommandGUI.AddMainOutPoint();

if (nextIds.Count < 3) nextIds.Add(new List<ConnectionTarget>()); // for parallel flow
CommandGUI.AddMainOutPoint();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
base.Editor_OnDrawContents(_absPosition);
CommandGUI.DrawOutPoint(1); //draw alternative exit output
CommandGUI.DrawLabel("Alternative Exit", true);
CommandGUI.AddRectHeight(5f);
CommandGUI.DrawOutPoint(2); //draw parallel flow output
CommandGUI.DrawLabel("After 5 Seconds", true);
}
#endif
}