Skip to main content

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;

...
}

String Field

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;
...
}

Nested Field

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;
...
}

Array Field

tip

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;
...
}

Property In-Point

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; }

...
}

Pure Property In-Point

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); }
...
}

Property Out-Point

info

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();
}
...
}

Exit Out-Point

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.

info

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");
}
...
}

Parallel Out-Point

Full Code

MyCommand.cs
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");
}

}