Skip to main content

Custom Command Type

info

As of version 1.6.0, ICommandAutoDraw interface is introduced, which scraps all the necessary boilerplates to manually draw a command. It will be explained in Auto Draw Command. However, we recommend you to keep reading through this page to get to know better on how a command works.

We can create a new command type, both for Main Command and Property Command. To make this easier to understand, let's actually learn by example.

This will guide you in making a Main Command that prints a string using Debug.Log, that receives a string as the input to be printed.

Then we will also make a Property Command that outputs a string which then used as the input of the Main Command.

1. Main Command

Creating a New Command Script

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

New Command Script

Open the script, and this is how the script looks like by default.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;

[System.Serializable]
[RegisterCommand("Custom/PrintCommand", typeof(SequineFlowCommandData))]
public class PrintCommand : Command
{
public override void Execute(CommandExecutionFlow _flow)
{
//Your logic here... (Before Exit Method)
Exit();
}
}
  • [System.Serializable] is needed in order our command to have serializable fields by using public fields or [SerializeField] attribute.
  • [RegisterCommand] is an attribute that registers our command, so that it appears when we open the Create Command context menu (by right clicking).
    • pathName: The displayed name of the command node. You can categorize the commands by forming a path using /.
    • commandDataTypes: The command data scope which we want this command to be used in. This will always be SequineFlowCommandData, unless you want to learn something more advanced by going low-level.
  • Command is the base class of every Main Command.
  • Execute method is called when the main connection (the white connection) visits this command.

Back to Unity Editor, and once everything's compiled, we'll see that our newly created command is now included in the Create Command context menu.

Create Command Context Menu

Command Created

Currently, the command will do nothing when it's executed, and we'll learn how to make it do something.

Defining the Execute Method

We can then define what the command needs to do inside the Execute method. Keep in mind that we need to call Exit method somewhere (ideally in the end of the process) to end the command and proceed to the next command. Calling base.Execute(_flow) is also valid since it calls Exit method as well.

danger

Make sure not to call Exit twice. For example, if we've already used base.Execute(_flow), then we don't have to call Exit anymore.

Since we need our command to print a string, let's actually make a string field, and print it inside the Execute method.

...
[System.Serializable]
[RegisterCommand("Custom/PrintCommand", typeof(SequineFlowCommandData))]
public class PrintCommand : Command
{
public override void Execute(CommandExecutionFlow _flow)
{
string textToPrint = "Hello World!";
Debug.Log(textToPrint);
Exit();
}
}

Let's get into Play Mode and execute our Sequine Flow. Upon execution, we will see that it prints our Hello World! string.

Command Print

Drawing a Field

Right now, it's not flexible enough to use since we have to hard-code the string. Let's say we want to edit the text from the command node, then we need to draw a TextField.

Remember that our class has [System.Serializable] attribute. It means we can use public field or [SerializeField] so that our field is serialized and editable through the editor. So now, instead of declaring the textToPrint inside the Execute method, let's put it outside as public field.

...
public class PrintCommand : Command
{
public string textToPrint = "Hello World!";

public override void Execute(CommandExecutionFlow _flow)
{
Debug.Log(textToPrint);
Exit();
}
}

Currently, the textToPrint field is serialized, but will not be shown inside the command node. To draw the field, we use CommandGUI, which is a utility class that can draw various types of field. You can check the Scripting API to see all available drawer methods. But for now, we'll just use DrawTextField method to draw a single line string field.

We can't just call CommandGUI.DrawTextField anywhere. We can only draw it inside the Editor_OnDrawContents method which we need to override.

Any kind of method which name starts with Editor_ can only be called in Editor, not in Build version of the game. Upon building your game, you will get an error if your script contain those methods. Hence, we have to put any method which starts with Editor_ inside a #if UNITY_EDITOR prepocessor directive. Any script contained inside the #if UNITY_EDITOR will be scrapped in the build version. You can read more about Unity's Conditional Compilation if you need to know more about it.

Here is how we should override our Editor_OnDrawContents method.

...
public class PrintCommand : Command
{
public string textToPrint = "Hello World!";

public override void Execute(CommandExecutionFlow _flow)
{
Debug.Log(textToPrint);
Exit();
}

#if UNITY_EDITOR
public override void Editor_OnDrawContents(Vector2 _absPosition)
{

}
#endif

}

Now we can finally write our drawing methods inside the Editor_OnDrawContents method.

...

#if UNITY_EDITOR
public override void Editor_OnDrawContents(Vector2 _absPosition) {
CommandGUI.DrawTextField(
"Text to Print",
"The text to print using this command.",
ref textToPrint);
}
#endif
...

Here are the parameters of CommandGUI's draw methods in general:

  • label: The label to be drawn inside the command node.
  • tooltip: The tooltip which is shown when we hover the field.
  • target: The field target which we want to modify. Note that it requires ref keyword so that it could directly modify the value since it will be passed by reference instead of passed by value.

Now let's get back to Unity and see how our command looks like.

Draw Text Field

As you can see, we can't comfortably read the label since there's not enough space for the label.

We can override the nodeWidth property to modify the width of the command node. Let's try to increase it to 200.

...
public class PrintCommand : Command
{
public override float nodeWidth => 200;

public string textToPrint = "Hello World!";

public override void Execute(CommandExecutionFlow _flow)
{
Debug.Log(textToPrint);
Exit();
}
...
}

Let's get back to Unity, and we'll see that we can now comfortably see the field. Let's try to modify the value, and play it.

Play Text Field

Drawing an In-Point as Input

We've managed to make our textToPrint field editable through the command node. But what if we want to actually assign a dynamic value instead of writing a constant string value?

In that case, we need to add an in-point and draw it inside the Editor_OnDrawContents as well.

But before we draw it, we need to ensure that we've added the input ports first. We can add an input port by modifying the inputIds property. inputIds contains a list of connection targets, which stores which Command Id each input refers to.

In this case, since we need 1 input for the textToPrint, then we need to ensure that inputIds Count is 1.

We have to add it inside the Editor_InitInPoints method, which we can override as well.

#if UNITY_EDITOR
public override void Editor_InitInPoints()
{
base.Editor_InitInPoints();
if (inputIds.Count < 1) inputIds.Add(new ConnectionTarget());
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawTextField(
"Text to Print",
"The text to print using this command.",
ref textToPrint);
}
#endif

After ensuring the inputIds, we also need to add an in-point. If you get confused over the difference, just keep in mind that inputIds is the actual data that contains the informations for the connection targets, while an in-point is the drawer, which only responsible to draw the clickable input point.

Using CommandGUI, we can add a main in-point using the AddMainInPoint method, while we can also add a property in-point using the AddPropertyInPoint method.

For now, since we need an in-point to draw a string input, we will use the AddPropertyInPoint method, specifying string as the type parameter.

...
public override void Editor_InitInPoints()
{
base.Editor_InitInPoints();
if (inputIds.Count < 1) inputIds.Add(new ConnectionTarget());
CommandGUI.AddPropertyInPoint<string>();
}
...

However, we're not done only by adding the in-point. We should also draw it using the DrawInPoint method, specifying the in-point index as the parameter.

#if UNITY_EDITOR
public override void Editor_InitInPoints()
{
base.Editor_InitInPoints();
if (inputIds.Count < 1) inputIds.Add(new ConnectionTarget());
CommandGUI.AddPropertyInPoint<string>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawInPoint(1);
CommandGUI.DrawTextField(
"Text to Print",
"The text to print using this command.",
ref textToPrint);
}
#endif

By now, you might get confused on why we put 1 as the parameter for DrawInPoint instead of 0. That is because one main in-point is actually added first when we call base.Editor_InitInPoints(). Remember that we somehow has a white in-point being there in our command in the first place, and that's where it came from.

Therefore, in this case:

  • The in-point at index 0 is the main in-point
  • The in-point at index 1 is the string property in-point.

Draw In Point

Using the Input from In-Point

We can use a String Formatter command to create a string and try to use it as our input.

When we try to play it, notice that it turns out ignoring the input, and insisted to print the text from the Text Field.

String Formatter

That's because we currently haven't utilize the input yet.

To use the value from the input, we need to call the GetInput method. It should be called at the same frame when the Execute method is called, because if it's called asynchronously in a different frame later, the state of the _flow may be changed and no longer relevant.

Let's get back to our Execute method, and modify it.

...
public override void Execute(CommandExecutionFlow _flow)
{
string value_textToPrint = GetInput<string>(_flow, 0, textToPrint);
Debug.Log(value_textToPrint);
Exit();
}
...

GetInput method returns a generic object type, which is why it requires a type parameter to cast the value (in this case,string). Here are the parameters:

  • flow: The command execution flow context. We can simply pass the _flow passed by the Execute method. We will cover about flow later in Alternative Exit and Parallel Flow.
  • index: The input index of the inputIds.
  • defaultValue: The default value that will be used if the in-point is disconnected.

Here is how it translated to human language: Get an input value from in-point at index 0, and if the input is empty, get the value from the textToPrint field instead.

We can try to play it again, and now it should print the value from the input whenever it's available.

Print The Input

Polishing the Looks

Functionality-wise, our command is currently working properly. But one thing that might bother users, is that it's quite ambiguous to look at which value is actually going to be printed. Both the value from the String Formatter and the value from the textToPrint field are shown.

We can make it look more intuitive by hiding the textToPrint field when the input is connected.

We have to check whether the inputIds at index 0 is connected or not by looking at the targetId value. targetId with value 0 means it's not referencing a commmand (null), therefore, disconnected. When the targetId is 0 (disconnected), we draw the Text Field. Otherwise, we will just draw the label.

...
public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawInPoint(1);
if (inputIds[0].targetId == 0)
{
CommandGUI.DrawTextField(
"Text to Print",
"The text to print using this command.",
ref textToPrint);
} else
{
CommandGUI.DrawLabel(
"Text to Print",
"The text to print using this command.");
}
}
...

Polishing Print Command

Additionally, if you are familiar with Unity Editor IMGUI programming, you can also draw anything you want inside the Editor_OnDrawContents method.

2. Property Command

Now that we already have PrintCommand which is a Main Command that prints a string, let's also create a Property Command called MyTextsCommand which contains several string outputs that can be used as an input for our PrintCommand.

To remind you that Property Command is a command that has no main/white in-points and out-points at all.

Previously, we used String Formatter command as the input. But it has already been provided by Sequine, and we won't learn anything about Property Command without actually making one.

Creating a New Property Command Script

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

New Property Command Script

Open the script, and this is how the script looks like by default.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;

[System.Serializable]
[RegisterCommand("Custom/MyTextsCommand", typeof(SequineFlowCommandData))]
public class MyTextsCommand : PropertyCommand<float>
{
public override float GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch(_pointIndex)
{
case 0:
float valueToReturn = 1f;
//Your logic here...
return valueToReturn;
default:
return 0f;
}
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
if (nextIds.Count < 1) nextIds.Add(new List<ConnectionTarget>());
CommandGUI.AddPropertyOutPoint<float>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawOutPoint(0);
CommandGUI.DrawLabel("Out", true);
}
#endif
}
  • PropertyCommand is the base class of every Property Command. This will also automatically put this command under the Property category in Create Command context menu.
  • GetOutput method is called when another command accesssed one of the out-point of this command.

For the implementation, it mainly uses GetOutput method. Property Command doesn't need to get executed, hence, doesn't need to be exited as well.

Create Property Command

Currently, the command outputs a float, which we can't connect as the input of PrintCommand. We'll learn how to make it outputs some strings.

Defining the GetOutput Method

We can define what value we get upon getting the output inside the GetOutput method.

Since we need to output a string, let's make a string variable, and return it as the output. We should also change the generic type parameter of the PropertyCommand from <float> to string. By changing the type parameter, with should also change the return type of the GetOutput method with the same type.

By now, you should have been familiar with Editor_InitOutPoints method, and we need to change the CommandGUI.AddPropertyOutPoint method's type parameter from float to string, so that the out-point is drawn as string point (pink-colored), instead of drawn as float point (light-blue-colored).

We can draw a label using CommandGUI.DrawLabel method, and pass true as the second parameter if we want it to align-right.

...
public class MyTextsCommand : PropertyCommand<string>
{
public override string GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch(_pointIndex)
{
case 0:
string text1 = "I am Text 1!";
return text1;
default:
return null;
}
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
if (nextIds.Count < 1) nextIds.Add(new List<ConnectionTarget>());
CommandGUI.AddPropertyOutPoint<string>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawOutPoint(0);
CommandGUI.DrawLabel("Out", true);
}
#endif
}

After compilation, try to connect the output of MyTextsCommand to PrintCommand.

Let's get into Play Mode and execute our Sequine Flow. Upon execution, we will see that it prints our I am Text 1! string.

Get Property Command

Adding More Out-Points

Let's try to add one more out-point, so that we can choose which output we want to use.

  • We knew previously that we have to prepare the inputs inside Editor_InitInPoints. So, for outputs, we have to prepare them inside Editor_InitOutPoints.
  • If we have inputIds for the reference data container of the inputs, then we also have nextIds for the reference data container of the outputs (or next commands for main out-points).
  • If we have CommandGUI.AddPropertyInPoint to add an in-point, then we also have CommandGUI.AddPropertyOutPoint to add an out-point.
  • If we have CommandGUI.DrawInPoint to draw the in-point, then we also have CommandGUI.DrawOutPoint to draw the out-point.

Let's have 2 outputs where the first one outputs I am Text 1!, while the second one outputs I am Text 2!.

To define which value to output at certain out-point index, we simply need to define it based on the pointIndex parameter which passed from the GetOutput method.

We can then use conditional statement to select which value, or even use certain collections (array, list, or dictionary) if you want a faster/performant approach.

For now, we will use a simple switch-case method where we return text1 if the pointIndex is 0, and return text2 if the pointIndex is 1.

...
public class MyTextsCommand : PropertyCommand<string>
{
public override string GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch(_pointIndex)
{
case 0:
string text1 = "I am Text 1!";
return text1;
case 1:
string text2 = "I am Text 2!";
return text2;
default:
return null;
}
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
if (nextIds.Count < 1) nextIds.Add(new List<ConnectionTarget>()); //This is for text1
CommandGUI.AddPropertyOutPoint<string>();

if (nextIds.Count < 2) nextIds.Add(new List<ConnectionTarget>()); //This is for text2
CommandGUI.AddPropertyOutPoint<string>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawOutPoint(0);
CommandGUI.DrawLabel("Text1", true);
CommandGUI.DrawOutPoint(1);
CommandGUI.DrawLabel("Text2", true);
}
#endif
}

Multiple Out-Points

Full Codes

PrintCommand.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;

[System.Serializable]
[RegisterCommand("Custom/PrintCommand", typeof(SequineFlowCommandData))]
public class PrintCommand : Command
{
public override float nodeWidth => 200;

public string textToPrint = "Hello World!";

public override void Execute(CommandExecutionFlow _flow)
{
string value_textToPrint = GetInput<string>(_flow, 0, textToPrint);
Debug.Log(value_textToPrint);
Exit();
}

#if UNITY_EDITOR
public override void Editor_InitInPoints()
{
base.Editor_InitInPoints();
if (inputIds.Count < 1) inputIds.Add(new ConnectionTarget());
CommandGUI.AddPropertyInPoint<string>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawInPoint(1);
if (inputIds[0].targetId == 0) {
CommandGUI.DrawTextField(
"Text to Print",
"The text to print using this command.",
ref textToPrint);
}
else {
CommandGUI.DrawLabel(
"Text to Print",
"The text to print using this command.");
}
}
#endif

}
MyTextsCommand.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Calcatz.Sequine;
using Calcatz.CookieCutter;

[System.Serializable]
[RegisterCommand("Custom/MyTextsCommand", typeof(SequineFlowCommandData))]
public class MyTextsCommand : PropertyCommand<string>
{
public override string GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch(_pointIndex)
{
case 0:
string text1 = "I am Text 1!";
return text1;
case 1:
string text2 = "I am Text 2!";
return text2;
default:
return null;
}
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
if (nextIds.Count < 1) nextIds.Add(new List<ConnectionTarget>()); //This is for text1
CommandGUI.AddPropertyOutPoint<string>();

if (nextIds.Count < 2) nextIds.Add(new List<ConnectionTarget>()); //This is for text2
CommandGUI.AddPropertyOutPoint<string>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawOutPoint(0);
CommandGUI.DrawLabel("Text1", true);
CommandGUI.DrawOutPoint(1);
CommandGUI.DrawLabel("Text2", true);
}
#endif
}

Main Command vs Property Command

Although Command seems to be strictly divided into 2 types, in fact, those 2 types are only semantically distinguished to differentiate their purpose.

Technically it's also possible for a Main Command to have property out-points and uses GetOutput method. While it's also possible for Property Command to have main out-points and uses Execute method.

Also, just like PropertyCommand, we can also use generic parameter types <T...> in Command to define the output type of Command's GetOutput method.

In fact, here's the actual source code of PropertyCommand which barely change anything from Command class, and mainly just getting rid off the base methods in order to remove the main in-point and main out-point which was supposed to be added by default.

PropertyCommand.cs
namespace Calcatz.CookieCutter {

[System.Serializable]
public abstract class PropertyCommand : Command {

#if UNITY_EDITOR
public override void Editor_InitInPoints() {

}
public override void Editor_InitOutPoints() {

}
public override void Editor_OnDrawTitle(out string _tooltip) {
_tooltip = null;
}
#endif

}
}

Command Output Types

We previously learned that we can define the output type of a command with generic parameter types that can be passed in either Command or PropertyCommand.

Output with Unspecified Type

But let's see what happened if we don't pass any generic parameter type. In that case, GetOutput will not be overridable, and instead we can override the GetOutputBoxed.

        public override object GetOutputBoxed(CommandExecutionFlow _flow, int _pointIndex) { 
switch(_pointIndex) {
case 0:
return (T)(object)"My returned string";
default:
return null;
}
}

As the method name suggests, it involves boxing, which is a conversion to object type that leads to memory allocation and may cause a slight performance overhead. This is no longer encouraged to use since version 1.5.0 as the new strong typed GetOutput is faster.

Output with Multiple Types

In our previous example, we made a property command that outputs a string by deriving the command from PropertyCommand<string>. But, what if the command should output other types, such as boolean or float? In that case, we can actually add more than one generic parameter types. Let's implement that in our existing MyTextsCommand script.

The only difference in the implementation is that in the GetOutput method, the return type would be OneOf, with the same generic parameter types specified in class' declaration.

MyTextsCommand.cs
...
public class MyTextsCommand : PropertyCommand<string, float, bool>
{
public override OneOf.OneOf<string, float, bool> GetOutput(CommandExecutionFlow _flow, int _pointIndex)
{
switch(_pointIndex)
{
case 0:
string text1 = "I am Text 1!";
return text1;
case 1:
string text2 = "I am Text 2!";
return text2;
case 2:
return true;
case 3:
return 50f;
default:
return 0f;
}
}

#if UNITY_EDITOR
public override void Editor_InitOutPoints()
{
if (nextIds.Count < 1) nextIds.Add(new List<ConnectionTarget>()); //This is for text1 (pointIndex 0)
CommandGUI.AddPropertyOutPoint<string>();

if (nextIds.Count < 2) nextIds.Add(new List<ConnectionTarget>()); //This is for text2 (pointIndex 1)
CommandGUI.AddPropertyOutPoint<string>();

if (nextIds.Count < 3) nextIds.Add(new List<ConnectionTarget>()); //This is for pointIndex 2
CommandGUI.AddPropertyOutPoint<bool>();

if (nextIds.Count < 4) nextIds.Add(new List<ConnectionTarget>()); //This is for pointIndex 3
CommandGUI.AddPropertyOutPoint<float>();
}

public override void Editor_OnDrawContents(Vector2 _absPosition)
{
CommandGUI.DrawOutPoint(0);
CommandGUI.DrawLabel("Text1", true);
CommandGUI.DrawOutPoint(1);
CommandGUI.DrawLabel("Text2", true);
CommandGUI.DrawOutPoint(2);
CommandGUI.DrawLabel("Boolean Value", true);
CommandGUI.DrawOutPoint(3);
CommandGUI.DrawLabel("Float Value", true);
}
#endif
}

Ideally, upon using GetInput, we should also pass the OneOf type to get the value (GetInput<OneOf.OneOf<string, float, bool>>). This is the fastest way in term of performance which doesn't allocate memory.

We can still use GetInput with only one of the types such as GetInput<string>, but the value will be retrieved using a cached Reflection which will add a slight performance overhead.

PrintCommand.cs
...
public class PrintCommand : Command
{
...
public override void Execute(CommandExecutionFlow _flow)
{
string value_textToPrint = GetInput<OneOf.OneOf<string, float, bool>>(_flow, 0, textToPrint).AsT0;
Debug.Log(value_textToPrint);
Exit();
}
...