Skip to content

Attributes

SyntaxDescriptionNote
[UdonSynced]Variable sync
[UdonSynced(UdonSyncMode.Linear)]Sync with interpolationWhich kinds can be specified depends on the type.
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]Specifying send timing
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]Continuous sending
[FieldChangeCallback(nameof(Value))]Change notification
[NetworkCallable]Allowing calls over the network
[RecursiveMethod]Allowing recursionBecause storage for parameters and local variables isn’t created per call.
[RecursiveMethod] int Fact(int n)Recursion in a local function
[RecursiveMethod] public static int Fact(int n)Recursion in methods of a static class, struct and record
[RecursiveMethod] IsEven / [RecursiveMethod] IsOddMethods that call each otherMarking only one of them is an error (Forms that don’t compile (4)).
other.SendCustomEvent("Tick")Entering a method again while it runsThe compiler saves the arguments and local variables before the call and restores them after it.
[Inline]Inlining calls
[SerializeField]Showing a private field
[HideInInspector]Hiding a public field
[Header] [Tooltip]Header and description
[Range(0f, 1f)]Specifying a value range
[Space]Spacing
[SerializeField, Range(0f, 1f)]Multiple attributes on one field
[System.Obsolete]Marking as obsolete
SyntaxDescriptionErrorReasonAlternative
private int Down(int n) { return Down(n - 1); }Calling itself without the attributeTUKI0001runtimeAdd [RecursiveMethod] (or write it with a loop or your own stack)
IsEven with [RecursiveMethod] and IsOdd without it call each otherMarking only one of the methods that call each otherTUKI0001by designAdd [RecursiveMethod] to every method in the cycle
SendCustomNetworkEvent(…, nameof(Take), 1) (without [NetworkCallable])Calling a method with arguments over the network by name, without the attributeTUKI0001runtimeAdd [NetworkCallable] on the receiving side
[DllImport("x")]Calls an external libraryTUKI0001runtimeUse a method the runtime provides
The registered name of a method with parametersWithout the attribute it is registered under a name that includes the parameter types, which doesn’t match the name you wrote with nameof, so it can’t be referenced by name. Add [NetworkCallable] and it is registered under the name as written
The namespace of the sync attributesThey can be written from this language’s namespace. using the namespace you migrated from at the same time makes two attributes of the same name visible, which is a compile error (removing one of the usings makes it compile. File layout and namespaces)
The 5 not in this table[Reactive], [Computed], [Effect], and [On] are under Reactive programming; [TsukimiTest] is under Testing

A variable with [UdonSynced] keeps the same value across players.

using UnityEngine;
using Tsukimi;
public class AttributesSynced : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
RequestSerialization();
Debug.Log(value); // => 1
}
}

UdonSyncMode specifies the kind of interpolation.

using UnityEngine;
using Tsukimi;
public class AttributesSyncedInterpolated : TsukimiBehaviour
{
[UdonSynced(UdonSyncMode.Linear)] private float value;
void Start()
{
Debug.Log(value); // => 0
}
}

With Manual, the value is sent when you call RequestSerialization().

using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class AttributesSyncModeManual : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
RequestSerialization();
}
}

With Continuous, the value is sent without calling RequestSerialization().

using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]
public class AttributesSyncModeContinuous : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
}
}

When a synced value arrives, the setter of the specified property is called.

using UnityEngine;
using Tsukimi;
public class AttributesFieldChangeCallback : TsukimiBehaviour
{
[UdonSynced, FieldChangeCallback(nameof(Value))] private int backing;
public int Value
{
get { return backing; }
set { backing = value; }
}
void Start()
{
Value = 1;
Debug.Log(Value); // => 1
}
}

A method called across the network with arguments needs this attribute.

using UnityEngine;
using Tsukimi;
using VRC.SDK3.UdonNetworkCalling;
public class AttributesNetworkCallable : TsukimiBehaviour
{
[NetworkCallable]
public void Receive(int n)
{
Debug.Log(n);
}
void Start()
{
Debug.Log(1); // => 1
}
}

A method that calls itself needs this attribute.

using UnityEngine;
using Tsukimi;
public class AttributesRecursiveMethod : TsukimiBehaviour
{
[RecursiveMethod]
private int CountDown(int n)
{
return n <= 0 ? 0 : CountDown(n - 1);
}
void Start()
{
Debug.Log(CountDown(3)); // => 0
}
}

It can also be put on a local function.

using UnityEngine;
using Tsukimi;
public class AttributesRecursiveLocal : TsukimiBehaviour
{
void Start()
{
[RecursiveMethod]
int Fact(int n) { return n <= 1 ? 1 : n * Fact(n - 1); }
Debug.Log(Fact(5)); // => 120
}
}

Recursion in methods of a static class, struct and record

Section titled “Recursion in methods of a static class, struct and record”

It can also be put on a method of a static class, and on a method of a struct or a record.

using UnityEngine;
using Tsukimi;
public static class RecursiveMath
{
[RecursiveMethod]
public static int Fact(int n) { return n <= 1 ? 1 : n * Fact(n - 1); }
}
public struct RecursiveStep
{
public int Step;
[RecursiveMethod]
public int Sum(int n) { return n <= 0 ? 0 : Step + Sum(n - 1); }
}
public record RecursiveTag(int Step)
{
[RecursiveMethod]
public int Sum(int n) { return n <= 0 ? 0 : Step + Sum(n - 1); }
}
public class AttributesRecursiveHelpers : TsukimiBehaviour
{
void Start()
{
RecursiveStep s = new RecursiveStep();
s.Step = 2;
RecursiveTag t = new RecursiveTag(3);
Debug.Log(RecursiveMath.Fact(5)); // => 120
Debug.Log(s.Sum(3)); // => 6
Debug.Log(t.Sum(3)); // => 9
}
}

When methods call each other, every method in the cycle needs it.

using UnityEngine;
using Tsukimi;
public class AttributesRecursiveMutual : TsukimiBehaviour
{
[RecursiveMethod]
private bool IsEven(int n) { return n == 0 ? true : IsOdd(n - 1); }
[RecursiveMethod]
private bool IsOdd(int n) { return n == 0 ? false : IsEven(n - 1); }
void Start()
{
Debug.Log(IsEven(4)); // => true
}
}

Entering a method that is still running again, from a call that runs other code on the spot, needs no attribute.

using Tsukimi;
using VRC.Udon;
public class AttributesReentryThroughOther : TsukimiBehaviour
{
public UdonBehaviour other;
public int depth;
public int last;
public void Hit()
{
int mine = depth;
depth = depth + 1;
// Even if other's Tick calls this Behaviour's Hit back, mine still holds this call's value after it returns.
if (depth < 2) other.SendCustomEvent("Tick");
last = mine;
}
}
Calls that saveEvents sent to this Behaviour (SendCustomEvent to itself, and SendCustomNetworkEvent to itself unless the target is Others), and calls that may run another Behaviour’s code on the spot. Another Behaviour of any class may call this one back, so this covers method calls, SendCustomEvent and SetProgramVariable on it, SetActive, writing enabled, Destroy, Networking.SetOwner, calls that change a Transform’s parent or sibling order (SetParent, assigning parent, DetachChildren, SetSiblingIndex and the like; OnTransformParentChanged and OnTransformChildrenChanged run before the call returns), VRCObjectPool.TryToSpawn and the like
Calls that save nothingReading another Behaviour’s fields (GetProgramVariable included) and delayed sends (SendCustomEventDelayedFrames and the like). A delayed send runs later, so it is a different program, not a cheaper way to write the same one
Exceptions to the calls that saveInstantiate, and writing enabled on a 3D collider. Nothing runs during these calls (the copy’s OnEnable and Start run on a later frame, and switching a collider off sends no trigger event)
CostThe save carries every local variable and intermediate value the method holds at that call, and a method called from several places also compares against each caller to find where to return, so facts.cost can grow well past the call itself. To compare two ways of writing it, measure facts.cost with inspect
CheckingThe methods that save are listed in facts.recursiveMethods of inspect (Hit in this example)
Exporting to UdonSharpA method that may be entered again through another Behaviour is exported with [RecursiveMethod], so UdonSharp also keeps its local variables across the call. A Behaviour with a method that may be entered again through one of its own events that starts on the spot is not exported (Entering a method again while it runs)

A method with this attribute is expanded at the call site regardless of its size.

using UnityEngine;
using Tsukimi;
public class AttributesInline : TsukimiBehaviour
{
[Inline]
private int Twice(int n) { return n * 2; }
void Start()
{
Debug.Log(Twice(2)); // => 4
}
}

Even a non-public field can be assigned a value from the Inspector.

using UnityEngine;
using Tsukimi;
public class AttributesSerializeField : TsukimiBehaviour
{
[SerializeField] private int value;
void Start()
{
Debug.Log(value); // => 0
}
}

A private field shown in the Inspector

The field stays public but no longer appears in the Inspector.

using UnityEngine;
using Tsukimi;
public class AttributesHideInInspector : TsukimiBehaviour
{
[HideInInspector] public int value;
void Start()
{
Debug.Log(value); // => 0
}
}

One public field exists, but no row appears in the Inspector

[Header] shows a heading, and [Tooltip] shows a description on hover, in the Inspector.

using UnityEngine;
using Tsukimi;
public class AttributesHeaderAndTooltip : TsukimiBehaviour
{
[Header("Group")]
[Tooltip("note")]
public int value;
void Start()
{
Debug.Log(value); // => 0
}
}

A heading shown in the Inspector

The description only appears on hover, so it isn’t in this image.

It becomes a slider in the Inspector, and values outside the given range can no longer be assigned.

using UnityEngine;
using Tsukimi;
public class AttributesRange : TsukimiBehaviour
{
[Range(0f, 1f)] public float value;
void Start()
{
Debug.Log(value); // => 0
}
}

The Inspector field shown as a slider

When the Inspector is narrow, no slider appears and only the numeric field is shown. The range restriction applies just the same.

Adds spacing between items in the Inspector.

using UnityEngine;
using Tsukimi;
public class AttributesSpace : TsukimiBehaviour
{
[Space]
public int value;
void Start()
{
Debug.Log(value); // => 0
}
}

The Inspector with the spacing applied

The one above has the spacing. Without it, they sit flush like this.

The Inspector without the spacing

using UnityEngine;
using Tsukimi;
public class AttributesMultipleOnOneField : TsukimiBehaviour
{
[SerializeField, Range(0f, 1f)] private float value;
void Start()
{
Debug.Log(value); // => 0
}
}

Using a member with this attribute produces a warning.

using UnityEngine;
using Tsukimi;
public class AttributesObsolete : TsukimiBehaviour
{
[System.Obsolete]
public void Old() { }
void Start()
{
Debug.Log(1); // => 1
}
}