Ports & Triggers
Ports and triggers belong to entities and are arguably the most important part of Warudo scripting. They are used to pass data between entities, trigger actions, and provide user interaction in the editor.
Data Input Ports
Data input ports are used to provide data to an entity by either the user (using the editor) or another entity. Data inputs can be of various types, such as strings, numbers, booleans, or even complex types like structured data or arrays.
A data input is defined as a public field in an entity subclass, decorated with the [DataInput]
attribute. In the Getting Started example, we saw a DataInput
that defines a number slider:
[DataInput]
[IntegerSlider(1, 100)]
public int LuckyNumber = 42;
Here are a few more examples:
- String Input:
[DataInput]
public string MyName = "Alice"; - Enum Input: (Shown as a dropdown in the editor)
public enum Color {
Red,
Green,
Blue
}
[DataInput]
public Color MyColor = Color.Red; - Array Input: (Shown as an editable list in the editor)
[DataInput]
public float[] MyFavoriteNumbers = new float[] { 3.14f, 2.718f };
This is how they look in the editor (we use a node here, but these data inputs work the same way in assets and plugins):
Note how they are initialized with the default values we specified in the code. These default values will be assigned to the data input again when the user clicks the "Reset" button next to the data input label.
A data input typically has a serializable type ("serializable" here means that the data value can be saved and restored when Warudo is closed and reopened). The following types are serializable by default:
- Primitive types:
int
,float
,bool
,string
, any Enum type - Unity types:
Vector2
,Vector3
,Vector4
,Color
- Structured data types
- Asset references
- Arrays of serializable types
For nodes, it is possible to define non-serializable data inputs that cannot be edited in the editor but instead processed by the node itself. For example, the following code calls the ToString()
method on the generic object
data input:
[NodeType(Id = "dc28819e-5149-4573-945e-40e81e2874c4", Title = "ToString()", Category = "CATEGORY_ARITHMETIC")]
public class ToStringNode : Node {
[DataInput]
public object A; // Not serialized
[DataOutput]
[Label("OUTPUT_STRING")]
public string Result() {
return A?.ToString();
}
}
Other common data input types that are not serializable but can be passed between nodes include Dictionary
, GameObject
and Quaternion
.
Attributes
Data input ports can be annotated with attributes to provide additional context to the editor. For example, the [IntegerSlider]
attribute we saw earlier specifies that the data input should be displayed as a slider with a range of 1 to 100. Here are the supported attributes:
[Label(string label)]
: Specifies the label of the data input.[HideLabel]
: Specifies that the label of the data input should be hidden.[Description(string description)]
: Specifies the description of the data input.[HiddenIf(string methodName)]
: Specifies that the data input should be hidden if the specified method returnstrue
. The method must be apublic
orprotected
method in the entity class that returns abool
.[DataInput]
public int MyNumber = 0;
[DataInput]
[HiddenIf(nameof(IsSecretDataInputHidden))]
public string SecretDataInput = "I am hidden unless MyNumber is 42!";
public bool IsSecretDataInputHidden() => MyNumber != 42;[HiddenIf(string dataInputPortName, If @if, object value)]
: Specifies that the data input should be hidden if the specified data input port satisfies the@if
condition. The value must be a constant.[DataInput]
public int MyNumber = 0;
[DataInput]
[HiddenIf(nameof(MyNumber), If.NotEqual, 42)]
public string SecretDataInput = "I am hidden unless MyNumber is 42!";[HiddenIf(string dataInputPortName, Is @is)]
: Specifies that the data input should be hidden if the specified data input port satisfies the@is
condition.[DataInput]
public CharacterAsset MyCharacter;
[DataInput]
[HiddenIf(nameof(MyCharacter), Is.NullOrInactive)]
public string SecretDataInput = "I am hidden unless MyCharacter is selected and active!";[DisabledIf(...)]
: Similar to[HiddenIf(...)]
, but the data input is disabled instead of hidden.[Hidden]
: Specifies that the data input should be always hidden. This is useful when you want to use a data input in code but not expose it to the user.[Disabled]
: Specifies that the data input should be always disabled (non-editable). This is useful for array data inputs that have a constant length, or for visualizing data inputs that are always programmatically set, etc.[Section(string title)]
: Specifies that the data input and all subsequent data inputs should be displayed in a new section with the specified title, unless another section is specified.[SectionHiddenIf(string methodName)]
: Requires attribute[Section]
. Specifies that the section should be hidden if the specified method returnstrue
. The method must be apublic
orprotected
method in the entity class that returns abool
. The@if
and@is
conditions are also supported.[Markdown(bool primary = false)]
: Specifies that the data input should be displayed as a Markdown text and cannot be edited. The data input must be of typestring
. Ifprimary
istrue
, the text will be displayed in a larger font size without a color background.
[HiddenIf]
and [DisabledIf]
attributes are evaluated every frame when the asset or node is visible in the editor. Therefore, you should avoid using expensive operations in these methods.
Some attributes are specific to certain data input types:
[IntegerSlider(int min, int max, int step = 1)]
: Requires data typeint
orint[]
. Specifies that the data input should be displayed as an integer slider with the specified range.[FloatSlider(float min, float max, float step = 0.01f)]
: Requires data typefloat
orfloat[]
. Specifies that the data input should be displayed as a float slider with the specified range.[AutoCompleteResource(string resourceType, string defaultLabel = null)]
: Requires data typestring
. Specifies that the data input should be displayed as an auto-complete list of resources of the specified type. For example, the "Character → Default Idle Animation" data input is defined as[AutoCompleteResource("CharacterAnimation")]
. Please refer to the Resource Providers & Resolvers page for more information.[AutoCompleteList(string methodName, bool forceSelection = false, string defaultLabel = null)]
: Requires data typestring
. Specifies that the data input should be displayed as a dropdown menu generated by the specified method. The method must be apublic
orprotected
method in the entity class that returns aUniTask<AutoCompleteList>
. Note that the method can be asynchronous. IfforceSelection
istrue
, the user can only select a value from the dropdown list, or the value is assignednull
.using System.Linq;
[DataInput]
[AutoComplete(nameof(AutoCompleteVipName), forceSelection: true)]
public string VipName = "Alice";
protected async UniTask<AutoCompleteList> AutoCompleteVipName() {
return AutoCompleteList.Single(vipNames.Select(name => new AutoCompleteEntry {
label = name, // This is what the user sees
value = name // This is what the field stores
}).ToList());
}
private List<string> vipNames; // Entity-controlled runtime data
// Some other code should update the vipNames list提示Autocomplete lists are useful when you want to provide a list of options that are not known at compile time. For example, you can use an autocomplete list to provide a list of files from a directory, a list of emotes from the remote server, etc. They are used very frequently in Warudo's internal nodes and assets!
[MultilineInput]
: Requires data typestring
. Specifies that the data input should be displayed as a multiline text input field.[CardSelect]
: Requires an enum data type. Specifies that the data input should be displayed as a card selection list (similar to "Camera → Control Mode").public enum Color {
[Label("#00FF00")]
[Description("I am so hot!")]
Red,
[Label("#00FF00")]
[Description("I am so natural!")]
Green,
[Label("#0000FF")]
[Description("I am so cool!")]
Blue
}
[DataInput]
[CardSelect]
public Color MyColor = Color.Red;
Enums
You can customize the editor labels of enum values by using the [Label(string label)]
attribute in your enum type. For example:
public enum Color {
[Label("#FF0000")]
Red,
[Label("#00FF00")]
Green,
[Label("#0000FF")]
Blue
}
As mentioned in the Attributes section, you can also use the [Description(string description)]
and [Icon(string icon)]
attributes to show the enum input as a list of cards.
icon
should be a single SVG element (e.g., <svg>...</svg>
). For example:
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 512 512">
<path>...</path>
</svg>
Asset References
Asset references are only available in assets, nodes, and non-plugin structured data.
A data input can be used to reference another asset in the current scene. For example, the following code defines a data input that references a CharacterAsset
:
[DataInput]
public CharacterAsset MyCharacter;
In the editor, the user can select a character asset from the dropdown list:
You can then access ports and triggers of the referenced asset:
[FlowInput]
public Continuation Enter() {
if (MyCharacter.IsNonNullAndActive()) {
MyCharacter.EnterExpression("Joy", transient: true); // Make the character smile!
}
return Exit;
}
You can check if the asset is null
or inactive by using asset.IsNullOrInactive()
, and vice versa, asset.IsNonNullAndActive()
.
What if you want to filter the list of assets shown in the dropdown? You can use the [AssetFilter(string methodName)]
attribute to specify a method that is used to filter the assets in the scene. The method must be a public
or protected
method that receives a parameter of the asset type and returns a bool
. For example:
[DataInput]
[AssetFilter(nameof(FilterCharacterAsset))]
public CharacterAsset MyCharacter;
protected bool FilterCharacterAsset(CharacterAsset character) {
return character.Active; // Only show active characters
}
Accessing Data Inputs Programmatically
Let's say you have an entity. There are two ways to read its data inputs:
- Access the data input field directly. For example, if you have a node with a public data input field
public int MyNumber = 42;
, you can read the value ofMyNumber
directly by usingnode.MyNumber
. - Use the
T GetDataInput<T>(string key)
orobject GetDataInput(string key)
method. This method is available in all entities and returns the value of the data input with the specified name. For example, if you have a node with a data input namedMyNumber
, you can read the value ofMyNumber
by usingnode.GetDataInput<int>("MyNumber")
(ornode.GetDataInput<int>(nameof(node.MyNumber))
which is stylistically preferred).
The key of a port is always the name of the field, unless the port is dynamically added (see Dynamic Ports)
The second method is useful when you need to access data inputs dynamically, for example, when you need to access a data input based on a string variable. (Also see Dynamic Ports.) Otherwise, the two methods do not have practical differences.
Similarly, to write to a data input, you can either assign a value to the data input field directly or use the void SetDataInput<T>(string key, T value)
method. For example, to set the value of a data input named MyNumber
, you can use node.MyNumber = 42
or node.SetDataInput("MyNumber", 42, broadcast: true)
(or node.SetDataInput(nameof(node.MyNumber), 42, broadcast: true)
which is stylistically preferred).
Another alternative to SetDataInput(nameof(MyNumber), 42, broadcast: true)
is to write:
MyNumber = 42;
BroadcastDataInput(nameof(MyNumber));
However, in this case, the second method is strongly recommended due to two reasons:
- It ensures watchers of this data input are notified of the change.
- By setting the
broadcast
parameter totrue
, the change is sent to the editor. Otherwise, you need to useBroadcastDataInput(string key)
to manually send the change to the editor.
You should use the first method only if:
- You are updating the data input extremely frequently, and you do not need to send every change to the editor, i.e., you will call
BroadcastDataInput(string key)
sporadically. This saves performance. - You explicitly do not want to notify watchers of this data input. This is rare.
Data Output Ports
Data output ports are node-specific and are used to provide data to other nodes. A data output is defined as a public method in a node subclass, decorated with the [DataOutput]
attribute. The method can return any non-void type. Here is an example:
[DataOutput]
public int RandomNumber() {
return Random.Range(1, 100); // Return a random number between 1 and 100
}
Data outputs support a subset of data input attributes: [Label]
, [HideLabel]
, [Description]
, [HiddenIf]
, and [DisabledIf]
.
Flow Input Ports
Flow input ports are node-specific and are used to receive flow signals from other nodes to trigger certain actions. A flow input is defined as a public method in a node subclass, decorated with the [FlowInput]
attribute. The method must return a flow output Continuation
. Here is an example:
[DataInput]
public bool FlowToA = true;
[FlowInput]
public Continuation Enter() {
return FlowToA ? ExitA : ExitB; // If FlowToA is true, trigger ExitA; otherwise, trigger ExitB
}
[FlowOutput]
public Continuation ExitA;
[FlowOutput]
public Continuation ExitB;
Flow inputs support a subset of data input attributes: [Label]
, [HideLabel]
, and [Description]
. Note that if the method is named Enter()
and without a [Label]
attribute, the label will be automatically set to the word "Enter" localized in the editor's language.
Flow Output Ports
Flow output ports are node-specific and are used to send flow signals to other nodes. A flow output is defined as a public field in a node subclass, decorated with the [FlowOutput]
attribute. The field must be of type Continuation
. See Flow Inputs for an example.
Flow outputs support a subset of data input attributes: [Label]
, [HideLabel]
, and [Description
]. Note that if the field is named Exit
and without a [Label]
attribute, the label will be automatically set to the word "Exit" localized in the editor's language.
Triggers
Triggers are, simply put, buttons that can be clicked in the editor to trigger certain actions. A trigger is defined as a public method in an entity subclass, decorated with the [Trigger]
attribute. Here is an example:
[Trigger]
public void ShowPopupMessage() {
Context.Service.PromptMessage("Title of the message", "Content of the message");
}
Which is rendered in the editor as:
When the user clicks the button, the ShowPopupMessage
method is called.
Triggers support a subset of data input attributes: [Label]
, [HideLabel]
, [Description]
, [HiddenIf]
, [DisabledIf]
, [Section]
, and [SectionHiddenIf]
.
Asynchronous Triggers
Trigger methods can be asynchronous. For example, if you want to show a message after a delay, you can use UniTask
:
[Trigger]
public async void ShowPopupMessageAfterDelay() { // Note the async keyword
await UniTask.Delay(TimeSpan.FromSeconds(1)); // Wait for 1 second
Context.Service.PromptMessage("Title of the message", "Content of the message");
}
A more practical example is to show a confirmation dialog before proceeding:
[Trigger]
public async void ShowConfirmationDialog() {
bool confirmed = await Context.Service.PromptConfirmation("Are you sure?", "Do you want to proceed?");
if (confirmed) {
// Proceed
}
}
Invoking Triggers Programmatically
Similar to accessing data inputs, you can invoke triggers programmatically by calling the method directly or using the void InvokeTrigger(string key)
method on the entity. For example, to invoke a trigger named ShowPopupMessage
, you can use entity.ShowPopupMessage()
or entity.InvokeTrigger("ShowPopupMessage")
.
Port Order
By default, ports are automatically ordered based on their declaration order in the entity class. However, you can manually specify the order of ports by using the order
parameter in the [DataInput]
, [DataOutput]
, [FlowInput]
, [FlowOutput]
, and [Trigger]
attributes. For example:
[DataInput(order = 1)]
public int MyNumber = 42;
[DataInput(order = -1)]
public string MyString = "Hello, World!"; // This will be displayed before MyNumber
Dynamic Ports
Sometimes, you want to dynamically add or remove ports based on certain conditions. For example, the built-in Multi Gate node has a dynamic number of flow outputs (exits) based on the "Exit Count" data input.
To achieve this, you can access the underlying port collections at runtime:
FlowOutputPortCollection.GetPorts().Clear(); // Clear all flow output ports
for (var i = 1; i <= ExitCount; i++) {
AddFlowOutputPort("Exit" + i, new FlowOutputProperties {
label = "EXIT".Localized() + " " + i
}); // Create a new flow output port for each exit
}
Broadcast(); // Notify the editor that the ports have changed
You can find the full source code of the Multi Gate node here.
You should not add or remove ports frequently (i.e., on every frame), as it may cause performance issues.
Dynamic Port Properties
You can also dynamically change the properties of a port, such as label, description, or type-specific properties. For example, consider the following:
[DataInput]
[IntegerSlider(1, 10)]
public int CurrentItem = 1;
private int itemCount = 10; // Assume we have 10 items initially
The range of the [IntegerSlider]
is determined at compile time. But if itemCount
changes, we will want to update the range of the slider. To do this, you can access the port properties directly:
var properties = GetDataInputPort(nameof(CurrentItem)).Properties;
properties.description = $"Select from {itemCount} items."; // Change the description
var typeProperties = (IntegerDataInputTypeProperties) properties.typeProperties; // Get type-specific properties
typeProperties.min = 1;
typeProperties.max = itemCount; // Change the slider range
BroadcastDataInputProperties(nameof(CurrentItem)); // Notify the editor that the properties have changed