Custom Command Type
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).
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 usingpublic
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.
- pathName: The displayed name of the command node. You can categorize the commands by forming a path using
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.
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.
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.
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.
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.
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.
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.
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 theExecute
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.
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.");
}
}
...
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).
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.
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.
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 insideEditor_InitOutPoints
. - If we have
inputIds
for the reference data container of the inputs, then we also havenextIds
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 haveCommandGUI.AddPropertyOutPoint
to add an out-point. - If we have
CommandGUI.DrawInPoint
to draw the in-point, then we also haveCommandGUI.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
}
Full Codes
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
}
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.
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.
...
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.
...
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();
}
...