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).
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
insideEditor_InitOutPoints
method. - Add a new main out-point using
CommandGUI.AddMainOutPoint
method insideEditor_InitOutPoints
method. - Call
CommandGUI.DrawOutPoint
insideEditor_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
}
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.
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.
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.
RunSubFlow
creates a new flow context. This enables a command to continue flowing through multiple next commands.
Full Code
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
}