Auto Draw Command
Auto Draw Command 是一种其编辑器节点会被自动绘制的 Command,包括字段、in-point 和 out-point。效果就像我们在 Inspector 中使用 MonoBehaviour 或 ScriptableObject 时一样。
如果想把自定义的 Command 声明为 Auto Draw Command,需要在类声明中实现 ICommandAutoDraw 接口。从 1.6.0 版本开始,当你通过 Assets 菜单选择 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();
}
}
字段
任何 public 字段,或使用 [SerializeField] 声明的字段,都会被自动绘制。
...
public class MyCommand : Command, ICommandAutoDraw
{
public string myString;
...
}
这同样适用于嵌套的可序列化类。
...
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;
...
}

实验性功能
数组字段也可以被自动绘制,不过这仍然属于实验性功能。它实际上并不支持数组(System.Array),而是需要使用 List。此外,还存在一些已知问题和限制:
- 无法撤销元素的重新排序
- 多层嵌套字段(类中再包含类)虽然可以被正确绘制,但修改的值不会生效
- 目前不支持嵌套数组
public class MyCommand : Command, ICommandAutoDraw
{
...
public List<ClassB> bArray;
...
}

或者,如果需要向 Command 传递数组,我们也可以创建一个包含该数组的组件(MonoBehaviour)或资源(ScriptableObject),然后把这个组件或资源传递给 Command。也就是说,在 Command 类中,不再声明数组字段,而是改为使用组件或资源字段。
Property In-Points
我们不一定只能从字段本身获取数值,也可以把它声明为一个 in-point,这样数值就可以从其他 Command Node 的 in-point 传入。实现方式是在字段声明上方添加 [PropertyInPoint(inputIndex)] 特性。
当 in-point 被连接时,数值输入框会消失,这表示原本字段中的值会被忽略,改为使用来自已连接 Command Node 的动态值。
...
public class MyCommand : Command, ICommandAutoDraw
{
..
[PropertyInPoint(0)]
public string myString;
...
}

不仅可以把 [PropertyInPoint(inputIndex)] 特性用于字段,我们也可以把它加在方法或属性的声明上。由于方法和属性本身不可序列化,并且拥有自己的取值逻辑,因此这个 in-point 会变成一个纯粹的 in-point,而不会再绘制数值输入框。
...
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; }
...
}

需要注意,in-point 的绘制顺序不一定按照索引顺序排列,而是按照字段在类中的声明顺序来绘制。 不过如果是方法或属性,那么它们会优先于字段被绘制,也就是会排在这些带输入框的 in-point 之前。
在运行时使用 Property In-Point
在运行时,如果需要获取 in-point 的值,我们可以使用 GetInput 方法,并传入 _flow 与 _index 参数,其中 _index 是该 in-point 的索引。这样会从已连接的 Command 中获取数值;如果没有连接,则默认会从对应的字段、方法或属性中取得该值。
...
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();
}
}
输入优化
默认情况下,由于使用了 ICommandAutoDraw 和 [PropertyInPoint(inputIndex)],GetInput 可以直接开箱即用。获取到的值类型是 object(System.Object),而将它转换为其他类型会涉及 boxing,这会带来轻微的性能开销。
如果只是偶尔调用一次,这种开销可以忽略。但如果该 Command 会被高频调用,那么建议通过直接获取值的方式来避免 boxing。
...
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
我们可以在字段、方法或属性的声明上方添加 [PropertyOutPoint(outputIndex)] 特性来创建一个 out-point。
关于 Main Out-Points 我们会在下一节介绍,不过这里有一点非常重要:Property Out-Points 和 Main Out-Points 共用同一套输出索引顺序。因此,对于非 property 的 Command,也就是没有继承自 PropertyCommand 的类,索引为 0 的输出会被保留为默认的退出点。
...
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); }
...
}

为了让概念更清晰,获取输入本质上就是获取另一个 Command 的输出。in-point 与 out-point 的区别在于:in-point 是由 所属的 Command 本身 通过调用 GetInput 来获取的,而 out-point 则是由引用该 out-point 的 其他 Command 来获取。在底层实现中,GetInput 实际上会调用被连接 Command 的 GetOutput 方法。
输出优化
默认情况下,和我们之前在 in-point 中了解到的一样,由于使用了 ICommandAutoDraw 和 [PropertyOutPoint(outputIndex)],获取输出可以直接使用。得到的值类型是 object(System.Object),在转换为其他类型时会涉及 boxing,因此会带来轻微的性能开销。
如果只是偶尔获取一次,这个开销几乎可以忽略。但如果该 Command 的输出会被高频读取,那么建议在 Command 的声明中加入输出类型的泛型参数,并重写 GetOutput 方法,以避免 boxing。
...
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
我们通过使用 [MainOutPoint(index, label)] 特性来添加 main out-point。
再次提醒,main 白色端口并不是用来传递数值的,它决定的是 Command 执行流程的顺序。
理解这一点后就会明白,这个特性并不需要依附在某个字段、属性或返回特定值的方法上。相反,它只能被添加在特定的被重写方法上,具体取决于这个 main out-point 是用于 替代出口,还是用于 并行流程。
出口 Out-Points
要创建一个出口 out-point,我们需要重写 GetNextOutputIndex 方法,并在该方法上方添加 [MainOutPoint(index, label)] 特性。如果需要多个出口,可以添加多个特性。
正如之前解释过的,要记住 property out-point 和 main out-point 共享同一套索引顺序。
GetNextOutputIndex 方法用于在调用 Exit 时决定接下来要执行哪个 Command。再提醒一次,一个 Command 只能通过 一个 main out-point 退出。
下面是一个示例,其中 Command 会在索引 0、4 或 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();
}
...
}

并行 Out-Points
再次回顾前一章的内容,从技术上来说,我们可以通过并行流程让一个 Command 同时从多个 main out-point 退出。
要创建并行的 out-point,需要重写 Execute 方法,并在该方法上方添加 [MainOutPoint(index, label)] 特性。如果需要多个并行出口,可以添加多个特性。
请注意,exit 和 parallel out-point 使用的是同一个 [MainOutPoint(index, label)] 特性。区分方式在于它被标注在 GetNextOutputIndex 还是 Execute 方法之上。这样设计是为了更直观,因为 GetNextOutputIndex 本身就是用于决定 Exit 时所采用的输出索引的位置。
...
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);
}
...
}
与在 替代出口与并行流程 章节中介绍的手动方式相比,我们不再需要手动创建 SubFlowInfo。由于使用了 ICommandAutoDraw 和 [MainOutPoint(index, label)] 特性,这一步已经被自动处理好了。因此,我们只需要调用 RunSubFlow 方法即可。
另外,我们也可以在调用 RunSubFlow 时传入 label,而不是输出索引。这也意味着 main out-point 的 label 应当是唯一的。至于传入索引还是 label,可以根据个人偏好来决定。
...
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");
}
...
}

完整代码
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");
}
}