Auto Draw Command
An Auto Draw Command is a command which the editor node (including fields, in-points, and out-points) is automatically drawn, just like how we normally work with MonoBehaviour
or ScriptableObject
in Inspector.
To declare our custom command as an Auto Draw Command, we need to add ICommandAutoDraw
interface to our custom command class declaration. As of version 1.6.0, it is automatically added when we create a command script by opening the Assets menu and choose Create -> Sequine -> C# Command Script.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;
[System.Serializable]
[RegisterCommand("Custom/My Command", typeof(SequineFlowCommandData))]
public class MyCommand : Command, ICommandAutoDraw
{
public override void Execute(CommandExecutionFlow _flow)
{
//Your logic here... (Before Exit Method)
Exit();
}
}
Fields
Any public
field or a field that is declared with [SerializeField]
attribute will be automatically drawn.
...
public class MyCommand : Command, ICommandAutoDraw
{
public string myString;
...
}
This also works with nested serializable classes.
...
public class MyCommand : Command, ICommandAutoDraw
{
public override float nodeWidth => 275f;
[System.Serializable]
public class ClassA
{
public int myInt;
public float myFloat;
public ClassB bInstance;
}
[System.Serializable]
public class ClassB
{
public bool myBool;
public Vector2 myVector;
}
public string myString;
public ClassA aInstance;
...
}
Experimentals
Array fields can also be automatically drawn, although this is still experimental. It doesn't actually work with array (System.Array
), but we have to use List
. However, there are some known issues and limitations where this won't work with:
- We can't undo reorder elements
- Nesting a field more than once (class inside class) will be correctly drawn but will not modify the values
- Nested array is currently not supported
public class MyCommand : Command, ICommandAutoDraw
{
...
public List<ClassB> bArray;
...
}
Alternatively, to pass an array to a command, we can also make a component (MonoBehaviour
) or asset (ScriptableObject
) that holds the array, and pass that component/asset to the command. So, in our command class, instead of having an array field, we use a component/asset field.
Property In-Points
Instead of getting the value strictly from the field's value, we can declare it as an in-point, where the value can be assigned from an in-point from the command node. We do it by adding [PropertyInPoint(inputIndex)]
attribute on top of our field's declaration.
The value input box will disappear when the in-point is connected, indicating that the actual field's value will be ignored, and the dynamic value from the connected command node will be used instead.
...
public class MyCommand : Command, ICommandAutoDraw
{
..
[PropertyInPoint(0)]
public string myString;
...
}
Not only on field, we can actually use [PropertyInPoint(inputIndex)]
attribute on top of a method or property declaration. By doing so, since method and property are not serializable and have its own value calculation, the in-point will become a pure in-point without a value input being drawn.
...
public class MyCommand : Command, ICommandAutoDraw
{
..
[PropertyInPoint(0)]
public string myString;
[PropertyInPoint(1)]
public string pureInPointString { get { return "abc"; } }
[PropertyInPoint(2)]
public int GetIntegerValue() { return 5; }
...
}
Notice that the in-points are not necessariy drawn in order of the point index. They are drawn using the order of the field's declaration. Except if they are methods or properties, then they will all be drawn first before the fields (non-pure in-points).
Using Property In-Point at Runtime
To get the value of our in-point during runtime, we use GetInput
method, passing _flow
and _index
parameters, where _index
is the index of the in-point. This will get the value from the connected command, and if it's not connected, then the value is retrieved from the referred field/method/property by default.
...
public class MyCommand : Command, ICommandAutoDraw
{
...
public override void Execute(CommandExecutionFlow _flow)
{
string myStringValue = GetInput<string>(_flow, 0);
string pureInPointStringValue = GetInput<string>(_flow, 1);
int integerValue = GetInput<int>(_flow, 2);
Debug.Log("Values: " + myStringValue + ", " + pureInPointStringValue + ", " + integerValue);
Exit();
}
}
Optimizing Input
By default, GetInput
simply works out of the box due to the ICommandAutoDraw
and [PropertyInPoint(inputIndex)]
being used. The retrieved value's type is object
(System.Object
) which casting to another type involves boxing that has a slight performance overhead.
It's negligible to call it once or occasionally, but if this particular command is going to be called intensively, then it's recommended to avoid boxing by getting the value directly instead.
...
public class MyCommand : Command, ICommandAutoDraw
{
...
public override void Execute(CommandExecutionFlow _flow)
{
string myStringValue = IsInputConnected(0) ? GetConnectedInputValue<string>(_flow, 0) : myString;
string pureInPointStringValue = IsInputConnected(1) ? GetConnectedInputValue<string>(_flow, 1) : pureInPointString;
int integerValue = IsInputConnected(2) ? GetConnectedInputValue<int>(_flow, 2) : GetIntegerValue();
Debug.Log("Values: " + myStringValue + ", " + pureInPointStringValue + ", " + integerValue);
Exit();
}
}
Property Out-Points
We can use [PropertyOutPoint(outputIndex)]
attribute on top of a field, method, or property declaration to create an out-point.
We will talk about Main Out-Points in the next section, but it's important to know that both Property Out-Points and Main Out-Points share the same output index order. So, for a non-property command (A command class not derived from PropertyCommand
), the output at index 0
is reserved as the default exit point.
...
public class MyCommand : Command, ICommandAutoDraw
{
...
[PropertyOutPoint(1)]
private float floatOutput = 20f;
[PropertyOutPoint(2)]
private string stringOutput { get { return "def"; } }
[PropertyOutPoint(3)]
private Vector3 GetVectorOutput() { return new Vector3(5, 5, 5); }
...
}
Just to make the concept clearer, getting an input is essentially about getting an output of another command. The difference between in-point and out-point is that in-point is retrieved by the owning command itself that calls the GetInput
, while out-point is retrieved from another command that refers to that out-point. In the background, what GetInput
does actually includes calling the GetOutput
method of the connected command.
Optimizing Output
By default, just like what we've learned from in-point previously, retrieving output simply works out of the box due to the ICommandAutoDraw
and [PropertyOutPoint(outputIndex)]
being used. The retrieved value's type is object
(System.Object
) which casting to another type involves boxing that has a slight performance overhead.
It's negligible to retrieve it once or occasionally, but if the output of this particular command is going to be retrieved intensively, then it's recommended to avoid boxing by adding the generic type parameters of the output types in our Command
declaration, and overriding the GetOutput
method.
...
public class MyCommand : Command<float, string, Vector3>, ICommandAutoDraw
{
...
[PropertyOutPoint(1)]
private float floatOutput = 20f;
[PropertyOutPoint(2)]
private string stringOutput { get { return "def"; } }
[PropertyOutPoint(3)]
private Vector3 GetVectorOutput() { return new Vector3(5, 5, 5); }
public override OneOf<float, string, Vector3> GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch (_pointIndex) {
case 1:
return floatOutput;
case 2:
return stringOutput;
case 3:
return GetVectorOutput();
default:
return default;
}
}
...
}
Main Out-Points
We add main out-points using the [MainOutPoint(index, label)]
attribute.
To remind you, a main (white) point is a point that is not about passing a value, but it is what defines the sequence of the command execution flow.
With that in mind, it made more sense now that the attribute doesn't need a field, property, or a method that returns certain value. Instead, the attribute can only be added in specific overriden methods, depending whether the main out-point is intended for an Alternative Exit, or for Parallel Flow.
Exit Out-Points
To create an exit out-point, we override the GetNextOutputIndex
method, and we add the [MainOutPoint(index, label)]
attribute on top of that method. We can add more than one attributes if we want to add multiple exit out-points.
As explained previously, keep in mind that property out-points and main out-points share the same index order.
GetNextOutputIndex
is the method that tells which command to go next when the Exit
method is called. Just to remind you, that a command can only exit through 1 main out-point only.
Below is an example where the command will randomly choose to exit through either index 0, 4, or 5.
...
public class MyCommand : Command<float, string, Vector3>, ICommandAutoDraw
{
...
private int randomExitIndex;
[MainOutPoint(4, "Alternative Exit 1")]
[MainOutPoint(5, "Alternative Exit 2")]
public override int GetNextOutputIndex()
{
return randomExitIndex;
}
public override void Execute(CommandExecutionFlow _flow)
{
...
// Exit randomly either to index 0, 4, or 5
int random = Random.Range(0, 3);
if (random == 0) randomExitIndex = 0;
else if (random == 1) randomExitIndex = 4;
else randomExitIndex = 5;
Exit();
}
...
}
Parallel Out-Points
Again, as explained from the previous chapter, we can technically make our command exit through multiple main out-points using parallel flow.
To create a parallel out-point, we override the Execute
method, and we add the [MainOutPoint(index, label)]
attribute on top of that method. We can add more than one attributes if we want to add multiple parallel out-points.
Notice that both exit and parallel out-points uses the same [MainOutPoint(index, label)]
attribute. To make it clear, the main in-point type is distinguised whether it's being put on top of GetNextOutputIndex
or Execute
method. It is decided that way to make it more intuitive because GetNextOutputIndex
is the place where we decide the output index of the Exit
method.
...
public class MyCommand : Command<float, string, Vector3>, ICommandAutoDraw
{
...
[MainOutPoint(6, "Parallel Example")]
[MainOutPoint(7, "Parallel Wait for 5s")]
public override void Execute(CommandExecutionFlow _flow)
{
...
RunSubFlow(6);
WaitFor5Seconds();
...
}
private async void WaitFor5Seconds()
{
await Task.Delay(5000);
RunSubFlow(7);
}
...
}
Compared to the manual way as explained in Alternative Exit and Parallel Flow chapter, we don't need to manually create a SubFlowInfo
as it has been automatically handled due to ICommandAutoDraw
and [MainOutPoint(index, label)]
attribute being used. So, all we need to do is to call RunSubFlow
method.
Alternatively, we can also pass a label instead of output index to the RunSubFlow
method. This also implies that main out-point's label should be unique. It's up to your preference whether to use output index or label to pass to the RunSubFlow
method.
...
public class MyCommand : Command<float, string, Vector3>, ICommandAutoDraw
{
...
[MainOutPoint(6, "Parallel Example")]
[MainOutPoint(7, "Parallel Wait for 5s")]
public override void Execute(CommandExecutionFlow _flow)
{
...
RunSubFlow("Parallel Example");
WaitFor5Seconds();
...
}
private async void WaitFor5Seconds()
{
await Task.Delay(5000);
RunSubFlow("Parallel Wait for 5s");
}
...
}
Full Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;
using OneOf;
using System.Threading.Tasks;
[System.Serializable]
[RegisterCommand("Custom/My Command", typeof(SequineFlowCommandData))]
public class MyCommand : Command<float, string, Vector3>, ICommandAutoDraw
{
public override float nodeWidth => 275f;
[System.Serializable]
public class ClassA
{
public int myInt;
public float myFloat;
public ClassB bInstance;
}
[System.Serializable]
public class ClassB
{
public bool myBool;
public Vector2 myVector;
}
[PropertyInPoint(0)]
public string myString;
[PropertyInPoint(1)]
public string pureInPointString { get { return "abc"; } }
[PropertyInPoint(2)]
public int GetIntegerValue() { return 5; }
public ClassA aInstance;
public List<ClassB> bArray;
[PropertyOutPoint(1)]
private float floatOutput = 20f;
[PropertyOutPoint(2)]
private string stringOutput { get { return "def"; } }
[PropertyOutPoint(3)]
private Vector3 GetVectorOutput() { return new Vector3(5, 5, 5); }
public override OneOf<float, string, Vector3> GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch (_pointIndex) {
case 1:
return floatOutput;
case 2:
return stringOutput;
case 3:
return GetVectorOutput();
default:
return default;
}
}
private int randomExitIndex;
[MainOutPoint(4, "Alternative Exit 1")]
[MainOutPoint(5, "Alternative Exit 2")]
public override int GetNextOutputIndex()
{
return randomExitIndex;
}
[MainOutPoint(6, "Parallel Example")]
[MainOutPoint(7, "Parallel Wait for 5s")]
public override void Execute(CommandExecutionFlow _flow)
{
string myStringValueBoxed = GetInput<string>(_flow, 0);
string pureInPointStringValueBoxed = GetInput<string>(_flow, 1);
int integerValueBoxed = GetInput<int>(_flow, 2);
Debug.Log("Values Boxed: " + myStringValueBoxed + ", " + pureInPointStringValueBoxed + ", " + integerValueBoxed);
string myStringValue = IsInputConnected(0) ? GetConnectedInputValue<string>(_flow, 0) : myString;
string pureInPointStringValue = IsInputConnected(1) ? GetConnectedInputValue<string>(_flow, 1) : pureInPointString;
int integerValue = IsInputConnected(2) ? GetConnectedInputValue<int>(_flow, 2) : GetIntegerValue();
Debug.Log("Values: " + myStringValue + ", " + pureInPointStringValue + ", " + integerValue);
RunSubFlow("Parallel Example");
WaitFor5Seconds();
// Exit randomly either to index 0, 4, or 5
int random = Random.Range(0, 3);
if (random == 0) randomExitIndex = 0;
else if (random == 1) randomExitIndex = 4;
else randomExitIndex = 5;
Exit();
}
private async void WaitFor5Seconds()
{
await Task.Delay(5000);
RunSubFlow("Parallel Wait for 5s");
}
}