Documentation index for AI agents (llms.txt) A Markdown version of every page is available: request the page's source under /docs/, follow the "alternate" link in this page's head, or start from /llms.txt.
Visual Script logo Scripting API

Custom Signals#

A Signal source adds one way to produce or write an existing value type. Once discovered, it works in every field that accepts that type.

Adding a source to an existing type#

Derive from the type's abstract base, such as TypeNumber, TypeString, or TypeGameObject, and override Get. This example assumes MyComponent exposes a numeric Value property:

using System;
using UnityEngine;
using VisualScript.Icons;
using VisualScript.Runtime;

[Get]

[Title("My Value")]
[Category("My System/My Value")]
[Description("The current value of the custom system")]

[Image(typeof(IconNumber), ColorTheme.Type.Teal)]

[Serializable]
public class TypeNumberMyValue : TypeNumber
{
    [SerializeField] private GetGameObject m_Source = new GetGameObject();

    public override double Get(Args args)
    {
        MyComponent component = this.m_Source.Get<MyComponent>(args);
        return component != null ? component.Value : 0d;
    }

    public override string ToString() => $"{this.m_Source} value";
}

Get returns the component's Value, or 0 when the resolved object has no MyComponent. Three parts define the source's editor and runtime behavior:

  • [Get] adds the type to Get dropdowns. Add [Set] and override Set for a writable source.
  • Get(Args) produces the value from the current context.
  • ToString() returns the phrase shown in the collapsed field.

Writable sources#

Add [Set] and implement Set only when the underlying value is writable. This partial example keeps the source field and Get implementation from the previous example:

[Get] [Set]

[Serializable]
public class TypeNumberMyValue : TypeNumber
{
    public override double Get(Args args) { /* … */ }

    public override void Set(double value, Args args)
    {
        MyComponent component = this.m_Source.Get<MyComponent>(args);
        if (component != null) component.Value = value;
    }
}

Set writes the supplied value when MyComponent exists. Leave [Set] off read-only sources so designers don't select a write operation that can't change the value.

Signals must be pure#

Like Conditions, Get can be called many times per frame. A binding may read it every frame for each subscription, so reading a Signal must not change state.

Side effects make Get depend on evaluation frequency

A Get that spawns an object, writes a variable, or advances state changes the game every time it's evaluated. That frequency depends on how designers use the Signal.

Evaluation cost matters for the same reason. If a binding polls a Get that raycasts, it repeats that raycast every frame.

Reporting changes#

Override Subscription to declare how the Signal detects change. This is what makes On Number Change work with your source. Choose the mode based on the available notifications and evaluation cost:

Mode Behavior Cost
None Not bindable. The default. No subscription work
OnChange Polled each frame; fires when the value differs. One Get per frame per subscriber
OnUpdate Fires every frame regardless. One callback per frame
OnFixedUpdate Fires every physics step. One callback per step
Custom Your implementation subscribes and unsubscribes. Depends on the implementation
OnEditorChange Fires when edited in the Inspector. Editor only. No runtime subscription work

Use OnChange to poll a cheap Get implementation:

protected override SubscriptionMode Subscription => SubscriptionMode.OnChange;

OnChange reads the value every frame and notifies subscribers when it differs. When the underlying source already raises an event, use Custom to avoid that polling. Override OnSubscribe to register the callback and return its subscription Guid, then release it in OnUnsubscribe.

Don't put an expensive Get behind OnChange

OnChange polls. Declaring it on a Signal that searches the scene means that search runs every frame for every binding. Either use Custom, or leave it as None so designers can't accidentally bind to it.

Editor previews#

GetPreview(GameObject source) returns a value for the Inspector outside Play mode. Implement it when the field would otherwise give no indication of its result.

Adding a whole new value type#

A new value type requires four pieces:

  1. An abstract TypeMyThing : TType<MyThing> base.
  2. A GetMyThing : TSignalGet<TypeMyThing, MyThing> wrapper, and SetMyThing if it's writable.
  3. At least one concrete source — usually a constant and a variable lookup.
  4. An editor drawer implementing ITypeDrawer so the field renders properly.

Reserve this work for a type used throughout the project. For one field, prefer a Scriptable Object Signal or a Game Object Signal that resolves a component.

Checklist#

  • [Serializable], derived from the matching Type… base.
  • [Get], [Set], or both, with matching overrides.
  • Get is pure and proportionately cheap.
  • ToString() returns a readable phrase.
  • Subscription matches the source's change mechanism and cost.

Where to go next#

Visual Script logoVisual Script © Catsoft Works 2026. All rights reserved.