Finding it by what you want to do
A list that takes you from what you want to do to the page that explains how to write it.
Input and interaction
Section titled “Input and interaction”I want to react when someone uses the thing in front of them
void Interact()
Example
using UnityEngine;using Tsukimi;
// Light switch: each touch turns the room light on or off.//// Setup:// - Put this script on the object used as the switch.// - The same object needs a Collider (without one it cannot be touched).// - Pass the Light to turn on and off to roomLight in the Inspector.// - The light switches only on the screen of the player who touched it (it is not synced).public class GoalsInteract : TsukimiBehaviour{ public Light roomLight;
// Called only on the screen of the player who touched this object. // On desktop, touching is aiming at it and left-clicking. public override void Interact() { roomLight.enabled = !roomLight.enabled; // turn it off if on, on if off }}Note
- It is called only on the screen of the player who touched it. To change everyone’s screen the same way, you need Sync.
I want to catch the use action (the trigger)
void InputUse(bool value, VRC.Udon.Common.UdonInputEventArgs args)
Example
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
// Signal sound: play a sound at the moment the Use button is pressed (it plays even when holding nothing).//// Setup:// - Put this script on any one object.// - Pass the AudioSource to play to signal in the Inspector.// - The sound plays only on the screen of the player who pressed the button.public class GoalsInputUse : TsukimiBehaviour{ public AudioSource signal;
// Called on the pressing player's screen when the Use button is pressed (value is true) and released (value is false). // The Use button is a left click on desktop and usually the trigger in VR. // It does not arrive while a menu is open. public override void InputUse(bool value, UdonInputEventArgs args) { if (!value) return; // do nothing on release signal.Play(); }}Note
- The Use button is left-click on desktop and the trigger on most controllers.
- Nothing arrives while the menu is open. Opening it sends a release, and closing it does not send a press.
I want to catch a jump
void InputJump(bool value, VRC.Udon.Common.UdonInputEventArgs args)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.Udon.Common;
// Double jump: pressing the jump button once more in the air gives one more jump.//// Setup:// - Put this script on any one object.// - Change the strength of the second jump with secondJump in the Inspector.public class GoalsInputJump : TsukimiBehaviour{ public float secondJump = 4f;
private bool usedSecondJump;
// Called on the pressing player's screen when the jump button is pressed (value is true) and released (value is false). // The jump button is Space on desktop and usually a face button on the controller in VR. public override void InputJump(bool value, UdonInputEventArgs args) { if (!value) return;
VRCPlayerApi me = Networking.LocalPlayer; if (me.IsPlayerGrounded()) { usedSecondJump = false; // leave the first jump from the ground to the normal jump return; } if (usedSecondJump) return; // the second jump happens only once in the air
// Keep the current sideways momentum and replace only the upward speed. Vector3 v = me.GetVelocity(); me.SetVelocity(new Vector3(v.x, secondJump, v.z)); usedSecondJump = true; }}Note
- Nothing arrives while the menu is open. Opening it sends a release, and closing it does not send a press.
I want to read how far the stick is pushed
void InputMoveHorizontal(float value, VRC.Udon.Common.UdonInputEventArgs args)
Example
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
// Steering: turn the vehicle model left and right with the left/right movement input.//// Setup:// - Put this script on the vehicle object to turn.// - Change how many degrees it turns per second with turnSpeed in the Inspector.// - The direction changes only on the screen of the player giving input (to show it to others, sync it with VRC Object Sync or similar).public class GoalsInputMoveHorizontal : TsukimiBehaviour{ public float turnSpeed = 90f;
private float steer; // the last left/right input received (-1 to 1)
// Called on the screen of the player giving input when left/right movement input arrives. value is -1 (left) to 1 (right). // On desktop it is the A / D keys (one of -1, 0, 1); in VR, the stick. public override void InputMoveHorizontal(float value, UdonInputEventArgs args) { steer = value; // turning happens in Update; here we only remember the input }
void Update() { transform.Rotate(0f, steer * turnSpeed * Time.deltaTime, 0f); }}Note
- The value is roughly -1 to 1. On desktop it comes from keys, so only whole numbers arrive.
- Nothing arrives while the menu is open.
I want to read the look input
void InputLookHorizontal(float value, VRC.Udon.Common.UdonInputEventArgs args)
Example
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
// Panning camera mount: swing a security camera mount left and right with the left/right look input.//// Setup:// - Put this script on the mount object to swing.// - Set the swing range (degrees to each side) and the degrees turned per second in the Inspector.public class GoalsInputLookHorizontal : TsukimiBehaviour{ public float maxAngle = 60f; public float turnSpeed = 45f;
private float look; // the last left/right look input received (-1 to 1) private float angle; // the mount's current direction (0 is straight ahead)
// Called on the screen of the player giving input when left/right look input arrives. value is -1 (left) to 1 (right). // On desktop it is moving the mouse left and right; in VR, the stick. public override void InputLookHorizontal(float value, UdonInputEventArgs args) { look = value; }
void Update() { // Clamp the angle so it stays within the swing range, then face that way. angle = Mathf.Clamp(angle + look * turnSpeed * Time.deltaTime, -maxAngle, maxAngle); transform.localRotation = Quaternion.Euler(0f, angle, 0f); }}Note
- The value is roughly -1 to 1. On desktop only whole numbers arrive.
- Nothing arrives while the menu is open.
I want to pick up a keyboard key
Input.GetKeyDown(KeyCode.Space)
Example
using UnityEngine;using Tsukimi;
// Desktop shortcut key: the F key turns the hand light on and off.//// Setup:// - Put this script on any one object.// - Pass the Light to turn on and off to handLight in the Inspector.// - This is keyboard input, so it does not arrive in VR (VR controls are received with input events).public class GoalsInputKey : TsukimiBehaviour{ public Light handLight;
// Every frame, check whether the F key was just pressed. void Update() { if (Input.GetKeyDown(KeyCode.F)) { handLight.enabled = !handLight.enabled; } }}Holding things
Section titled “Holding things”I want to react when it is picked up
void OnPickup()
Example
using UnityEngine;using Tsukimi;using VRC.SDK3.Components;
// Flashlight: the light turns on when picked up and off when let go.//// Setup:// - Put this script on the item to be picked up (the flashlight).// - The same object needs VRC Pickup, a Collider and a Rigidbody (VRC Pickup is added by the attribute below).// - Pass the Light to beam and the switch-sound AudioSource to click in the Inspector.// Leave the Light off at first.// - The light and sound change only on the screen of the player who picked it up (how to show them to others is on the sync page).[RequireComponent(typeof(VRCPickup))]public class GoalsOnPickup : TsukimiBehaviour{ public Light beam; public AudioSource click;
// Called only on the screen of the player who picked up this object. // Picking up is a left click on desktop and the grab button in VR (usually the grip). public override void OnPickup() { beam.enabled = true; // turn the light on click.Play(); // play the switch sound }
// Called when the player holding it lets go, only on that player's screen. public override void OnDrop() { beam.enabled = false; // turn the light off }}Note
- It is called only on the screen of the player who picked it up.
- An object with VRC Pickup needs a Rigidbody and a Collider.
I want to react when it is dropped
void OnDrop()
Example
using UnityEngine;using Tsukimi;using VRC.SDK3.Components;
// A few seconds after being let go, the item returns to where it was first placed.//// Setup:// - Put this script on the item to be picked up.// - The same object needs VRC Pickup, a Collider and a Rigidbody (VRC Pickup is added by the attribute below).// - To show the returned position to other players too, also add VRC Object Sync to the same object.// - Change the delay before it returns with returnSeconds in the Inspector.[RequireComponent(typeof(VRCPickup))]public class GoalsOnDrop : TsukimiBehaviour{ public float returnSeconds = 5f;
private VRCPickup pickup; private Rigidbody body; private Vector3 homePosition; private Quaternion homeRotation;
void Start() { // Get the components once, and remember the starting position and rotation. pickup = GetComponent<VRCPickup>(); body = GetComponent<Rigidbody>(); homePosition = transform.position; homeRotation = transform.rotation; }
// Called when the player holding it lets go, only on that player's screen. // On desktop, letting go is a right click. public override void OnDrop() { // Call ReturnHome after returnSeconds seconds. SendCustomEventDelayedSeconds(nameof(ReturnHome), returnSeconds); }
public void ReturnHome() { // If someone picked it up again while waiting, do not return it. if (pickup.IsHeld) return;
// Clear the momentum from being thrown, then put it back where it started, facing the same way. body.velocity = Vector3.zero; body.angularVelocity = Vector3.zero; transform.SetPositionAndRotation(homePosition, homeRotation); }}Note
- It is called only on the screen of the player who dropped it.
I want to catch the use action while it is held
void OnPickupUseDown()
Example
using UnityEngine;using Tsukimi;using VRC.SDK3.Components;
// Water gun: while holding it, water and sound come out only as long as the Use button is held down.//// Setup:// - Put this script on the item to be picked up (the water gun).// - The same object needs VRC Pickup, a Collider and a Rigidbody (VRC Pickup is added by the attribute below).// - Turn on Auto Hold in VRC Pickup. Without it, the Use button does not reach it on desktop.// - Pass the water Particle System to water and the AudioSource to sound in the Inspector.// Turn off Play On Awake on both.// - The water and sound appear only on the holder's screen (how to show them to others is on the sync page).[RequireComponent(typeof(VRCPickup))]public class GoalsOnPickupUseDown : TsukimiBehaviour{ public ParticleSystem water; public AudioSource sound;
// Called at the moment the player holding this object presses the Use button, only on that player's screen. // The Use button is a left click on desktop and the trigger in VR. public override void OnPickupUseDown() { water.Play(); sound.Play(); }
// Called at the moment the Use button is released. public override void OnPickupUseUp() { StopWater(); }
// Also stop when the player lets go while still holding the button (so the water does not keep running). public override void OnDrop() { StopWater(); }
private void StopWater() { water.Stop(); sound.Stop(); }}Note
- On desktop it is not called unless Auto Hold is on in VRC Pickup.
- It is called once, when the button is pressed. Releasing it calls
OnPickupUseUp.
I want to react when a PhysBone is grabbed
void OnPhysBoneGrabbed(VRC.Dynamics.PhysBoneGrabbedInfo physBoneInfo)
Example
using UnityEngine;using Tsukimi;using VRC.Dynamics;using VRC.SDK3.Dynamics.PhysBone.Components;
// Swinging rope: a creaking sound plays only while the PhysBone is being grabbed.//// Setup:// - Put this script on the same object as VRC Phys Bone (a rope or flag placed in the world)// (VRC Phys Bone is added by the attribute below).// - Turn on Allow Grabbing in VRC Phys Bone.// - Pass the creaking AudioSource to creak in the Inspector. Turn on Loop and turn off Play On Awake.[RequireComponent(typeof(VRCPhysBone))]public class GoalsOnPhysBoneGrabbed : TsukimiBehaviour{ public AudioSource creak;
// Called when a player grabs this PhysBone. public override void OnPhysBoneGrabbed(PhysBoneGrabbedInfo physBoneInfo) { creak.Play(); }
// Called when the grabbing hand lets go. public override void OnPhysBoneReleased(PhysBoneReleasedInfo physBoneInfo) { creak.Stop(); }}Note
- Only a program on the same object as the PhysBone receives it.
I want to make a held object leave the hand
pickup.Drop()
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.Components;
// No-carry zone: entering this area while holding the item makes the player let go of it.//// Setup:// - Put this script on an empty object that marks the area.// - Add a Collider to the same object and turn on Is Trigger.// - Pass the VRC Pickup of the item to take away to pickup in the Inspector.public class GoalsDropOnEnter : TsukimiBehaviour{ public VRCPickup pickup;
// Called when a player's body enters this area. // It is called on everyone's screen, including when another player enters. public override void OnPlayerTriggerEnter(VRCPlayerApi player) { // Act only when the player who entered is you and you are the one holding the item. if (!player.isLocal) return; if (pickup.currentPlayer != player) return;
pickup.Drop(); // make the player let go }}Players and avatars
Section titled “Players and avatars”Players
Section titled “Players”I want to react when someone joins
void OnPlayerJoined(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// Arrival board: when someone comes in, show their name and the current number of players on the board.//// Setup:// - Put this script on an empty object used as the board.// - Pass the TextMeshProUGUI that shows the text (UI's Text - TextMeshPro) to board in the Inspector.// - The board text changes on each player's own screen (it is not synced).public class GoalsPlayerJoined : TsukimiBehaviour{ public TextMeshProUGUI board;
// Called on everyone's screen when a player joins the instance. // When you join, it is called once for every player already there (including yourself). // When someone else joins, it is called only for the player who joined. public override void OnPlayerJoined(VRCPlayerApi player) { board.text = player.displayName + " joined (" + VRCPlayerApi.GetPlayerCount() + " here now)"; }}Note
- When you join, it is called once for every player already there, including yourself. Players who join later arrive one at a time.
I want to react when someone leaves
void OnPlayerLeft(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// Departure board: when someone leaves, show their name on the board.//// Setup:// - Put this script on an empty object used as the board.// - Pass the TextMeshProUGUI that shows the text to board in the Inspector.public class GoalsPlayerLeft : TsukimiBehaviour{ public TextMeshProUGUI board;
// Called when a player leaves the instance. The player who left is passed in player. public override void OnPlayerLeft(VRCPlayerApi player) { board.text = player.displayName + " left"; }}I want to react when an avatar changes
void OnAvatarChanged(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// Mirror height: when your avatar changes, move the mirror's center to your eye height.//// Setup:// - Put this script on any one object.// - Pass the Transform of the mirror to adjust (the object with VRC Mirror Reflection) to mirror in the Inspector.// - Assumes a world whose floor is at height 0 (eye height is measured in meters from the floor).public class GoalsAvatarChanged : TsukimiBehaviour{ public Transform mirror;
// Called when a player's avatar has finished loading. The player whose avatar changed is passed in player. // It is also called when someone else's avatar changes, so act only for yourself. public override void OnAvatarChanged(VRCPlayerApi player) { if (!player.isLocal) return;
// Match only the mirror's height to the current avatar's eye height (in meters). float eye = player.GetAvatarEyeHeightAsMeters(); Vector3 p = mirror.position; mirror.position = new Vector3(p.x, eye, p.z); }}Note
- It also arrives for other players each time theirs is synced. To handle only your own, check
player.isLocal.
I want to react when eye height changes
void OnAvatarEyeHeightChanged(VRC.SDKBase.VRCPlayerApi player, float prevEyeHeightAsMeters)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// Walk speed by height: when your eye height changes, scale walking and running speed with your height.//// Setup:// - Put this script on any one object.// - Set the reference eye height and the speeds for it in the Inspector.public class GoalsAvatarEyeHeightChanged : TsukimiBehaviour{ public float baseEyeHeight = 1.6f; // at this eye height (in meters), the speeds below apply public float baseWalkSpeed = 2f; public float baseRunSpeed = 4f;
// Called when a player's eye height changes (switching avatars, or changing the avatar's size). // prevEyeHeightAsMeters holds the eye height before the change. // It is also called for other players each time their new eye height arrives, so act only for yourself. public override void OnAvatarEyeHeightChanged(VRCPlayerApi player, float prevEyeHeightAsMeters) { if (!player.isLocal) return;
float scale = player.GetAvatarEyeHeightAsMeters() / baseEyeHeight; player.SetWalkSpeed(baseWalkSpeed * scale); player.SetRunSpeed(baseRunSpeed * scale); }}Note
- It also arrives for other players each time theirs is synced. To handle only your own, check
player.isLocal.
I want to react when someone sits in a station
void OnStationEntered(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// Driver's seat guide: show the controls guide when you sit, and hide it when you stand up.//// Setup:// - Put this script on the seat (the object with VRC Station) (VRC Station is added by the attribute below).// - The same object also needs a Collider (usually with Is Trigger on). Touching it calls Interact below, which seats you.// - Pass the controls-guide object (UI or similar) to guide in the Inspector. Keep it hidden at first.[RequireComponent(typeof(VRC.SDK3.Components.VRCStation))]public class GoalsStationEntered : TsukimiBehaviour{ public GameObject guide;
// Called on the screen of the player who touched this object. // A station does not seat you just by touching it, so seat yourself in this station here. public override void Interact() { Networking.LocalPlayer.UseAttachedStation(); }
// Called when a player sits in this station. The player who sat is passed in player. // The guide should appear only for the player who sat, so show it only when that is you. public override void OnStationEntered(VRCPlayerApi player) { if (player.isLocal) guide.SetActive(true); }
// Called when a player gets up from this station. public override void OnStationExited(VRCPlayerApi player) { if (player.isLocal) guide.SetActive(false); }}Note
- Touching a station does not seat the player. Call
UseAttachedStation()fromInteract, as in the example.
I want to react when someone respawns
void OnPlayerRespawn(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// Start over: when you respawn, reset your score to 0.//// Setup:// - Put this script on the object that counts the score.// - Pass the TextMeshProUGUI that shows the score to board in the Inspector.// - The score exists only on each player's own screen (it is not synced).public class GoalsPlayerRespawn : TsukimiBehaviour{ public TextMeshProUGUI board; private int score;
// Called when a player respawns (by pressing Respawn in the menu). // The player who respawned is passed in player. Reset the score only for yourself. public override void OnPlayerRespawn(VRCPlayerApi player) { if (!player.isLocal) return;
score = 0; board.text = "Score: " + score; }
// Add 1 point (called from another script with SendCustomEvent). public void AddPoint() { score++; board.text = "Score: " + score; }}Sync and networking
Section titled “Sync and networking”I want a value shared with everyone
[UdonSynced] private int value;
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// みんなで数えるカウンター: 誰かが触ると 1 増え、全員の画面に同じ数が出る。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 数を出す TextMeshProUGUI を、インスペクタの label に渡す。// - 同期の送り方は Manual(下の属性)。値を変えた人が RequestSerialization で送る。[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public class GoalsSyncValue : TsukimiBehaviour{ public TextMeshProUGUI label;
// [UdonSynced] を付けた変数は、オーナーが送った値が全員に届く。 // オーナー以外が書き換えても、ほかの人には届かない(次に届いた値で上書きされる)。 [UdonSynced] private int count;
void Start() { Show(); }
// 触った人の画面で呼ばれる。書き換えられるのはオーナーだけなので、先に自分をオーナーにする。 public override void Interact() { Networking.SetOwner(Networking.LocalPlayer, gameObject); count++; RequestSerialization(); Show(); }
// ほかの人が送った値が届いたときに呼ばれる。届いた値はもう count に入っている。 public override void OnDeserialization() { Show(); }
void Show() { label.text = count.ToString(); }}I want to become the owner of an object
Networking.SetOwner(Networking.LocalPlayer, gameObject)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// 持ち主の札: 触った人がこのオブジェクトのオーナーになり、札に今のオーナーの名前が出る。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 名前を出す TextMeshProUGUI を、インスペクタの label に渡す。public class GoalsTakeOwnership : TsukimiBehaviour{ public TextMeshProUGUI label;
void Start() { Show(Networking.GetOwner(gameObject)); }
// 触った人の画面で呼ばれる。オーナーを自分に移す。 // 同期する変数を書き換えて送れるのはオーナーだけなので、書く前にこれを呼ぶ。 public override void Interact() { if (!Networking.IsOwner(gameObject)) Networking.SetOwner(Networking.LocalPlayer, gameObject); }
// オーナーが移ったときに、全員の画面で呼ばれる。新しいオーナーが player に入る。 public override void OnOwnershipTransferred(VRCPlayerApi player) { Show(player); }
void Show(VRCPlayerApi owner) { label.text = "いまの持ち主: " + owner.displayName; }}I want to choose between sending on my own cue and sending automatically
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 回る看板: オーナーの画面で回した角度を、全員の画面に自動で送り続ける。//// Setup:// - このスクリプトは、回したいオブジェクトに付ける。// - 送り方は Continuous(下の属性)。オーナーの値が一定の間隔で自動で送られ、// 受け取った側は値の間を補間する。RequestSerialization は要らない。// - 送るきっかけを自分で出したいとき(ボタンを押したときだけ送る、など)は// BehaviourSyncMode.Manual にして、値を変えたあとに RequestSerialization を呼ぶ。[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]public class GoalsSyncMode : TsukimiBehaviour{ public float degreesPerSecond = 45f;
// Continuous では、補間の仕方を UdonSynced の引数で選べる(Linear は角度のように連続した値向け)。 [UdonSynced(UdonSyncMode.Linear)] private float angle;
void Update() { // 値を進めるのはオーナーだけ。ほかの人は届いた値で表示だけを合わせる。 if (Networking.IsOwner(gameObject)) angle = (angle + degreesPerSecond * Time.deltaTime) % 360f; transform.localRotation = Quaternion.Euler(0f, angle, 0f); }}I want a guide to how much is too much to send
[NetworkCallable(100)]
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.UdonNetworkCalling;using VRC.Udon.Common.Interfaces;
// 拍手のボタン: 触ると全員の画面で拍手の音が鳴る。連打しても、1 秒に送る回数は上限までに抑えられる。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 拍手の音を入れた AudioSource を、インスペクタの clap に渡す。public class GoalsNetworkCallableRate : TsukimiBehaviour{ public AudioSource clap;
// 触った人の画面で呼ばれる。全員の画面で Clap を呼ぶ。 public override void Interact() { SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Clap)); }
// ネットワークから呼ばれる処理には [NetworkCallable] を付ける。 // 引数は 1 秒に送る回数の上限(省くと 5・最大 100)。超えた分は捨てられず、 // 送り手の側で待たされて、上限の間隔で順に送られる。 [NetworkCallable(10)] public void Clap() { clap.Play(); }}Note
- 上限を超えて送った分は捨てられず、送り手の側で待たされてから順に送られます。
- 引数の無い
publicメソッドは[NetworkCallable]が無くても呼べますが、これは古い書き方との互換のためで、VRChat は勧めていません。
Events
Section titled “Events”I want to call a method on another behaviour
other.SendCustomEvent("Ping")
Example
using UnityEngine;using Tsukimi;using VRC.Udon;
// 呼び鈴: 触ると、別のオブジェクトに付いた Behaviour の Ring を呼ぶ。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 呼ばれる側の Behaviour(public void Ring() を持つもの)を、インスペクタの bell に渡す。// - 呼ばれる側の型が決まっているなら、フィールドをその型で宣言して bell.Ring() と書くほうが、// 綴りの間違いがコンパイルで見つかる。名前で呼ぶのは、相手の型を決めずに差し替えたいときの形。public class GoalsCallOther : TsukimiBehaviour{ public UdonBehaviour bell;
// 触った人の画面で呼ばれる。相手の処理を名前で呼ぶ(自分の画面の中だけで走る)。 public override void Interact() { bell.SendCustomEvent("Ring"); }}I want the same method called on everyone’s side
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ping))
Example
using UnityEngine;using Tsukimi;using VRC.SDK3.UdonNetworkCalling;using VRC.Udon.Common.Interfaces;
// 鐘: 誰かが触ると、そのインスタンスにいる全員の画面で鐘が鳴る。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 鐘の音を入れた AudioSource を、インスペクタの sound に渡す。public class GoalsNetworkEvent : TsukimiBehaviour{ public AudioSource sound;
// 触った人の画面で呼ばれる。全員の画面(自分も含む)で Ring を呼ぶ。 // 自分以外にだけ送るなら NetworkEventTarget.Others、オーナーにだけなら Owner。 public override void Interact() { SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ring)); }
// ネットワークから呼ばれる処理。名前が _ で始まるものは呼ばれない。 [NetworkCallable] public void Ring() { sound.Play(); }}Note
- 名前が
_で始まるメソッドは、ネットワークからは呼べません。ほかの人から呼ばれたくない処理はその名前にします。
I want the call delayed by a moment
SendCustomEventDelayedSeconds(nameof(Ping), 1.5f)
Example
using UnityEngine;using Tsukimi;
// 自動で閉まる扉: 触ると開き、3 秒たつと閉まる。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 開いているあいだ消しておく扉の板を、インスペクタの door に渡す。public class GoalsDelayedSeconds : TsukimiBehaviour{ public GameObject door; public float closeAfter = 3f;
// 触った人の画面で呼ばれる。扉を消し、closeAfter 秒後に Close を呼ぶよう予約する。 public override void Interact() { door.SetActive(false); SendCustomEventDelayedSeconds(nameof(Close), closeAfter); }
// 予約した時間がたつと呼ばれる。予約は取り消せないので、 // 待っているあいだにもう一度触ると、最初の予約の時刻で一度閉まる。 public void Close() { door.SetActive(true); }}I want the call delayed by a number of frames
SendCustomEventDelayedFrames(nameof(Ping), 10)
Example
using UnityEngine;using Tsukimi;
// 押した合図: ボタンを触ると、10 フレームのあいだだけランプを点ける。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 点けたり消したりするランプのオブジェクトを、インスペクタの lamp に渡す。最初は非表示にしておく。public class GoalsDelayedFrames : TsukimiBehaviour{ public GameObject lamp;
// 触った人の画面で呼ばれる。ランプを点け、10 フレーム後に Off を呼ぶよう予約する。 public override void Interact() { lamp.SetActive(true); SendCustomEventDelayedFrames(nameof(Off), 10); }
// 予約したフレーム数がたつと呼ばれる。時間でなくフレームで数えるので、 // 重い画面では点いている時間が長くなる(見た目の長さを揃えたいなら DelayedSeconds を使う)。 public void Off() { lamp.SetActive(false); }}Variables
Section titled “Variables”I want to read a field on another behaviour
(int)other.GetProgramVariable("count")
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.Udon;
// 点数の表示板: 別のオブジェクトに付いた Behaviour の score を読んで、毎フレーム表示する。//// Setup:// - 点数を持つ側の Behaviour(int score というフィールドを持つもの)を、インスペクタの game に渡す。// - 表示する TextMeshProUGUI を、インスペクタの label に渡す。// - 相手の型が決まっているなら、フィールドをその型で宣言して game.score と書くほうが、// 名前と型の間違いがコンパイルで見つかる。名前で読むのは、相手の型を決めずに差し替えたいときの形。public class GoalsGetProgramVariable : TsukimiBehaviour{ public UdonBehaviour game; public TextMeshProUGUI label;
void Update() { // 名前で読むと object で返るので、元の型へキャストする。 // 名前が違うと null が返り、キャストで止まる。 int score = (int)game.GetProgramVariable("score"); label.text = "Score: " + score; }}I want to write a field on another behaviour
other.SetProgramVariable("count", 5)
Example
using UnityEngine;using Tsukimi;using VRC.Udon;
// 難易度のボタン: 触ると、別のオブジェクトに付いた Behaviour の level を 3 にする。//// Setup:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 難易度を持つ側の Behaviour(int level というフィールドを持つもの)を、インスペクタの game に渡す。// - 相手の型が決まっているなら、フィールドをその型で宣言して game.level = 3 と書くほうが、// 名前と型の間違いがコンパイルで見つかる。public class GoalsSetProgramVariable : TsukimiBehaviour{ public UdonBehaviour game; public int level = 3;
// 触った人の画面で呼ばれる。相手の変数を名前で書き換える(自分の画面の中だけ)。 // 書き換えても相手の処理は呼ばれないので、反映させる処理があるなら続けて呼ぶ。 public override void Interact() { game.SetProgramVariable("level", level); game.SendCustomEvent("ApplyLevel"); }}I want to know the type of that field
other.GetProgramVariableType("count")
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.Udon;
// 値の中身を見る板: 相手の Behaviour の value の型を確かめてから、型に合わせて表示する。//// Setup:// - 見たい側の Behaviour(value というフィールドを持つもの)を、インスペクタの target に渡す。// - 表示する TextMeshProUGUI を、インスペクタの label に渡す。public class GoalsGetProgramVariableType : TsukimiBehaviour{ public UdonBehaviour target; public TextMeshProUGUI label;
void Start() { // その名前の変数が無いときは null が返る。 System.Type type = target.GetProgramVariableType("value"); if (type == null) { label.text = "value という変数はありません"; } else if (type == typeof(int)) { label.text = "int: " + (int)target.GetProgramVariable("value"); } else { label.text = type.Name + " です"; } }}Audio, video and external data
Section titled “Audio, video and external data”I want to play a sound when something is touched
source.Play()
Example
using UnityEngine;using Tsukimi;
// Doorbell: rings when touched. Touching it again while it rings does not restart it.//// Setup:// - Put this script on the object used as the bell.// - The same object needs a Collider (without one it cannot be touched).// - Pass the AudioSource to ring to bell in the Inspector (put the sound in the AudioSource's AudioClip).// - The sound plays only on the screen of the player who touched it (how to let everyone hear it is on the sync page).public class GoalsAudioPlay : TsukimiBehaviour{ public AudioSource bell;
// Called only on the screen of the player who touched this object. public override void Interact() { if (bell.isPlaying) return; // do not restart while it is ringing bell.Play(); }}I want to layer a sound effect without stopping what is playing
source.PlayOneShot(clip, 0.7f)
Example
using UnityEngine;using Tsukimi;
// Coin sound: each touch layers another sound. Tapping quickly does not cut off the previous one.//// Setup:// - Put this script on the object used as the coin.// - The same object needs a Collider.// - Pass the AudioSource to source and the sound to play to coin in the Inspector.// - The sound plays only on the screen of the player who touched it.public class GoalsAudioOneShot : TsukimiBehaviour{ public AudioSource source; public AudioClip coin;
// Called only on the screen of the player who touched this object. public override void Interact() { // PlayOneShot plays on top of whatever is already playing, without stopping it. // So it does not sound the same every time, vary the volume slightly between 0.8 and 1. source.PlayOneShot(coin, Random.Range(0.8f, 1f)); }}I want to react when playback starts
void OnVideoStart()
Example
using UnityEngine;using Tsukimi;
// Show start: when the video starts playing, hide the "loading" display and dim the audience lights.//// Setup:// - Put this script on the same object as the video player (VRC Unity Video Player or VRC AVPro Video Player).// Video events arrive only from the video player on the same object.// - Pass the "loading" display to loading and the Light to dim to houseLight in the Inspector.public class GoalsVideoStart : TsukimiBehaviour{ public GameObject loading; public Light houseLight;
// Called when the video player on this object starts playback from a stopped state. public override void OnVideoStart() { loading.SetActive(false); houseLight.enabled = false; }}Note
- It arrives only from the video player on the same object.
I want to react when playback ends
void OnVideoEnd()
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.Video.Components.Base;
// Continuous play: when a video ends, load and play the next one. After the last one, go back to the first.//// Setup:// - Put this script on the same object as the video player (VRC Unity Video Player or VRC AVPro Video Player).// - Pass the same video player to player in the Inspector, and list the video URLs in playlist.// - Each player can load a new URL only once every 5 seconds (counted across all video players).// - Here loading happens on each player's own screen (how to keep everyone on the same video is on the sync page).public class GoalsVideoEnd : TsukimiBehaviour{ public BaseVRCVideoPlayer player; public VRCUrl[] playlist;
private int current;
// Called when the video player on this object finishes playing (when the video reaches the end, and when it is stopped). public override void OnVideoEnd() { current = (current + 1) % playlist.Length; // move to the next index; after the last comes 0 player.PlayURL(playlist[current]); }}Note
- It arrives only from the video player on the same object.
- It arrives when the video reaches the end, and also when a player stops it.
I want to react when playback fails
void OnVideoError(VRC.SDK3.Components.Video.VideoError videoError)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDK3.Components.Video;
// Playback failure notice: when a video cannot be loaded, show a notice that matches the reason.//// Setup:// - Put this script on the same object as the video player (VRC Unity Video Player or VRC AVPro Video Player).// - Pass the TextMeshProUGUI that shows the notice to status in the Inspector.public class GoalsVideoError : TsukimiBehaviour{ public TextMeshProUGUI status;
// Called when the video player on this object fails to load a video. The reason is passed in videoError. public override void OnVideoError(VideoError videoError) { if (videoError == VideoError.RateLimited) status.text = "Loading too quickly. Wait about 5 seconds and try again"; else if (videoError == VideoError.InvalidURL) status.text = "The URL is not valid"; else if (videoError == VideoError.AccessDenied) status.text = "This URL cannot be loaded"; else status.text = "The video could not be loaded"; }}Note
- It arrives only from the video player on the same object.
- Even when retrying after a failure, a player can load a URL at most once every 5 seconds.
Loading from outside
Section titled “Loading from outside”I want to receive an image fetched from outside
void OnImageLoadSuccess(VRC.SDK3.Image.IVRCImageDownload result)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.Image;using VRC.Udon.Common.Interfaces;
// Poster from a URL: on entering the world, load the image at a URL, put it on the poster, and hide the "loading" display.//// Setup:// - Put this script on any one object near the poster.// - Pass the image URL to imageUrl, the Material to put it on to poster, and the "loading" display to loading in the Inspector.// - Images up to 2048x2048 can be loaded. One image loads every 5 seconds at most; anything beyond that waits in a queue.// - URLs outside the listed sites load only if the viewer has turned on "Allow Untrusted URLs" in their settings.public class GoalsImageLoadSuccess : TsukimiBehaviour{ public VRCUrl imageUrl; public Material poster; public GameObject loading;
private VRCImageDownloader downloader;
void Start() { // Keep the downloader in a field (if it is not kept, it can be cleaned up partway through). downloader = new VRCImageDownloader(); // Once loaded, it is put on poster's main texture automatically, and the result events arrive at this script. downloader.DownloadImage(imageUrl, poster, (IUdonEventReceiver)this, null); }
// Called when the image has loaded. result.Result holds the loaded Texture2D. public override void OnImageLoadSuccess(IVRCImageDownload result) { loading.SetActive(false); }
void OnDestroy() { downloader.Dispose(); // release the memory used by the loaded images }}Note
- Images can be up to 2048×2048. One image loads every 5 seconds, and the rest wait in line.
- URLs outside the allowed sites load only if the viewer has turned on “Allow Untrusted URLs”.
- Keep the downloader in a field and call
Dispose()when you are done with it.
I want to receive a string fetched from outside
void OnStringLoadSuccess(VRC.SDK3.StringLoading.IVRCStringDownload result)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.StringLoading;using VRC.Udon.Common.Interfaces;
// Notice board: on entering the world, load the text placed at a URL and show it on the board (the content can change without re-uploading the world).//// Setup:// - Put this script on the object used as the board.// - Pass the URL of the text to noticeUrl and the TextMeshProUGUI that shows it to board in the Inspector.// - One string loads every 5 seconds at most. URLs outside the listed sites (GitHub Pages, Gist, Pastebin and others)// load only if the viewer has turned on "Allow Untrusted URLs" in their settings.public class GoalsStringLoadSuccess : TsukimiBehaviour{ public VRCUrl noticeUrl; public TextMeshProUGUI board;
void Start() { // The result events arrive at the script passed as the second argument (here, this one). VRCStringDownloader.LoadUrl(noticeUrl, (IUdonEventReceiver)this); }
// Called when the text has loaded. result.Result holds the string, read as UTF-8. public override void OnStringLoadSuccess(IVRCStringDownload result) { board.text = result.Result; }}Note
- One URL loads every 5 seconds, and the rest wait in line.
- URLs outside the allowed sites load only if the viewer has turned on “Allow Untrusted URLs”.
I want to react when the fetch failed
void OnImageLoadError(VRC.SDK3.Image.IVRCImageDownload result)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.Image;using VRC.Udon.Common.Interfaces;
// Fallback picture: if the image at the URL cannot be loaded, put a prepared image on instead and show the reason.//// Setup:// - Put this script on any one object near the poster.// - Pass the image URL to imageUrl, the Material to put it on to poster, the fallback image to fallback,// and the TextMeshProUGUI that shows the reason to status in the Inspector.public class GoalsImageLoadError : TsukimiBehaviour{ public VRCUrl imageUrl; public Material poster; public Texture2D fallback; public TextMeshProUGUI status;
private VRCImageDownloader downloader;
void Start() { downloader = new VRCImageDownloader(); downloader.DownloadImage(imageUrl, poster, (IUdonEventReceiver)this, null); }
// Called when the image could not be loaded. result.ErrorMessage holds the reason. public override void OnImageLoadError(IVRCImageDownload result) { poster.mainTexture = fallback; status.text = "The image could not be loaded: " + result.ErrorMessage; }
void OnDestroy() { downloader.Dispose(); }}I want to react when a key is pressed
void MidiNoteOn(int channel, int number, int velocity)
Example
using UnityEngine;using Tsukimi;
// Keyboard-lit light: pressing a key changes the color by pitch and the brightness by how hard it is pressed.//// Setup:// - Place a VRC Midi Listener in the scene and set its Behaviour to the object with this script.// Turn on Note On in Active Events (no events are on at first).// - Pass the Light to light up to stageLight in the Inspector.// - The MIDI device used is the first one found (choose one with the launch option --midi=device name).public class GoalsMidiNoteOn : TsukimiBehaviour{ public Light stageLight; public float maxIntensity = 3f;
// Called when a MIDI Note On is received (pressing a key or button, or MIDI playback). // channel is 0-15, number is the note number 0-127 (60 is middle C), velocity is how hard it was pressed, 0-127. public override void MidiNoteOn(int channel, int number, int velocity) { stageLight.color = Color.HSVToRGB(number / 127f, 1f, 1f); // rotate the hue by pitch stageLight.intensity = velocity / 127f * maxIntensity; // the harder the press, the brighter }}Note
- All Active Events on VRC Midi Listener start turned off. Tick the events you use.
I want to react when a key is released
void MidiNoteOff(int channel, int number, int velocity)
Example
using UnityEngine;using Tsukimi;
// Light that goes out on release: on only while keys are held, off when released.//// Setup:// - Place a VRC Midi Listener in the scene and set its Behaviour to the object with this script.// Turn on Note On and Note Off in Active Events.// - Pass the Light to turn on and off to keyLight in the Inspector.public class GoalsMidiNoteOff : TsukimiBehaviour{ public Light keyLight;
private int held; // the number of keys currently held
// Called when a MIDI Note On is received. public override void MidiNoteOn(int channel, int number, int velocity) { // A Note On with velocity 0 is sometimes sent to mean "released" (a MIDI convention). if (velocity == 0) { Release(); return; } held++; keyLight.enabled = true; }
// Called when a MIDI Note Off is received (usually when a key or button is released). public override void MidiNoteOff(int channel, int number, int velocity) { Release(); }
private void Release() { if (held > 0) held--; if (held == 0) keyLight.enabled = false; // turn off once every key is released }}Note
- Some devices send a velocity-0
MidiNoteOninstead ofMidiNoteOffwhen a key is released. - All Active Events on VRC Midi Listener start turned off. Tick the events you use.
I want to receive MIDI control changes
void MidiControlChange(int channel, int number, int value)
Example
using UnityEngine;using Tsukimi;
// Brightness knob: turning a chosen knob (control number) on the MIDI device changes the light's brightness.//// Setup:// - Place a VRC Midi Listener in the scene and set its Behaviour to the object with this script.// Turn on Control Change in Active Events.// - Pass the Light whose brightness changes to roomLight, and put the knob's number in knob, in the Inspector// (knob numbers differ by device; check the device's manual, or display number below once to find it).public class GoalsMidiControlChange : TsukimiBehaviour{ public Light roomLight; public int knob = 1; public float maxIntensity = 2f;
// Called when a MIDI Control Change is received (usually when a knob or slider on the device moves). // channel is 0-15, number is the control number 0-127, value is 0-127. public override void MidiControlChange(int channel, int number, int value) { if (number != knob) return; // ignore everything but the chosen knob roomLight.intensity = value / 127f * maxIntensity; }}Note
- All Active Events on VRC Midi Listener start turned off. Tick the events you use.
- Only the first device found is used.
Persistence and purchases
Section titled “Persistence and purchases”Saved data
Section titled “Saved data”I want to react when a player’s saved data comes back
void OnPlayerRestored(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.Persistence;
// Personal best: read the best score so far, show it on the board, and rewrite it when beaten.//// Setup:// - Put this script on the object that counts the score.// - Pass the TextMeshProUGUI that shows the best score to board in the Inspector.// - You can write only your own data (other players' data can be read but not written).public class GoalsPlayerRestored : TsukimiBehaviour{ public TextMeshProUGUI board; public int lastScore; // the score of the last play (another script sets it with SetProgramVariable)
private bool restored; // whether your own data can be read yet private int best;
// Called when a player's saved data has loaded (once per player, including yourself). // Do not read or write per-player data before this. public override void OnPlayerRestored(VRCPlayerApi player) { if (!player.isLocal) return; // handle only your own data restored = true;
// When nothing is saved (first visit), false is returned and best stays 0. if (PlayerData.TryGetInt(player, "best", out int saved)) best = saved; board.text = "Personal best: " + best; }
// Call this from another script with SendCustomEvent when a play ends. public void Finish() { if (!restored) return; // writing before loading would overwrite the previous record if (lastScore <= best) return; best = lastScore; PlayerData.SetInt("best", best); board.text = "Personal best: " + best; }}Note
- Do not read or write a player’s values before this event arrives for them.
- You can write only your own data.
I want to react when the storage limit is passed
void OnPlayerDataStorageExceeded(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// When saved data goes over capacity: stop further writes and show what happened on screen.//// Setup:// - Put this script on the same object as the script that writes per-player data.// - Pass the TextMeshProUGUI that shows the notice to notice in the Inspector.public class GoalsPlayerDataStorageExceeded : TsukimiBehaviour{ public TextMeshProUGUI notice;
[HideInInspector] public bool canSave = true; // the writing script checks this before writing
// Called when a player's saved data goes over the available capacity. public override void OnPlayerDataStorageExceeded(VRCPlayerApi player) { if (!player.isLocal) return; canSave = false; notice.text = "Saved data is over capacity (using " + Networking.GetPlayerDataStorageUsage(player) + " / limit " + Networking.GetPlayerDataStorageLimit() + " bytes)"; }}Note
- The function that reports how much is used is on
Networking, notPlayerData. The amount is in bytes.
I want to react when the storage is nearly full
void OnPlayerDataStorageWarning(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.Persistence;
// When capacity is running low: shrink the growing play history to its newer half and write it back.//// Setup:// - Put this script on the same object as the script that writes the play history (a string named "history").// - Pass the TextMeshProUGUI that shows the notice to notice in the Inspector.public class GoalsPlayerDataStorageWarning : TsukimiBehaviour{ public TextMeshProUGUI notice;
// Called when a player's saved data gets close to the available capacity. public override void OnPlayerDataStorageWarning(VRCPlayerApi player) { if (!player.isLocal) return; // you can write only your own data
if (PlayerData.TryGetString(player, "history", out string history)) { // The history is assumed to be in oldest-first order. Keep only the back half (the newer part). PlayerData.SetString("history", history.Substring(history.Length / 2)); } notice.text = "Saved data is running low on capacity, so older records were reduced"; }}Note
- The function that reports how much is used is on
Networking, notPlayerData. The amount is in bytes.
Purchases
Section titled “Purchases”I want to react when something is bought
void OnPurchaseConfirmed(VRC.Economy.IProduct product, VRC.SDKBase.VRCPlayerApi player, bool purchasedNow)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.Economy;
// Supporters' room: open the bonus room's door only for players who bought the chosen product. Show thanks when bought on the spot.//// Setup:// - Put this script on any one object near the door.// - In the Inspector, put the bonus product's ID in productId, the door to keep closed in door, and the TextMeshProUGUI for the thanks in thanks.// - The door opens only on the buyer's own screen (it stays closed on everyone else's).public class GoalsPurchaseConfirmed : TsukimiBehaviour{ public string productId; public GameObject door; public TextMeshProUGUI thanks;
// Called when a player's purchase record has been loaded and confirmed // (when you join, when someone joins, and when something is bought on the spot). // product is the purchased product, player is the buyer, purchasedNow is true when bought on the spot (false for an earlier purchase). public override void OnPurchaseConfirmed(IProduct product, VRCPlayerApi player, bool purchasedNow) { if (!player.isLocal) return; // handle only what you bought if (product.ID != productId) return; // only the chosen product
door.SetActive(false); // remove the door so the room can be entered if (purchasedNow) thanks.text = "Thank you for buying " + product.Name; }}Note
- It arrives not only on purchase but also when a purchase record is confirmed as you or someone else joins. Use
purchasedNowto tell whether it was just bought.
I want to react when a purchase expires
void OnPurchaseExpired(VRC.Economy.IProduct product, VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.Economy;
// Time-limited bonus: when the chosen product expires, close the bonus room's door again.//// Setup:// - Put this script on any one object near the door (the opening side uses the same door as the purchase example).// - Put the time-limited product's ID in productId and the door to close again in door in the Inspector.public class GoalsPurchaseExpired : TsukimiBehaviour{ public string productId; public GameObject door;
// Called when your side detects that a product owned by someone in the instance has expired. // product is the expired product, player is its owner. public override void OnPurchaseExpired(IProduct product, VRCPlayerApi player) { if (!player.isLocal) return; // only for your own product if (product.ID != productId) return;
door.SetActive(true); // put the door back and close the room }}Note
- It arrives when your client notices that someone in the instance has an expired purchase.
I want to receive the list of products
void OnListAvailableProducts(VRC.Economy.IProduct[] products)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.Economy;using VRC.Udon.Common.Interfaces;
// Product board: on entering the world, list the names and descriptions of the products sold in this world on the board.//// Setup:// - Put this script on the object used as the board.// - Pass the TextMeshProUGUI to list them in to board in the Inspector.public class GoalsListAvailableProducts : TsukimiBehaviour{ public TextMeshProUGUI board;
void Start() { // The result arrives at OnListAvailableProducts on the script passed in (here, this one). Store.ListAvailableProducts((IUdonEventReceiver)this); }
// Called when the result of Store.ListAvailableProducts arrives. products holds every product of this world. public override void OnListAvailableProducts(IProduct[] products) { string text = ""; foreach (IProduct p in products) text += p.Name + " — " + p.Description + "\n"; board.text = text; }}Note
- The result arrives at the program you passed to
ListAvailableProducts.
Motion and display
Section titled “Motion and display”Position and physics
Section titled “Position and physics”I want to launch an object upward
body.AddForce(force, ForceMode.Impulse)
Example
using UnityEngine;using Tsukimi;
// Jump pad for objects: launches objects (with a Rigidbody) that enter the area above the pad upward.//// Setup:// - Put this script on the same object as the Collider (Is Trigger) that forms the pad's area.// - power sets how hard it launches.public class GoalsUnityForce : TsukimiBehaviour{ public float power = 6f;
// Called when some Collider enters this area. void OnTriggerEnter(Collider other) { Rigidbody body = other.attachedRigidbody; if (body == null) return; // objects without a Rigidbody cannot be moved // Impulse applies it as an instant push that takes mass into account. body.AddForce(Vector3.up * power, ForceMode.Impulse); }}I want to find out what is in front of something
Physics.Raycast(origin, direction, out hit, 12.5f)
Example
using UnityEngine;using Tsukimi;using TMPro;
// Laser rangefinder: casts a ray straight ahead of the device and shows the distance to and name of what it hits.//// Setup:// - Put this script on the rangefinder object. It measures along the device's front (the blue axis).// - Pass the TextMeshProUGUI that shows the result to readout, and the marker placed where it hits to marker.public class GoalsUnityRaycast : TsukimiBehaviour{ public TextMeshProUGUI readout; public Transform marker;
void Update() { RaycastHit hit; // Position, direction, hit result, and reach (12.5 m). Returns true on a hit and fills in hit. if (Physics.Raycast(transform.position, transform.forward, out hit, 12.5f)) { readout.text = hit.collider.gameObject.name + " " + hit.distance.ToString("F2") + " m"; marker.position = hit.point; } else { readout.text = "---"; } }}I want to move something a little every frame
transform.Translate(velocity * Time.deltaTime)
Example
using UnityEngine;using Tsukimi;
// Elevator going back and forth: moves the floor up a little every frame and turns downward on reaching the top.//// Setup:// - Put this script on the elevator floor object.// - travel sets how high it moves (m), and speed how fast (m per second).// - The floor's movement is computed on each player's screen (it is not synced).public class GoalsUnityTranslate : TsukimiBehaviour{ public float travel = 4f; public float speed = 1f; private float bottom; private float direction = 1f;
void Start() { bottom = transform.position.y; }
void Update() { // Multiplying by Time.deltaTime (seconds since the previous frame) keeps the speed the same at any frame rate. transform.Translate(Vector3.up * direction * speed * Time.deltaTime, Space.World); float y = transform.position.y; if (y > bottom + travel) direction = -1f; else if (y < bottom) direction = 1f; }}I want to place something at a set position and rotation
transform.SetPositionAndRotation(position, rotation)
Example
using UnityEngine;using Tsukimi;
// Tidy-up button: pressing it returns scattered objects to where and how they were first placed, and stops their motion.//// Setup:// - Put this script on the tidy-up button (it needs a Collider).// - Pass the objects to return (with a Rigidbody) to items.public class GoalsUnityPlace : TsukimiBehaviour{ public Rigidbody[] items; private Vector3[] homePosition; private Quaternion[] homeRotation;
void Start() { homePosition = new Vector3[items.Length]; homeRotation = new Quaternion[items.Length]; for (int i = 0; i < items.Length; i++) { homePosition[i] = items[i].transform.position; homeRotation[i] = items[i].transform.rotation; } }
public override void Interact() { for (int i = 0; i < items.Length; i++) { // Sets position and rotation in one call. items[i].transform.SetPositionAndRotation(homePosition[i], homeRotation[i]); items[i].velocity = Vector3.zero; items[i].angularVelocity = Vector3.zero; } }}Animation
Section titled “Animation”I want to play a door-opening animation
animator.SetTrigger("Open")
Example
using UnityEngine;using Tsukimi;
// Treasure chest: touching it plays the lid-opening animation once.//// Setup:// - Put this script on the chest object (it needs a Collider). Pass the chest's Animator to chest.// - Create a Trigger parameter "Open" in the Animator Controller, and use it to move to the "open" state.public class GoalsUnityAnimatorTrigger : TsukimiBehaviour{ public Animator chest;
public override void Interact() { // A Trigger is set once and is cleared automatically when a transition uses it. chest.SetTrigger("Open"); }}I want to switch between open and closed
animator.SetBool("IsOpen", open)
Example
using UnityEngine;using Tsukimi;
// Automatic door: each touch switches between the open and closed states.//// Setup:// - Put this script on the door button (it needs a Collider). Pass the door's Animator to door.// - Create a Bool parameter "IsOpen" in the Animator Controller, moving to the open state when true and the closed state when false.public class GoalsUnityAnimatorBool : TsukimiBehaviour{ public Animator door; private bool open;
public override void Interact() { open = !open; // A Bool keeps its value until rewritten (unlike a Trigger, it is not cleared automatically). door.SetBool("IsOpen", open); }}Objects
Section titled “Objects”I want to show and hide an object
door.SetActive(!door.activeSelf)
Example
using UnityEngine;using Tsukimi;
// Hidden door: each pull of the bookshelf lever removes the wall or brings it back.//// Setup:// - Put this script on the lever object (it needs a Collider). Pass the wall object to remove to wall.// - The wall appears and disappears only on the screen of the player who touched the lever (it is not synced).public class GoalsUnityActive : TsukimiBehaviour{ public GameObject wall;
public override void Interact() { // Deactivating removes both the look and the collision. activeSelf is the current state. wall.SetActive(!wall.activeSelf); }}I want to make more copies of an object
Instantiate(prefab)
Example
using UnityEngine;using Tsukimi;
// Ball refill: each touch adds one ball to the stand. Stops at 10 so there are not too many.//// Setup:// - Put this script on the refill button (it needs a Collider).// - Pass the ball prefab to ball, and the Transform of where to put it to spawnPoint.public class GoalsUnityInstantiate : TsukimiBehaviour{ public GameObject ball; public Transform spawnPoint; private int count;
public override void Interact() { if (count >= 10) return; count++; GameObject copy = Instantiate(ball); // Position and rotation are set after copying. copy.transform.SetPositionAndRotation(spawnPoint.position, spawnPoint.rotation); }}Note
Instantiate(prefab, position, rotation), which passes position and rotation as arguments, cannot be written. CallSetPositionAndRotationafter copying.
I want to show text or a number
label.text = "count: " + count
Example
using System;using UnityEngine;using Tsukimi;using TMPro;
// Wall clock: shows the viewer's time (hours and minutes), refreshed every second.//// Setup:// - Put this script on the clock object. Pass the TextMeshProUGUI that shows the time to clock.public class GoalsUnityText : TsukimiBehaviour{ public TextMeshProUGUI clock; private float wait;
void Update() { wait -= Time.deltaTime; if (wait > 0f) return; wait = 1f; DateTime now = DateTime.Now; // Putting a string into text shows that text. Minutes are padded to 2 digits. clock.text = now.Hour + ":" + (now.Minute < 10 ? "0" : "") + now.Minute; }}I want to give off sparks or smoke
particles.Emit(12)
Example
using UnityEngine;using Tsukimi;
// Blacksmith's anvil: each hammer strike throws sparks. The harder the strike, the more sparks.//// Setup:// - Put this script on the anvil object (it needs a Collider). Pass the sparks' ParticleSystem to sparks.// - Set the ParticleSystem's Emission Rate to 0 (so it emits only when struck).public class GoalsUnityParticles : TsukimiBehaviour{ public ParticleSystem sparks;
// Called when something hits it. The speed of the hit decides the number of sparks. void OnCollisionEnter(Collision collision) { int count = Mathf.Clamp((int)(collision.relativeVelocity.magnitude * 5f), 3, 40); // Emits a set number of particles on the spot. sparks.Emit(count); }}Every frame
Section titled “Every frame”I want to follow an avatar’s bones at their latest positions
void PostLateUpdate()
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 頭の上の目印: 自分の頭の骨の少し上に、目印のオブジェクトを付いて回らせる。//// Setup:// - このスクリプトは、目印にするオブジェクトに付ける。// - 目印は自分の画面にだけ出る(位置は同期していない)。public class GoalsPostLateUpdate : TsukimiBehaviour{ public float heightAboveHead = 0.3f;
// IK の計算が済んだあと、フレームの終わり近くに呼ばれる。 // Update や LateUpdate で骨の位置を読むと 1 フレーム前の位置になり、動くと目印が遅れて見える。 public override void PostLateUpdate() { VRCPlayerApi me = Networking.LocalPlayer; if (me == null) return; // アバターにその骨が無いときは (0, 0, 0) が返る。 Vector3 head = me.GetBonePosition(HumanBodyBones.Head); if (head == Vector3.zero) return; transform.position = head + Vector3.up * heightAboveHead; }}Reactive
Section titled “Reactive”I want a change to take effect on its own
[Reactive]
Example
using UnityEngine;using Tsukimi;using TMPro;
// Ammo display: each shot lowers the count, and the display and the gun's color catch up on their own.//// Setup:// - Put this script on the object you touch to shoot (it needs a Collider).// - Pass the TextMeshProUGUI that shows the count to ammoText, and the Renderer whose color changes to body.// - The count changes only on the screen of the player who touched it (it is not synced).public class GoalsReactiveField : TsukimiBehaviour{ public TextMeshProUGUI ammoText; public Renderer body;
// A field tracked for changes. Make it private. [Reactive] private int ammo = 6;
// Runs whenever ammo changes. It also runs once at startup, so the display is right from the start. // It is not called from anywhere. [Effect] private void ShowAmmo() { ammoText.text = ammo + " / 6"; body.material.color = ammo > 0 ? Color.white : Color.red; }
// Each touch fires one shot. At 0, it reloads to 6. public override void Interact() { if (ammo > 0) ammo = ammo - 1; else ammo = 6; }}Note
- On a synced field, values that arrive over the network do not run the effect (warning
TUKI0115). - Writing back, inside an effect, to a value that the effect reads is a circular dependency and a compile error.
I want a value derived from other values
[Computed]
Example
using UnityEngine;using Tsukimi;
// Two-key door: the door disappears and lets you through only when both the left and right switches are on.//// Setup:// - Put this script on an empty object that manages the door.// - Pass the door object to door.// - Call ToggleLeft / ToggleRight from the left and right switches// (for example, with SendCustomEvent from each switch's Interact).public class GoalsReactiveComputed : TsukimiBehaviour{ public GameObject door;
[Reactive] private bool left; [Reactive] private bool right;
// A value derived from left and right. It is recalculated when either one changes. [Computed] private bool Open => left && right;
// It reads only Open. If only left is on, Open stays false, so this does not run. [Effect] private void ApplyDoor() { door.SetActive(!Open); }
public void ToggleLeft() { left = !left; } public void ToggleRight() { right = !right; }}I want something to happen only at the moment it changes
[On(nameof(charge))]
Example
using UnityEngine;using Tsukimi;
// Score chime: plays a sound only at the moment the score goes up. Not when it goes down, and not at startup.//// Setup:// - Put this script on the object that counts the score.// - Pass the AudioSource to play to chime.// - Call AddPoint to add a point, and ResetScore to start over.public class GoalsReactiveOn : TsukimiBehaviour{ public AudioSource chime;
[Reactive] private int score;
// Runs at the moment score changes. It does not run at startup. // The argument holds the value from just before the change. [On(nameof(score))] private void OnScoreChanged(int before) { if (score > before) chime.Play(); }
public void AddPoint() { score = score + 1; } public void ResetScore() { score = 0; }}Note
- It does not run at startup. To match the look right from startup, use
[Effect].
I want to write the dependencies myself
[Effect(nameof(width), nameof(height))]
Example
using UnityEngine;using Tsukimi;
// Light dimmer: sets the light again only when the brightness or warmth changes.// Another value on the same Behaviour (the blink count) changing does not touch the light.//// Setup:// - Put this script on the panel that controls the light.// - Pass the Light to set to lamp.// - Call Brighter / Warmer / Blink from the panel's buttons.public class GoalsReactiveEffectDeps : TsukimiBehaviour{ public Light lamp;
[Reactive] private float brightness = 1f; [Reactive] private float warmth; [Reactive] private int blinkCount;
// It reads only the two names in the parentheses. A change to blinkCount does not run it. // It also runs once at startup. [Effect(nameof(brightness), nameof(warmth))] private void ApplyLight() { lamp.intensity = brightness; lamp.color = Color.Lerp(Color.white, new Color(1f, 0.7f, 0.4f), warmth); }
public void Brighter() { brightness = brightness >= 3f ? 0.5f : brightness + 0.5f; } public void Warmer() { warmth = warmth >= 1f ? 0f : warmth + 0.25f; } public void Blink() { blinkCount = blinkCount + 1; }}I want to know how a change is detected
Equals
Example
using UnityEngine;using Tsukimi;
// Marker: even when the same position is written every frame, the marker moves and logs only when the position really changes.//// Setup:// - Put this script on the object that manages the marker.// - Pass the object to follow to target, and the marker to move to marker.public class GoalsReactiveEquals : TsukimiBehaviour{ public Transform target; public Transform marker;
[Reactive] private Vector3 spot;
// Runs only when spot "changes". Vector3 is compared with its typed Equals, so // writing the same value again does not run it, and even a tiny difference counts as a change (it does not swallow error the way == does). [Effect] private void MoveMarker() { marker.position = spot; Debug.Log("Moved the marker to " + spot); }
// Every frame, write the target's position. While the target is still, the effect above does not run. private void Update() { spot = target.position; }}Note
QuaternionandColortreat NaN as equal to NaN. Other value types keep counting as changed once NaN gets in.
Surfaces
Section titled “Surfaces”I want to set the colour of a surface without writing a shader
[Surface] static Color4 M(SurfaceId id, ...)
Example
using UnityEngine;using Tsukimi;
// Health gauge: paints the board green from the left for the fraction that remains, and dark gray for the rest.// No shader file is written. The code that decides the color is written in C#.//// Setup:// - Put this script on the object that manages the gauge.// - Pass the Renderer of the board used as the gauge (a Quad, for example) to gauge.// - Put the remaining fraction (0 to 1) in hp. Call Hit when taking damage.public class GoalsSurfaceColor : TsukimiBehaviour{ public Renderer gauge; public float hp = 1f;
// Called for each pixel of the surface, and the returned color appears on that pixel. id.UV is the position on the surface (0 to 1). [Surface] static Color4 Bar(SurfaceId id, float rest) { if (id.UV.x < rest) return new Color4(0.2f, 0.9f, 0.3f, 1f); return new Color4(0.15f, 0.15f, 0.15f, 1f); }
// Values are passed on every Gpu.Show call. It is called every frame to repaint with the current hp. void Update() { Gpu.Show(nameof(Bar), gauge, hp); }
public void Hit() { hp = Mathf.Max(0f, hp - 0.1f); }}Note
- This chapter is experimental. The way to write it may change.
Gpu.Showuses the values as they were when it was called. After changing a value, call it again.
I want shading that follows the direction the surface faces
Vector3.Dot(id.Normal, toLight)
Example
using UnityEngine;using Tsukimi;
// Toon-style statue: splits how the light falls into 3 steps, and draws with only 3 colors: lit, middle, and shadow.//// Setup:// - Put this script on the statue object. Pass the statue's Renderer to statue.// - baseColor (the color of the lit side) sets the color. The shadow side is painted with the same color darkened.public class GoalsSurfaceShading : TsukimiBehaviour{ public Renderer statue; public Vector3 baseColor = new Vector3(0.8f, 0.75f, 0.7f);
[Surface] static Color4 Toon(SurfaceId id, Vector3 baseColor) { // Choose the light direction yourself (lights placed in the scene cannot be read). Vector3 toLight = new Vector3(0.4f, 1f, 0.3f).normalized; // id.Normal is the direction the surface faces (world space, length 1). The closer it is to the light direction, the closer this is to 1. float lit = Vector3.Dot(id.Normal, toLight); float shade = lit > 0.5f ? 1f : (lit > 0f ? 0.7f : 0.4f); return new Color4(baseColor.x * shade, baseColor.y * shade, baseColor.z * shade, 1f); }
void Update() { Gpu.Show(nameof(Toon), statue, baseColor); }}Note
- Lights placed in the scene cannot be read. Choose the light direction yourself. Shadows are not received either.
I want the look to change with the viewing direction
Vector3.Dot(id.Normal, id.ViewDir)
Example
using UnityEngine;using Tsukimi;
// Ghost rim light: the closer a spot is to the outline as seen by the viewer (where the surface faces sideways), the more it glows pale blue.//// Setup:// - Put this script on the ghost object. Pass the ghost's Renderer to ghost.// - glow sets the strength of the glow (0 means no glow).public class GoalsSurfaceRim : TsukimiBehaviour{ public Renderer ghost; public float glow = 1f;
[Surface] static Color4 Rim(SurfaceId id, float glow) { // id.ViewDir is the direction from that pixel toward the viewer. // Where the surface faces the viewer the dot product is close to 1, and at the outline it is close to 0. float edge = 1f - Mathf.Abs(Vector3.Dot(id.Normal, id.ViewDir)); float t = Mathf.Clamp01(edge * edge * glow); return Color4.Lerp(new Color4(0.1f, 0.1f, 0.15f, 1f), new Color4(0.6f, 0.8f, 1f, 1f), t); }
void Update() { Gpu.Show(nameof(Rim), ghost, glow); }}I want to pass a value into a surface
static Color4 M(SurfaceId id, GpuBuffer2D buf, int n, bool b, Vector2 v, Vector3 w)
Example
using UnityEngine;using Tsukimi;
// Flowing striped floor: the number of stripes, the color, and the speed are passed from the Behaviour, and the stripes flow over time.//// Setup:// - Put this script on the floor object. Pass the floor's Renderer to floor.// - stripes sets the number of stripes, color the color, and speed how fast they flow sideways (in UV units per second).public class GoalsSurfaceArgs : TsukimiBehaviour{ public Renderer floor; public int stripes = 8; public Vector3 color = new Vector3(0.2f, 0.6f, 1f); public float speed = 0.1f;
// Besides float, the arguments can be int, bool, Vector2, Vector3, Vector4, and GpuBuffer2D. [Surface] static Color4 Stripes(SurfaceId id, int stripes, Vector3 color, float offset) { float u = id.UV.x + offset; float band = Mathf.Floor(u * stripes) % 2f; float t = band > 0.5f ? 1f : 0.3f; return new Color4(color.x * t, color.y * t, color.z * t, 1f); }
// Values are passed on every call. Order the Gpu.Show arguments the same as this method's arguments (after id). void Update() { Gpu.Show(nameof(Stripes), floor, stripes, color, speed * Time.time); }}Note
- Pass the values to
Gpu.Showin the same positions as the method’s arguments (afterid).
I want to move the vertices themselves
[Surface] static Vector3 M(VertexId v, ...)
Example
using UnityEngine;using Tsukimi;
// Flag in the wind: makes the flag's vertices wave over time. The side attached to the pole (UV x of 0) does not move.//// Setup:// - Put this script on the flag object. Pass the Renderer of the flag mesh (a finely divided Plane, for example) to flag.// - amplitude sets how far it sways (in mesh units).public class GoalsSurfaceVertex : TsukimiBehaviour{ public Renderer flag; public float amplitude = 0.2f;
// Called for each vertex, and moves the vertex to the returned position. v.Position and the return value are in mesh space. [Surface] static Vector3 Wave(VertexId v, float time, float amplitude) { float sway = Mathf.Sin(time * 3f + v.UV.x * 6f) * amplitude * v.UV.x; return v.Position + new Vector3(0f, sway, 0f); }
// Also write the color side with the same name (the two make one shader). [Surface] static Color4 Wave(SurfaceId id, float time, float amplitude) { return id.UV.y > 0.5f ? new Color4(0.9f, 0.1f, 0.1f, 1f) : new Color4(1f, 1f, 1f, 1f); }
void Update() { Gpu.Show(nameof(Wave), flag, Time.time, amplitude); }}Note
- Even when vertices move, the
id.Normalread on the color side keeps the original direction. - Writing only the position side is an error. Also write the color side with the same name.
World events
Section titled “World events”Contact
Section titled “Contact”I want to react on contact
void OnContactEnter(VRC.Dynamics.ContactEnterInfo contactInfo)
Example
using UnityEngine;using Tsukimi;using VRC.Dynamics;using VRC.SDKBase;
// ハイタッチの的: アバターの手が触れると音が鳴り、触れた人の名前をログに出す。//// Setup:// - このスクリプトは、VRC Contact Receiver を付けたオブジェクトに付ける(同じオブジェクトでないと呼ばれない)。// - Contact Receiver の Collision Tags に、アバターの手が送るタグ(例: Hand)を入れる。// - 鳴らす音を入れた AudioSource を、インスペクタの sound に渡す。public class GoalsContactEnter : TsukimiBehaviour{ public AudioSource sound;
// Contact Sender が、このオブジェクトの Contact Receiver に触れ始めたときに呼ばれる。 // contactInfo.contactSender が触れた側。アバターの Sender なら player に持ち主が入り、 // ワールドに置いた Sender なら player は null。 public override void OnContactEnter(ContactEnterInfo contactInfo) { sound.Play(); VRCPlayerApi who = contactInfo.contactSender.player; if (who != null) Debug.Log(who.displayName + " が触れました"); }}Note
- 届くのは、VRC Contact Receiver と同じオブジェクトに付けたプログラムだけです。
I want to react when a player enters the area
void OnPlayerTriggerEnter(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 入ると点く明かり: 自分がこの範囲に入ったら明かりを点け、出たら消す。//// Setup:// - このスクリプトは、範囲にするオブジェクトに付ける。そのオブジェクトの Collider は Is Trigger を入れる。// - 点ける明かり(Light を持つオブジェクト)を、インスペクタの lamp に渡す。// - 明かりは自分の画面でだけ点く(同期はしていない)。public class GoalsPlayerTriggerEnter : TsukimiBehaviour{ public GameObject lamp;
// プレイヤーがこのトリガーの範囲に入ったときに呼ばれる。ほかの人が入ったときも呼ばれるので、 // 自分のことだけにしたいなら player.isLocal で分ける。 public override void OnPlayerTriggerEnter(VRCPlayerApi player) { if (player.isLocal) lamp.SetActive(true); }
// 範囲から出たときに呼ばれる。 public override void OnPlayerTriggerExit(VRCPlayerApi player) { if (player.isLocal) lamp.SetActive(false); }}Note
- インスタンスにいる誰が入っても呼ばれます。自分だけに反応させるなら
player.isLocalで分けます。
I want to react when a player collides
void OnPlayerCollisionEnter(VRC.SDKBase.VRCPlayerApi player)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 当たると鳴る壁: プレイヤーがこの壁にぶつかったら音を鳴らす。//// Setup:// - このスクリプトは、壁にするオブジェクトに付ける。そのオブジェクトの Collider は Is Trigger を入れない// (入れると、ぶつかる代わりに通り抜け、呼ばれるのは OnPlayerTriggerEnter になる)。// - 鳴らす音を入れた AudioSource を、インスペクタの sound に渡す。public class GoalsPlayerCollisionEnter : TsukimiBehaviour{ public AudioSource sound;
// プレイヤーがこの Collider にぶつかったときに呼ばれる。ほかの人がぶつかったときも呼ばれる。 public override void OnPlayerCollisionEnter(VRCPlayerApi player) { sound.Play(); }}Environment changes
Section titled “Environment changes”I want to react when the language changes
void OnLanguageChanged(string language)
Example
using UnityEngine;using Tsukimi;using TMPro;
// 言葉が切り替わる看板: 見ている人の表示言語が日本語なら日本語で、それ以外なら英語で出す。//// Setup:// - 文字を出す TextMeshProUGUI を、インスペクタの label に渡す。public class GoalsLanguageChanged : TsukimiBehaviour{ public TextMeshProUGUI label;
// 入室したときと、見ている人が表示言語を選び直したときに、その人の画面で呼ばれる。 // language は "en" / "ja" / "zh-CN" のような言語タグ(RFC 5646 の形)。 public override void OnLanguageChanged(string language) { if (language == "ja" || language.StartsWith("ja-")) label.text = "ようこそ"; else label.text = "Welcome"; }}Note
- 入室したときにも 1 回呼ばれます。
Startで言語を読み直す必要はありません。
I want to react when the input method changes
void OnInputMethodChanged(VRC.SDKBase.VRCInputMethod inputMethod)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 操作の案内の出し分け: 画面を触って操作している人にはタッチ用の案内を、それ以外の人には通常の案内を出す。//// Setup:// - タッチ用の案内と通常の案内のオブジェクトを、インスペクタの touchGuide と defaultGuide に渡す。public class GoalsInputMethodChanged : TsukimiBehaviour{ public GameObject touchGuide; public GameObject defaultGuide;
// 見ている人が使う入力の種類(キーボード・マウス・コントローラーなど)が変わったときに、その人の画面で呼ばれる。 public override void OnInputMethodChanged(VRCInputMethod inputMethod) { bool touch = inputMethod == VRCInputMethod.Touch; touchGuide.SetActive(touch); defaultGuide.SetActive(!touch); }}I want to react when the quality settings change
void OnVRCQualitySettingsChanged()
Example
using UnityEngine;using Tsukimi;using VRC.SDK3.Rendering;
// 影の代わり: 見ている人の影の描画距離が短いときだけ、足元に丸い影の板を出す。//// Setup:// - 丸い影の板(地面に置いた半透明の円など)を、インスペクタの blobShadow に渡す。public class GoalsQualitySettingsChanged : TsukimiBehaviour{ public GameObject blobShadow; public float minShadowDistance = 20f;
void Start() { Apply(); }
// 見ている人がグラフィックの設定を変え、VRCQualitySettings の値のどれかが変わったときに呼ばれる。 // 何度も続けて呼ばれることがあるので、中の処理は軽くしておく。 public override void OnVRCQualitySettingsChanged() { Apply(); }
void Apply() { blobShadow.SetActive(VRCQualitySettings.ShadowDistance < minShadowDistance); }}Note
- 設定を変えている間に何度も続けて呼ばれることがあります。中の処理は軽くしておきます。
Drones
Section titled “Drones”I want to react when a drone enters the area
void OnDroneTriggerEnter(VRC.SDKBase.VRCDroneApi drone)
Example
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// ドローンの関門: ドローンがこの輪を通ったら、飛ばしている人の名前を看板に出す。//// Setup:// - このスクリプトは、輪にするオブジェクトに付ける。そのオブジェクトの Collider は Is Trigger を入れる。// - 名前を出す TextMeshProUGUI を、インスペクタの board に渡す。public class GoalsDroneTriggerEnter : TsukimiBehaviour{ public TextMeshProUGUI board;
// ドローンがこのトリガーに入ったときに呼ばれる。GetPlayer で飛ばしている人が取れる。 public override void OnDroneTriggerEnter(VRCDroneApi drone) { VRCPlayerApi pilot = drone.GetPlayer(); if (pilot != null) board.text = pilot.displayName + " が通過"; }}Performance
Section titled “Performance”I want to start writing a kernel
return new Color4(...)
Example
using UnityEngine;using Tsukimi;
// Color-cycling sign: paints the whole board with a color that slowly shifts through red, green, and blue over time (computed on the GPU).//// Setup:// - Put this script on the board used as the sign. Pass the board's Renderer to display.public class GoalsGpuReturn : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D board;
void Start() { board = Gpu.Buffer(64, 64); // 64 cells wide × 64 cells tall }
// A method with [Kernel] runs on the GPU once for each cell. // The returned color is written to that cell (this is the only way to write). [Kernel] static Color4 Paint(KernelId id, float time) { float r = Mathf.Sin(time) * 0.5f + 0.5f; float g = Mathf.Sin(time + 2.1f) * 0.5f + 0.5f; float b = Mathf.Sin(time + 4.2f) * 0.5f + 0.5f; return new Color4(r, g, b, 1f); }
void Update() { Gpu.Run(nameof(Paint), board, Time.time); // run Paint on every cell of board Gpu.Show(board, display); // show the result on the board }}Note
- The only way to write is the kernel’s return value. Assigning to another cell is an error (
CS0200).
I want a short kernel on one line
static Color4 Step(...) => prev[id] * 0.5f
Example
using UnityEngine;using Tsukimi;
// Afterimage: each touch makes the board flash white, then it darkens a little every frame until it fades out.//// Setup:// - Put this script on the board object (it needs a Collider). Pass the board's Renderer to display.public class GoalsGpuExpression : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
// If the body is a single expression, it can be written with =>. Multiplies the previous frame's value by 0.95. [Kernel] static Color4 Fade(KernelId id, GpuBuffer2D prev) => prev[id] * 0.95f;
[Kernel] static Color4 Flash(KernelId id) => Color4.White;
public override void Interact() { Gpu.Run(nameof(Flash), current); }
void Update() { Gpu.Run(nameof(Fade), next, current); // read current and write next Gpu.Swap(ref current, ref next); // next frame, read the one just written Gpu.Show(current, display); }}I want to read the cell this kernel is computing
prev[id]
Example
using UnityEngine;using Tsukimi;
// Fading footprints: each touch brightens the board, then the brightness drops by a fixed amount every frame and stops at 0.//// Setup:// - Put this script on the floor board object (it needs a Collider). Pass the board's Renderer to display.public class GoalsGpuReadSelf : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
[Kernel] static Color4 Decay(KernelId id, GpuBuffer2D prev) { // prev[id] reads the previous frame's value of the cell being computed. float v = Mathf.Max(0f, prev[id].R - 0.01f); return new Color4(v, v, v, 1f); }
[Kernel] static Color4 Light(KernelId id) => Color4.White;
public override void Interact() { Gpu.Run(nameof(Light), current); }
void Update() { Gpu.Run(nameof(Decay), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}Note
- In the default buffer, components are rounded to 8 bits (256 steps). For finer values, create it with
GpuFormat.Halfor pack withGpu.Pack16x2.
I want the components of a colour one at a time
c.R c.G c.B c.A
Example
using UnityEngine;using Tsukimi;
// Black-and-white security camera: shows the camera image as black and white, brightness only.//// Setup:// - Put this script on the monitor board object. Pass the board's Renderer to display.// - Pass the RenderTexture that the security camera (Camera) renders to, to feed.public class GoalsGpuChannels : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D gray;
void Start() { camera = Gpu.Buffer(256, 256); gray = Gpu.Buffer(256, 256); }
[Kernel] static Color4 Gray(KernelId id, GpuBuffer2D src) { Color4 c = src[id]; // c.R, c.G, c.B, and c.A take out one component at a time. Weighted to match the human eye to get the brightness. float y = c.R * 0.299f + c.G * 0.587f + c.B * 0.114f; return new Color4(y, y, y, 1f); }
void Update() { Gpu.Load(camera, feed); // copy the camera image into the buffer Gpu.Run(nameof(Gray), gray, camera); Gpu.Show(gray, display); }}I want to build the colour I return from its components
new Color4(r, g, b, a)
Example
using UnityEngine;using Tsukimi;
// Color-swapping mirror: swaps red and blue in the reflected image, showing it in otherworldly colors.//// Setup:// - Put this script on the mirror board object. Pass the board's Renderer to display.// - Pass the RenderTexture of the camera that the mirror shows, to feed.public class GoalsGpuCompose : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D swapped;
void Start() { camera = Gpu.Buffer(256, 256); swapped = Gpu.Buffer(256, 256); }
[Kernel] static Color4 SwapRedBlue(KernelId id, GpuBuffer2D src) { Color4 c = src[id]; // new Color4(red, green, blue, opacity) builds the returned color from its components. return new Color4(c.B, c.G, c.R, 1f); }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(SwapRedBlue), swapped, camera); Gpu.Show(swapped, display); }}I want to use black or white as they are
Color4.White
Example
using UnityEngine;using Tsukimi;
// Shadow play: shows the camera image in just 2 colors, white where it is bright and black where it is dark.//// Setup:// - Put this script on the screen board object. Pass the board's Renderer to display.// - Pass the RenderTexture of the camera to show to feed, and the brightness (0 to 1) where white turns to black to threshold.public class GoalsGpuConstantColor : TsukimiBehaviour{ public Renderer display; public Texture feed; public float threshold = 0.5f; private GpuBuffer2D camera; private GpuBuffer2D shadow;
void Start() { camera = Gpu.Buffer(256, 256); shadow = Gpu.Buffer(256, 256); }
[Kernel] static Color4 Silhouette(KernelId id, GpuBuffer2D src, float threshold) { Color4 c = src[id]; float y = (c.R + c.G + c.B) / 3f; // Color4.White and Color4.Black return white and black as they are. return y > threshold ? Color4.White : Color4.Black; }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(Silhouette), shadow, camera, threshold); Gpu.Show(shadow, display); }}I want the pattern to change with the position of the cell
id.X id.Y
Example
using UnityEngine;using Tsukimi;
// Checkerboard floor: from each cell's position, builds a pattern where white and gray swap every 8 cells.//// Setup:// - Put this script on the floor board object. Pass the board's Renderer to display.public class GoalsGpuCellPosition : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(64, 64); Gpu.Run(nameof(Checker), floor); // the pattern never changes, so build it once at the start }
[Kernel] static Color4 Checker(KernelId id) { // id.X and id.Y are the horizontal and vertical position of the cell being computed (0, 0 is the bottom left). int tile = (id.X / 8 + id.Y / 8) % 2; return tile == 0 ? Color4.White : new Color4(0.4f, 0.4f, 0.4f, 1f); }
void Update() { Gpu.Show(floor, display); }}I want to look at a neighbouring cell
prev[id.Offset(1, -1)]
Example
using UnityEngine;using Tsukimi;
// Outline drawing: in the camera image, only where brightness differs a lot from the cells above, below, left, and right becomes a white line.//// Setup:// - Put this script on the screen board object. Pass the board's Renderer to display.// - Pass the RenderTexture of the camera to show to feed.public class GoalsGpuNeighbor : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D lines;
void Start() { camera = Gpu.Buffer(256, 256); lines = Gpu.Buffer(256, 256); }
static float Luma(Color4 c) => (c.R + c.G + c.B) / 3f;
[Kernel] static Color4 Edge(KernelId id, GpuBuffer2D src) { // id.Offset(x, y) points at a cell relative to the current cell. float dx = Luma(src[id.Offset(1, 0)]) - Luma(src[id.Offset(-1, 0)]); float dy = Luma(src[id.Offset(0, 1)]) - Luma(src[id.Offset(0, -1)]); float e = Mathf.Clamp01((Mathf.Abs(dx) + Mathf.Abs(dy)) * 4f); return new Color4(e, e, e, 1f); }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(Edge), lines, camera); Gpu.Show(lines, display); }}I want to know what a read outside the buffer gives at the edge
prev[id.Offset(-1000, -1000)]
Example
using UnityEngine;using Tsukimi;
// Paint flowing right: every frame, shifts the whole picture right by 1 cell. The color of the edge cell keeps flowing in at the left edge.//// Setup:// - Put this script on the board object (it needs a Collider). Pass the board's Renderer to display.// - Pass the picture to flow to palette (it is copied once at the start). Touching it restores the first picture.public class GoalsGpuOutside : TsukimiBehaviour{ public Renderer display; public Texture palette; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(128, 64); next = Gpu.Buffer(128, 64); Gpu.Load(current, palette); }
[Kernel] static Color4 Shift(KernelId id, GpuBuffer2D prev) { // Reads the cell to the left. At the left edge (id.X is 0) this points outside the buffer (-1), // but an index outside is clamped to the edge, so the left edge cell's own color comes back. No range check is needed. return prev[id.Offset(-1, 0)]; }
public override void Interact() { Gpu.Load(current, palette); }
void Update() { Gpu.Run(nameof(Shift), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}Note
- To wrap around to the opposite side at the edge, use
Wrap.
I want to tile a small pattern across the buffer
prev.Wrap(id.Offset(1, 0))
Example
using UnityEngine;using Tsukimi;
// Tiled floor: repeats a small 16×16 tile picture across the whole 128×128 floor.//// Setup:// - Put this script on the floor board object. Pass the board's Renderer to display.// - Pass the picture of one tile (16×16) to tileImage.public class GoalsGpuWrap : TsukimiBehaviour{ public Renderer display; public Texture tileImage; private GpuBuffer2D tile; private GpuBuffer2D floor;
void Start() { tile = Gpu.Buffer(16, 16); floor = Gpu.Buffer(128, 128); Gpu.Load(tile, tileImage); Gpu.Run(nameof(Tile), floor, tile); }
[Kernel] static Color4 Tile(KernelId id, GpuBuffer2D tile) { // Reads the small tile at the floor cell's position. Wrap wraps around to the opposite side when pointing outside, // so position (20, 3) becomes (4, 3) on the tile, and the tiles line up repeatedly. return tile.Wrap(id); }
void Update() { Gpu.Show(floor, display); }}Note
- In a buffer 64 wide, pointing at -1 returns cell 63.
I want no square blocks to show when it is scaled up
prev.Smooth(position)
Example
using UnityEngine;using Tsukimi;
// Enlarging a temperature map: stretches a coarse 16×16 temperature map onto a 256×256 board without visible square blocks.//// Setup:// - Put this script on the map board object. Pass the board's Renderer to display.// - Pass the coarse map picture (16×16) to coarse.public class GoalsGpuSmooth : TsukimiBehaviour{ public Renderer display; public Texture coarse; private GpuBuffer2D small; private GpuBuffer2D big;
void Start() { small = Gpu.Buffer(16, 16); big = Gpu.Buffer(256, 256); Gpu.Load(small, coarse); Gpu.Run(nameof(Enlarge), big, small); // the map never changes, so stretch it once at the start }
[Kernel] static Color4 Enlarge(KernelId id, GpuBuffer2D map) { // Smooth returns the value at a position (0 to 1), blended with the surrounding cells. // Converts the larger buffer's cell position to 0 to 1 and points into the smaller map. Vector2 position = new Vector2(id.X / 256f, id.Y / 256f); return map.Smooth(position); }
void Update() { Gpu.Show(big, display); }}Note
- It reads 4 cells, so it costs more than reading 1.
I want a different value in each cell
Gpu.Random01(n)
Example
using UnityEngine;using Tsukimi;
// TV static: paints each cell with a scattered brightness that changes every frame.//// Setup:// - Put this script on the TV screen board object. Pass the board's Renderer to display.public class GoalsGpuRandom : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D screen; private int frame;
void Start() { screen = Gpu.Buffer(128, 96); }
[Kernel] static Color4 Static(KernelId id, int frame) { // Random01 always returns the same value (0 to 1) for the same input. // Passing a number mixed from the cell position and the frame number gives a different value per cell and per frame. float v = Gpu.Random01(Gpu.Hash(Gpu.Hash(id.X, id.Y) + frame)); return new Color4(v, v, v, 1f); }
void Update() { frame = frame + 1; Gpu.Run(nameof(Static), screen, frame); Gpu.Show(screen, display); }}Note
- The same input always returns the same value. To change it every frame, mix the frame number into the input.
I want a smooth pattern
Gpu.Noise(v)
Example
using UnityEngine;using Tsukimi;
// Drifting clouds: builds a mottled cloud pattern from smooth noise and moves it sideways over time.//// Setup:// - Put this script on the sky board object. Pass the board's Renderer to display.public class GoalsGpuNoise : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D sky;
void Start() { sky = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Clouds(KernelId id, float time) { // Noise is smooth noise (0 to 1) where nearby positions give nearby values. float n = Gpu.Noise(new Vector2(id.X * 0.05f + time * 0.3f, id.Y * 0.05f)); float cloud = Mathf.SmoothStep(0.45f, 0.75f, n); return Color4.Lerp(new Color4(0.35f, 0.6f, 0.95f, 1f), Color4.White, cloud); }
void Update() { Gpu.Run(nameof(Clouds), sky, Time.time); Gpu.Show(sky, display); }}I want the size of the destination buffer inside a kernel
Gpu.OutWidth
Example
using UnityEngine;using Tsukimi;
// Sunset gradient: paints a sky that turns from orange to navy from the bottom of the board to the top. It looks the same whatever size the buffer is.//// Setup:// - Put this script on the background board object. Pass the board's Renderer to display.// - width and height set the buffer size.public class GoalsGpuOutSize : TsukimiBehaviour{ public Renderer display; public int width = 64; public int height = 256; private GpuBuffer2D sky;
void Start() { sky = Gpu.Buffer(width, height); Gpu.Run(nameof(Sunset), sky); }
[Kernel] static Color4 Sunset(KernelId id) { // Gpu.OutHeight is the number of cells vertically in the buffer being written. // Using it to convert the position to 0 to 1 means the size does not have to be passed as an argument. float t = id.Y / (float)Gpu.OutHeight; return Color4.Lerp(new Color4(1f, 0.55f, 0.2f, 1f), new Color4(0.05f, 0.05f, 0.25f, 1f), t); }
void Update() { Gpu.Show(sky, display); }}I want two fine-grained values in one cell
Gpu.Pack16x2(v)
Example
using UnityEngine;using Tsukimi;
// Slowly moving particles: keeps each particle's position (x and y) finely packed in one cell and moves it very slightly every frame.// At 8 bits (256 steps), such small moves would be rounded away and the particles would stop.//// Setup:// - Put this script on the object that holds the particles.// - There are 64×64 particles (one cell per particle). To see the positions, show positions or read them back.public class GoalsGpuPack : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D positions; private GpuBuffer2D next;
void Start() { positions = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
[Kernel] static Color4 Drift(KernelId id, GpuBuffer2D prev) { // Unpack16x2 takes out the 2 packed values (65536 steps each). Vector2 p = Gpu.Unpack16x2(prev[id]); p.x = Mathf.Repeat(p.x + 0.0002f, 1f); // 0.0002 to the right per frame // Pack16x2 packs 2 values into one cell for writing (it can only be used inside a kernel). return Gpu.Pack16x2(p); }
void Update() { Gpu.Run(nameof(Drift), next, positions); Gpu.Swap(ref positions, ref next); Gpu.Show(positions, display); }}Note
Gpu.Pack16x2can only be used inside a kernel. It is not used with buffers created withGpuFormat.Half.
I want a colour between two colours
Color4.Lerp(c, c2, h)
Example
using UnityEngine;using Tsukimi;
// Heat map coloring: paints temperature (0 to 1) with colors from cold blue to hot red.//// Setup:// - Put this script on the map board object. Pass the board's Renderer to display.// - Pass the temperature picture (the red component is the temperature) to heatmap.public class GoalsGpuLerp : TsukimiBehaviour{ public Renderer display; public Texture heatmap; private GpuBuffer2D heat; private GpuBuffer2D colored;
void Start() { heat = Gpu.Buffer(128, 128); colored = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Colorize(KernelId id, GpuBuffer2D src) { // Color4.Lerp(a, b, t) is a when t is 0, b when t is 1, and in between it mixes them in that proportion. return Color4.Lerp(new Color4(0.1f, 0.2f, 1f, 1f), new Color4(1f, 0.1f, 0.05f, 1f), src[id].R); }
void Update() { Gpu.Load(heat, heatmap); Gpu.Run(nameof(Colorize), colored, heat); Gpu.Show(colored, display); }}I want to put a repeated calculation in a function outside the kernel
Falloff(d)
Example
using UnityEngine;using Tsukimi;
// Spotlight ring: draws a ring of light that darkens with distance from the board's center, using the same falloff in 2 kernels.// The falloff formula is gathered into one function, called from both kernels.//// Setup:// - Put this script on the floor board object. Pass the board's Renderer to display.// - warm switches the light color (true for warm).public class GoalsGpuHelper : TsukimiBehaviour{ public Renderer display; public bool warm = true; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(128, 128); }
// A helper function called from kernels. It does not get [Kernel]. static float Falloff(float d) { return Mathf.Clamp01(1f - d * d); }
static float DistanceFromCenter(KernelId id) { return Vector2.Distance(new Vector2(id.X, id.Y), new Vector2(64f, 64f)) / 64f; }
[Kernel] static Color4 WarmLight(KernelId id) { float k = Falloff(DistanceFromCenter(id)); return new Color4(k, k * 0.8f, k * 0.5f, 1f); }
[Kernel] static Color4 CoolLight(KernelId id) { float k = Falloff(DistanceFromCenter(id)); return new Color4(k * 0.6f, k * 0.8f, k, 1f); }
void Update() { if (warm) Gpu.Run(nameof(WarmLight), floor); else Gpu.Run(nameof(CoolLight), floor); Gpu.Show(floor, display); }}I want to make a buffer to compute into
Gpu.Buffer(64, 64)
Example
using UnityEngine;using Tsukimi;
// Game of Life: advances generations of live cells (white) and dead cells (black) on 128×128 cells every frame.//// Setup:// - Put this script on the board object (it needs a Collider). Pass the board's Renderer to display.// - Touching it starts over from a random layout.public class GoalsGpuHostBuffer : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next; private int seed;
void Start() { // A buffer for computing on the GPU. Created with the number of cells across and down (4 components per cell). current = Gpu.Buffer(128, 128); next = Gpu.Buffer(128, 128); Reseed(); }
[Kernel] static Color4 Seed(KernelId id, int seed) { return Gpu.Random01(Gpu.Hash(Gpu.Hash(id.X, id.Y) + seed)) < 0.3f ? Color4.White : Color4.Black; }
[Kernel] static Color4 Life(KernelId id, GpuBuffer2D prev) { float n = 0f; for (int dy = -1; dy <= 1; dy++) for (int dx = -1; dx <= 1; dx++) if (dx != 0 || dy != 0) n += prev.Wrap(id.Offset(dx, dy)).R; bool alive = prev[id].R > 0.5f; bool live = n > 2.5f && n < 3.5f || alive && n > 1.5f && n < 2.5f; return live ? Color4.White : Color4.Black; }
private void Reseed() { seed = seed + 1; Gpu.Run(nameof(Seed), current, seed); }
public override void Interact() { Reseed(); }
void Update() { Gpu.Run(nameof(Life), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}I want values outside 0..1, or finer steps
Gpu.Buffer(64, 64, GpuFormat.Half)
Example
using UnityEngine;using Tsukimi;
// Water ripples: each touch drops a droplet in the center and computes the waves that spread and bounce back.// Wave height can go negative, so the buffers are created in a format that can hold values outside 0 to 1.//// Setup:// - Put this script on the water surface board object (it needs a Collider). Pass the board's Renderer to display.public class GoalsGpuHostFormat : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D before; private GpuBuffer2D now; private GpuBuffer2D next; private GpuBuffer2D shown;
void Start() { // Created with GpuFormat.Half, each component is a 16-bit float and can hold negative values and values above 1. // The format must be fixed when written (it cannot be passed in a variable). before = Gpu.Buffer(128, 128, GpuFormat.Half); now = Gpu.Buffer(128, 128, GpuFormat.Half); next = Gpu.Buffer(128, 128, GpuFormat.Half); shown = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Wave(KernelId id, GpuBuffer2D now, GpuBuffer2D before) { float around = (now[id.Offset(1, 0)].R + now[id.Offset(-1, 0)].R + now[id.Offset(0, 1)].R + now[id.Offset(0, -1)].R) * 0.25f; float h = (now[id].R * 2f - before[id].R + (around - now[id].R) * 0.9f) * 0.995f; return new Color4(h, 0f, 0f, 1f); }
[Kernel] static Color4 Drop(KernelId id, GpuBuffer2D now) { float d = Vector2.Distance(new Vector2(id.X, id.Y), new Vector2(64f, 64f)); return new Color4(now[id].R + (d < 3f ? 1f : 0f), 0f, 0f, 1f); }
[Kernel] static Color4 Tint(KernelId id, GpuBuffer2D h) { float v = Mathf.Clamp01(h[id].R * 0.5f + 0.5f); // map -1 to 1 onto 0 to 1 for display return new Color4(v * 0.3f, v * 0.6f, v, 1f); }
public override void Interact() { Gpu.Run(nameof(Drop), next, now); Gpu.Swap(ref now, ref next); }
void Update() { Gpu.Run(nameof(Wave), next, now, before); GpuBuffer2D t = before; before = now; now = next; next = t; Gpu.Run(nameof(Tint), shown, now); Gpu.Show(shown, display); }}Note
- Values above 65504 stay at 65504. No error or warning appears.
- Copying an 8-bit image with
Gpu.Loadcrushes values outside 0 to 1 before they arrive.
I want to send an image, a video or a camera feed to the GPU
Gpu.Load(current, source)
Example
using UnityEngine;using Tsukimi;
// Frosted glass video: passes the video player's image to the GPU, blurs it, then shows it.//// Setup:// - Put this script on the frosted glass board object. Pass the board's Renderer to display.// - Pass the RenderTexture that the video player outputs to, to video.public class GoalsGpuHostLoad : TsukimiBehaviour{ public Renderer display; public Texture video; private GpuBuffer2D frame; private GpuBuffer2D blurred;
void Start() { frame = Gpu.Buffer(128, 72); blurred = Gpu.Buffer(128, 72); }
[Kernel] static Color4 Blur(KernelId id, GpuBuffer2D src) { Color4 sum = Color4.Black; for (int dy = -2; dy <= 2; dy++) for (int dx = -2; dx <= 2; dx++) sum = sum + src[id.Offset(dx, dy)]; return sum * (1f / 25f); }
void Update() { // Copies an image, video, or camera image into the buffer. If the size differs, it is scaled to fit. Gpu.Load(frame, video); Gpu.Run(nameof(Blur), blurred, frame); Gpu.Show(blurred, display); }}Note
- To use an image as numbers rather than colors, turn off sRGB in that image’s import settings.
I want the last frame’s result to be the next frame’s input
Gpu.Swap(ref a, ref b)
Example
using UnityEngine;using Tsukimi;
// Campfire flames: blows heat in at the bottom row, and every frame reads "the previous frame's result" to raise it upward while it cools.//// Setup:// - Put this script on the flame board object. Pass the board's Renderer to display.public class GoalsGpuHostSwap : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next; private GpuBuffer2D shown; private int frame;
void Start() { current = Gpu.Buffer(64, 96); next = Gpu.Buffer(64, 96); shown = Gpu.Buffer(64, 96); }
[Kernel] static Color4 Burn(KernelId id, GpuBuffer2D prev, int frame) { if (id.Y == 0) return new Color4(Gpu.Random01(Gpu.Hash(id.X + frame * 131)), 0f, 0f, 1f); float below = (prev[id.Offset(-1, -1)].R + prev[id.Offset(0, -1)].R * 2f + prev[id.Offset(1, -1)].R) * 0.25f; return new Color4(Mathf.Max(0f, below - 0.012f), 0f, 0f, 1f); }
[Kernel] static Color4 Colorize(KernelId id, GpuBuffer2D heat) { float h = heat[id].R; return new Color4(Mathf.Clamp01(h * 3f), Mathf.Clamp01(h * 3f - 1f), Mathf.Clamp01(h * 3f - 2f), 1f); }
void Update() { frame = frame + 1; Gpu.Run(nameof(Burn), next, current, frame); // Swaps the source and destination. Next frame, read the one just written. Gpu.Swap(ref current, ref next); Gpu.Run(nameof(Colorize), shown, current); Gpu.Show(shown, display); }}I want to run the kernel I wrote, once
Gpu.Run(nameof(Step), next, current)
Example
using UnityEngine;using Tsukimi;
// Photo negative: each touch inverts the photo's light and dark just once (nothing is computed every frame).//// Setup:// - Put this script on the photo board object (it needs a Collider). Pass the board's Renderer to display.// - Pass the photo texture to photo.public class GoalsGpuHostRun : TsukimiBehaviour{ public Renderer display; public Texture photo; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(256, 256); next = Gpu.Buffer(256, 256); Gpu.Load(current, photo); Gpu.Show(current, display); }
[Kernel] static Color4 Invert(KernelId id, GpuBuffer2D src) { Color4 c = src[id]; return new Color4(1f - c.R, 1f - c.G, 1f - c.B, 1f); }
public override void Interact() { // Pass the destination first and then the source. For that one call, Invert runs on every cell. Gpu.Run(nameof(Invert), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}I want to pass a value that changes every frame, such as time, into a kernel
Gpu.Run(nameof(Step), next, current, phase)
Example
using UnityEngine;using Tsukimi;
// Pulsing ring of light: passes time and a radius to the kernel and draws rings that spread from the center and fade.//// Setup:// - Put this script on the floor board object. Pass the board's Renderer to display.// - radius sets the largest radius of the ring (in cells).public class GoalsGpuHostArgs : TsukimiBehaviour{ public Renderer display; public float radius = 60f; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Ring(KernelId id, float time, float radius) { float r = Mathf.Repeat(time, 1f) * radius; float d = Vector2.Distance(new Vector2(id.X, id.Y), new Vector2(64f, 64f)); float k = Mathf.Clamp01(1f - Mathf.Abs(d - r) / 3f) * (1f - r / radius); return new Color4(k * 0.4f, k, k * 0.8f, 1f); }
void Update() { // The values listed after the destination go, in order, to the kernel's second and later arguments. Gpu.Run(nameof(Ring), floor, Time.time, radius); Gpu.Show(floor, display); }}I want to pick the kernel to run by its name as a string
Gpu.Run("Step", next, current)
Example
using UnityEngine;using Tsukimi;
// Switching filters: each button press switches, in order, the filter applied to the camera image.//// Setup:// - Put this script on the switch button (it needs a Collider). Pass the Renderer of the board to show on to display.// - Pass the RenderTexture the camera renders to, to feed.public class GoalsGpuHostString : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D filtered; private int current; // 0: as is 1: sepia 2: night vision
void Start() { camera = Gpu.Buffer(256, 256); filtered = Gpu.Buffer(256, 256); }
[Kernel] static Color4 Plain(KernelId id, GpuBuffer2D src) => src[id];
[Kernel] static Color4 Sepia(KernelId id, GpuBuffer2D src) { float y = (src[id].R + src[id].G + src[id].B) / 3f; return new Color4(y * 1.1f, y * 0.9f, y * 0.7f, 1f); }
[Kernel] static Color4 Night(KernelId id, GpuBuffer2D src) { return new Color4(0f, Mathf.Clamp01(src[id].G * 2f), 0f, 1f); }
public override void Interact() { current = (current + 1) % 3; }
void Update() { Gpu.Load(camera, feed); // A kernel can also be pointed at by a name string. Write the name as a constant string (a name built from a variable or an expression cannot be written). // A misspelled name is rejected at compile time. Written with nameof, it is also caught on the C# side. if (current == 0) Gpu.Run("Plain", filtered, camera); else if (current == 1) Gpu.Run("Sepia", filtered, camera); else Gpu.Run("Night", filtered, camera); Gpu.Show(filtered, display); }}Note
- With
nameof, a misspelling is caught on the C# side. - Write the name as a constant string or with
nameof. A name built from a variable or an expression cannot be written.
I want more than one kernel in the same behaviour
Gpu.Run(nameof(Fade), current, next)
Example
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// Drawing board: while you are in front of the board, draws dots with a pen at your hand's position, and the erase button clears everything.// A kernel that draws and a kernel that erases sit in one Behaviour and are used as needed.//// Setup:// - Put this script on the board object (it needs a Collider; a 1×1 Quad).// - Pass the board's Renderer to display. Touching it clears everything.public class GoalsGpuHostTwo : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next;
void Start() { current = Gpu.Buffer(256, 256); next = Gpu.Buffer(256, 256); Gpu.Run(nameof(Clear), current); }
[Kernel] static Color4 Clear(KernelId id) => Color4.White;
[Kernel] static Color4 Pen(KernelId id, GpuBuffer2D prev, Vector2 tip) { float d = Vector2.Distance(new Vector2(id.X, id.Y), tip); return d < 3f ? Color4.Black : prev[id]; }
public override void Interact() { Gpu.Run(nameof(Clear), current); }
void Update() { // Converts the right index finger's position into a position on the board (cell coordinates). Vector3 hand = Networking.LocalPlayer.GetBonePosition(HumanBodyBones.RightIndexDistal); Vector3 local = transform.InverseTransformPoint(hand); if (Mathf.Abs(local.z) < 0.05f) { Vector2 tip = new Vector2((local.x + 0.5f) * 256f, (local.y + 0.5f) * 256f); Gpu.Run(nameof(Pen), next, current, tip); Gpu.Swap(ref current, ref next); } Gpu.Show(current, display); }}I want to pass many numbers, such as weights, into a kernel
Gpu.Run(nameof(Blend), next, current, weights)
Example
using UnityEngine;using Tsukimi;
// Pixel-art palette: splits the photo's brightness into 16 levels and replaces each with a color from a table (palette).//// Setup:// - Put this script on the frame board object. Pass the board's Renderer to display and the photo to photo.// - palette holds 16 colors (x, y, z are red, green, blue). If its length is not 16, the run does nothing.public class GoalsGpuHostTable : TsukimiBehaviour{ public Renderer display; public Texture photo; public Vector4[] palette = new Vector4[16]; private GpuBuffer2D src; private GpuBuffer2D dotted;
void Start() { src = Gpu.Buffer(64, 64); // read it small to look like pixel art dotted = Gpu.Buffer(64, 64); Gpu.Load(src, photo); }
// For a table argument, write its size like [Capacity(16)] (a power of 2, up to 1024). [Kernel] static Color4 Posterize(KernelId id, GpuBuffer2D src, [Capacity(16)] Vector4[] palette) { float y = (src[id].R + src[id].G + src[id].B) / 3f; Vector4 c = palette[Mathf.Min(15, (int)(y * 16f))]; return new Color4(c.x, c.y, c.z, 1f); }
void Update() { Gpu.Run(nameof(Posterize), dotted, src, palette); Gpu.Show(dotted, display); }}Note
- If the length of the array passed differs from the number written in
[Capacity], the run does nothing. - The total size of the tables one kernel takes is at most 4000.
I want to set the size of the table a kernel takes
[Capacity(4)] Vector4[] w
Example
using UnityEngine;using Tsukimi;
// Changing sky colors: passes just 4 colors — morning, noon, evening, night — in a table and builds the sky color for the time of day.//// Setup:// - Put this script on the sky board object. Pass the board's Renderer to display.// - dayLength sets the length of one day (in seconds).public class GoalsGpuHostSmallTable : TsukimiBehaviour{ public Renderer display; public float dayLength = 120f; private GpuBuffer2D sky; private Vector4[] colors = new Vector4[4];
void Start() { sky = Gpu.Buffer(16, 64); colors[0] = new Vector4(1f, 0.7f, 0.5f, 1f); // morning colors[1] = new Vector4(0.4f, 0.7f, 1f, 1f); // noon colors[2] = new Vector4(1f, 0.45f, 0.2f, 1f); // evening colors[3] = new Vector4(0.05f, 0.05f, 0.2f, 1f); // night }
// A small table of just 4 is passed the same way. [Kernel] static Color4 Sky(KernelId id, float phase, [Capacity(4)] Vector4[] colors) { int a = (int)phase % 4; int b = (a + 1) % 4; Vector4 c = Vector4.Lerp(colors[a], colors[b], phase - Mathf.Floor(phase)); float shade = 0.7f + 0.3f * id.Y / 64f; return new Color4(c.x * shade, c.y * shade, c.z * shade, 1f); }
void Update() { float phase = Mathf.Repeat(Time.time / dayLength, 1f) * 4f; Gpu.Run(nameof(Sky), sky, phase, colors); Gpu.Show(sky, display); }}I want to write a starting state without reading from a buffer
Gpu.Run(nameof(Seed), board, 0.25f)
Example
using UnityEngine;using Tsukimi;
// Initial state of a snowfield: at startup, writes the bumpy snow height just once. No source buffer is needed.//// Setup:// - Put this script on the snowfield board object. Pass the board's Renderer to display.// - bumpiness sets how bumpy it is.public class GoalsGpuHostNoBuffer : TsukimiBehaviour{ public Renderer display; public float bumpiness = 0.3f; private GpuBuffer2D snow;
void Start() { snow = Gpu.Buffer(128, 128); // Runs without a source. The cells handled are decided by the destination (snow). Gpu.Run(nameof(Initial), snow, bumpiness); }
[Kernel] static Color4 Initial(KernelId id, float bumpiness) { float n = Gpu.Noise(new Vector2(id.X * 0.08f, id.Y * 0.08f)); float h = 1f - bumpiness + n * bumpiness; return new Color4(h, h, h, 1f); }
void Update() { Gpu.Show(snow, display); }}I want the sum or the average of every cell
Gpu.Reduce(nameof(Brighter), middle, full)
Example
using UnityEngine;using Tsukimi;
// Automatic brightness: finds the average brightness of the camera image, and brightens it when dark and darkens it when bright.// Adds up the brightness of every cell into one cell, then divides by the number of cells for the average.//// Setup:// - Put this script on the monitor board object. Pass the board's Renderer to display.// - Pass the RenderTexture the camera renders to, to feed.public class GoalsGpuHostReduce : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D middle; private GpuBuffer2D total; private GpuBuffer2D corrected;
void Start() { camera = Gpu.Buffer(64, 64); // The sum goes above 1, so use a format that can hold values outside 0 to 1. middle = Gpu.Buffer(8, 8, GpuFormat.Half); total = Gpu.Buffer(1, 1, GpuFormat.Half); corrected = Gpu.Buffer(64, 64); }
// How to combine 2 values into 1. It gets [Reduce]. Here they are added. [Reduce] static Color4 Sum(Color4 a, Color4 b) { return a + b; }
[Kernel] static Color4 Expose(KernelId id, GpuBuffer2D src, GpuBuffer2D total) { Color4 t = total[new KernelId(0, 0)]; float mean = (t.R + t.G + t.B) / 3f / (64f * 64f); float gain = Mathf.Clamp(0.5f / Mathf.Max(mean, 0.01f), 0.5f, 4f); Color4 c = src[id]; return new Color4(Mathf.Clamp01(c.R * gain), Mathf.Clamp01(c.G * gain), Mathf.Clamp01(c.B * gain), 1f); }
void Update() { Gpu.Load(camera, feed); // Each destination cell gets the combined value of the range of the source that cell covers. // You lay out the stages yourself (64×64 → 8×8 → 1×1). Gpu.Reduce(nameof(Sum), middle, camera); Gpu.Reduce(nameof(Sum), total, middle); Gpu.Run(nameof(Expose), corrected, camera, total); Gpu.Show(corrected, display); }}Note
- You lay out the stages yourself. The number of calls you write is the number of runs.
I want the maximum or minimum of each component
Gpu.Max(peak, full)
Example
using UnityEngine;using Tsukimi;
// Visibility in a dark room: finds the brightest and darkest values in the camera image and stretches the image to fill that range.//// Setup:// - Put this script on the monitor board object. Pass the board's Renderer to display.// - Pass the RenderTexture the camera renders to, to feed.public class GoalsGpuHostMax : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D full; private GpuBuffer2D stretched; private GpuBuffer2D peak; private GpuBuffer2D floor;
void Start() { full = Gpu.Buffer(64, 64); stretched = Gpu.Buffer(64, 64); peak = Gpu.Buffer(1, 1); floor = Gpu.Buffer(1, 1); }
[Kernel] static Color4 Stretch(KernelId id, GpuBuffer2D src, GpuBuffer2D peak, GpuBuffer2D floor) { Color4 hi = peak[new KernelId(0, 0)]; Color4 lo = floor[new KernelId(0, 0)]; float span = Mathf.Max(0.0001f, Mathf.Max(hi.R, Mathf.Max(hi.G, hi.B)) - Mathf.Min(lo.R, Mathf.Min(lo.G, lo.B))); float low = Mathf.Min(lo.R, Mathf.Min(lo.G, lo.B)); Color4 c = src[id]; return new Color4((c.R - low) / span, (c.G - low) / span, (c.B - low) / span, 1f); }
void Update() { Gpu.Load(full, feed); // For each component, gathers the maximum and minimum of all cells into one cell. Use a separate destination buffer for each call. Gpu.Max(peak, full); Gpu.Min(floor, full); Gpu.Run(nameof(Stretch), stretched, full, peak, floor); Gpu.Show(stretched, display); }}Note
- Calling
Gpu.Maxand thenGpu.Minon the same buffer makes the later call overwrite the earlier result.
I want to show the result on an object
Gpu.Show(current, display)
Example
using UnityEngine;using Tsukimi;
// Neon tube with running light: computes a pattern of light that flows along the tube and shows it directly as the tube's look.//// Setup:// - Put this script on the neon tube object. Pass the tube's Renderer to tube.// - The tube mesh's UV must run along the tube's length horizontally (u).public class GoalsGpuHostShow : TsukimiBehaviour{ public Renderer tube; private GpuBuffer2D lights;
void Start() { lights = Gpu.Buffer(256, 4); }
[Kernel] static Color4 Chase(KernelId id, float time) { float k = Mathf.Pow(Mathf.Sin(id.X * 0.1f - time * 6f) * 0.5f + 0.5f, 8f); return new Color4(1f * k + 0.1f, 0.2f * k, 0.8f * k + 0.1f, 1f); }
void Update() { Gpu.Run(nameof(Chase), lights, Time.time); // Shows the computed buffer directly as that Renderer's look. Gpu.Show(lights, tube); }}I want to pass the result to a Unity or VRChat API
Gpu.Texture(current)
Example
using UnityEngine;using Tsukimi;
// Glowing patterned clothes: passes the computed pattern as the emission picture of another object's material.//// Setup:// - Put this script on the object that manages the pattern.// - Pass the Renderer of the object to light up to target. Its material is Standard with Emission turned on.public class GoalsGpuHostTexture : TsukimiBehaviour{ public Renderer target; private GpuBuffer2D pattern;
void Start() { pattern = Gpu.Buffer(64, 64); }
[Kernel] static Color4 Stripes(KernelId id, float time) { float k = Mathf.Sin(id.Y * 0.4f + time * 3f) > 0.6f ? 1f : 0f; return new Color4(0f, k, k, 1f); }
void Update() { Gpu.Run(nameof(Stripes), pattern, Time.time); // Gpu.Texture takes the buffer out as a Unity texture (the buffer itself, not a copy). target.material.SetTexture("_EmissionMap", Gpu.Texture(pattern)); }}Note
- What comes back is the buffer itself, not a copy.
I want to copy the result into my own RenderTexture
VRCGraphics.Blit(Gpu.Texture(current), target)
Example
using UnityEngine;using VRC.SDKBase;using Tsukimi;
// Showing the computed picture in UI: computes a wave pattern and copies it every frame into the RenderTexture shown by a UI RawImage.//// Setup:// - Put this script on the object that manages the pattern.// - Pass the destination RenderTexture to screen. Set the same one as the UI RawImage's Texture.// - Create screen with Color Space set to Linear (sRGB off) and Filter Mode set to Point.public class GoalsGpuHostBlit : TsukimiBehaviour{ public RenderTexture screen; private GpuBuffer2D wave;
void Start() { wave = Gpu.Buffer(128, 128); }
[Kernel] static Color4 Wave(KernelId id, float time) { float v = Mathf.Sin(id.X * 0.1f + time) * Mathf.Cos(id.Y * 0.1f - time) * 0.5f + 0.5f; return new Color4(v, v * 0.5f, 1f - v, 1f); }
void Update() { Gpu.Run(nameof(Wave), wave, Time.time); // Copy into a RenderTexture you prepared yourself. VRCGraphics.Blit(Gpu.Texture(wave), screen); }}Note
- Create the destination without sRGB (
RenderTextureReadWrite.Linear). By default sRGB is on, and the values the kernel returned do not go in as they are.
I want my own RenderTexture to be the destination
public GpuBuffer2D target;
Example
using UnityEngine;using Tsukimi;
// Writing straight into a minimap: uses a RenderTexture you prepared as the destination and writes the kernel's result straight into it.//// Setup:// - Put this script on the object that manages the minimap.// - Plug the minimap's RenderTexture into map in the Inspector (Color Space Linear, Filter Mode Point).// - Pass the terrain height picture to terrain.public class GoalsGpuHostSlot : TsukimiBehaviour{ // A public GpuBuffer2D is a slot for plugging in a RenderTexture you prepared yourself. public GpuBuffer2D map; public Texture terrain; private GpuBuffer2D height;
void Start() { height = Gpu.Buffer(128, 128); Gpu.Load(height, terrain); }
[Kernel] static Color4 Contour(KernelId id, GpuBuffer2D h) { float v = h[id].R; bool line = Mathf.Repeat(v * 10f, 1f) < 0.08f; return line ? Color4.Black : Color4.Lerp(new Color4(0.3f, 0.6f, 0.3f, 1f), Color4.White, v); }
void Update() { Gpu.Run(nameof(Contour), map, height); // write straight into the plugged-in RenderTexture }}Note
- The plugged-in RenderTexture’s settings are used as they are. Turn off sRGB and set Filter Mode to Point.
I want to read the result back as numbers in Udon
VRCAsyncGPUReadback.Request(Gpu.Texture(current), ...)
Example
using UnityEngine;using VRC.SDK3.Rendering;using VRC.Udon.Common.Interfaces;using Tsukimi;using TMPro;
// Color picker: reads the color of the picture's center cell back from the GPU and shows the values as numbers.//// Setup:// - Put this script on the color picker device object.// - Pass the picture to examine to picture, and the TextMeshProUGUI that shows the numbers to readout.public class GoalsGpuHostReadback : TsukimiBehaviour{ public Texture picture; public TextMeshProUGUI readout; private GpuBuffer2D buf; private byte[] cell = new byte[4]; // 1 cell = R, G, B, A, 4 bytes private bool waiting;
void Start() { buf = Gpu.Buffer(64, 64); }
void Update() { // Until the requested answer comes back, do not request the next one (and do not rewrite the buffer). if (waiting) return; Gpu.Load(buf, picture); waiting = true; // Request only the 1 center cell (x = 32, y = 32). VRCAsyncGPUReadback.Request(Gpu.Texture(buf), 0, 32, 1, 32, 1, 0, 1, TextureFormat.RGBA32, (IUdonEventReceiver)this); }
// Called after the GPU finishes its work (it does not return right when requested). public override void OnAsyncGpuReadbackComplete(VRCAsyncGPUReadbackRequest request) { waiting = false; if (request.hasError) return; if (!request.TryGetData(cell, 0)) return; readout.text = "R " + cell[0] + " / G " + cell[1] + " / B " + cell[2]; // arrives as 0 to 255 }}Note
- Leave the requested buffer as it is until the answer comes back. Overwriting it changes what arrives (it is not an error).
- If the receiving array’s size and the requested format do not match, the end of the array stays 0 with no error or warning.
Trained models
Section titled “Trained models”I want to run a trained model on Udon
[Onnx("gesture.onnx")]
I want to run a trained model on the GPU
[OnnxGpu("filter.onnx")]
I want heavy work spread over several frames
async FrameTask M()
Example
using UnityEngine;using Tsukimi;using TMPro;
// Score table sort: sorts the records from highest to lowest. Doing it in one frame makes the world look frozen, so// it moves on to the next frame after each pass, and when it is done it shows the top 3 on the table.//// Setup:// - Put this script on the score table object.// - scores holds the records. Pass the TextMeshProUGUI that shows the table to board.// - Call Rank to start sorting.public class GoalsAsyncHeavySort : TsukimiBehaviour{ public int[] scores; public TextMeshProUGUI board;
// It pauses at await and returns to Udon, and resumes from the next frame. // The caller does not wait for it to finish and moves straight on to its next statement. private async FrameTask SortDescending() { board.text = "Counting…"; for (int i = 0; i < scores.Length - 1; i++) { int best = i; for (int j = i + 1; j < scores.Length; j++) if (scores[j] > scores[best]) best = j; int t = scores[i]; scores[i] = scores[best]; scores[best] = t; await Async.Frame(); // after one pass, leave the rest for the next frame } board.text = "1st " + scores[0] + "\n2nd " + scores[1] + "\n3rd " + scores[2]; }
public void Rank() { SortDescending(); }}Note
- It does not run in parallel. It only moves forward a little at a time across frames.
I want to say how many frames to wait
await Async.Frames(30)
Example
using UnityEngine;using Tsukimi;using TMPro;
// Race start: when touched, counts 3, 2, 1, shows "GO", and opens the gate.//// Setup:// - Put this script on the start button (it needs a Collider).// - Pass the TextMeshProUGUI that shows the numbers to sign, and the gate object to open to gate.public class GoalsAsyncCountdown : TsukimiBehaviour{ public TextMeshProUGUI sign; public GameObject gate;
private async FrameTask Countdown() { gate.SetActive(true); for (int n = 3; n >= 1; n--) { sign.text = n.ToString(); await Async.Frames(60); // wait 60 frames (counted in frames, not seconds) } sign.text = "GO"; gate.SetActive(false); }
public override void Interact() { Countdown(); }}Note
- It can wait only in frames, not in seconds. On the screen of a player with a different frame rate, the wait is a different length.
I want to limit how many run at once
[MaxTasks(4)]
Example
using UnityEngine;using Tsukimi;
// Fireworks: each touch launches one. Up to 4 can be in the sky at the same time.//// Setup:// - Put this script on the launcher object (it needs a Collider).// - Pass 4 Lights for the fireworks to lights (each shot lights one up and fades it out).public class GoalsAsyncMaxTasks : TsukimiBehaviour{ public Light[] lights; private int next;
// Up to 4 runs of the same method can be in progress at once. Each has its own arguments and in-progress values. // Calls made while all 4 are running do not start. [MaxTasks(4)] private async FrameTask Burst(int slot) { Light l = lights[slot]; l.enabled = true; for (int f = 0; f < 30; f++) { l.intensity = 3f * (30 - f) / 30f; // fade out over 30 frames await Async.Frame(); } l.enabled = false; }
public override void Interact() { // If all 4 are still in the air, it does not start. Move on to the next light only when it started. FrameTask t = Burst(next); if (t.Started) next = (next + 1) % lights.Length; }}Note
- The body is copied once per slot, so more slots make the program larger.
I want to know whether it has already started
FrameTask t = M(); if (!t.Started)
Example
using UnityEngine;using Tsukimi;using TMPro;
// A door that does not stack up when mashed: if it is touched while it is still opening or closing, it only shows a message.//// Setup:// - Put this script on the door button (it needs a Collider).// - Pass the door's Transform to door, and the TextMeshProUGUI that shows the message to status.public class GoalsAsyncStarted : TsukimiBehaviour{ public Transform door; public TextMeshProUGUI status; private bool open;
// No count is written, so only 1 run can be in progress at a time. private async FrameTask Swing() { float from = open ? 90f : 0f; float to = open ? 0f : 90f; for (int f = 1; f <= 45; f++) { door.localRotation = Quaternion.Euler(0f, from + (to - from) * f / 45f, 0f); await Async.Frame(); } open = !open; status.text = ""; }
public override void Interact() { FrameTask t = Swing(); // While the previous movement has not finished, it does not start and Started is false. if (!t.Started) status.text = "The door is still moving"; }}Note
- A call made while the previous run has not finished does not start, and
Startedis false.
I want to split a long loop across frames
if (i % 256 == 255) await Async.Frame();
Example
using UnityEngine;using Tsukimi;using TMPro;
// Territory count: counts a 64×64 board, moving on to the next frame every 256 cells.// Counting it all in one frame makes the world look frozen for that time.//// Setup:// - Put this script on the object that holds the board.// - cells holds each cell's owner (0 is empty, 1 is red, 2 is blue). Its length is 4096.// - Pass the TextMeshProUGUI that shows the result to result. Call Tally to start counting.public class GoalsAsyncLoopSplit : TsukimiBehaviour{ public int[] cells = new int[4096]; public TextMeshProUGUI result;
private async FrameTask Count() { int red = 0; int blue = 0; for (int i = 0; i < cells.Length; i++) { if (cells[i] == 1) red++; else if (cells[i] == 2) blue++; // Pause every 256 iterations. i and the running counts keep their values across the pause. if (i % 256 == 255) await Async.Frame(); } result.text = "Red " + red + " / Blue " + blue; }
public void Tally() { Count(); }}I want to pass a value into a method that spans frames
async FrameTask Fade(int frames, int target)
Example
using UnityEngine;using Tsukimi;
// BGM volume fade: turns the volume up when you enter the area and down when you leave. The length and target volume are passed as arguments.//// Setup:// - Put this script on the same object as the Collider (Is Trigger) of the area where the BGM plays.// - Pass the AudioSource that is playing to bgm.public class GoalsAsyncArgs : TsukimiBehaviour{ public AudioSource bgm; private int latest;
// The arguments id, frames, and target still read as the values they were called with after await. // If you enter and leave right away, 2 runs are in progress at once, so only the newer one moves the volume. [MaxTasks(2)] private async FrameTask Fade(int id, int frames, float target) { float start = bgm.volume; for (int f = 1; f <= frames; f++) { if (id != latest) return; // give way to a fade called later bgm.volume = start + (target - start) * f / frames; await Async.Frame(); } }
// When you enter or leave this area, change the volume on your own screen. public override void OnPlayerTriggerEnter(VRC.SDKBase.VRCPlayerApi player) { if (!player.isLocal) return; latest++; Fade(latest, 60, 1f); }
public override void OnPlayerTriggerExit(VRC.SDKBase.VRCPlayerApi player) { if (!player.isLocal) return; latest++; Fade(latest, 120, 0f); }}I want to wait for another method that spans frames
await Fade(30)
Example
using UnityEngine;using Tsukimi;
// Fade to switch rooms: when touched, turns the light all the way down, switches rooms, then brings the light back.//// Setup:// - Put this script on the switch button (it needs a Collider).// - Pass the Light for the room to roomLight, and the two room objects to switch to dayRoom and nightRoom.// - The switch happens only on the screen of the player who touched it (it is not synced).public class GoalsAsyncAwaitOther : TsukimiBehaviour{ public Light roomLight; public GameObject dayRoom; public GameObject nightRoom;
private async FrameTask Fade(float from, float to) { for (int f = 1; f <= 30; f++) { roomLight.intensity = from + (to - from) * f / 30f; await Async.Frame(); } }
// Calling with await waits until the other method finishes before moving on to the next statement. private async FrameTask Switch() { await Fade(1f, 0f); // wait until it is fully dark bool night = !nightRoom.activeSelf; dayRoom.SetActive(!night); nightRoom.SetActive(night); await Fade(0f, 1f); // bring the light back }
public override void Interact() { Switch(); }}I want an event to start work that spans frames
async FrameTask Start()
Example
using UnityEngine;using Tsukimi;
// Opening show: when you enter the world, the corridor lights turn on one at a time from the front.//// Setup:// - Put this script on an empty object that manages the corridor lights.// - Pass the light objects, in the order to turn on, to lamps (leave them all inactive at first).public class GoalsAsyncStart : TsukimiBehaviour{ public GameObject[] lamps;
// Start can span frames. Udon calls Start as usual, // and the paused rest resumes from a later frame. private async FrameTask Start() { foreach (GameObject lamp in lamps) { lamp.SetActive(true); await Async.Frames(15); } }}Note
- If an event called every frame, such as
Update, spans frames, later calls do not start because the previous run has not finished. - Events inherited from the base class, such as
Interact(), cannot span frames because their return type cannot change.
Inlining
Section titled “Inlining”I want the cost of calling a small method gone
[Inline]
Example
using UnityEngine;using Tsukimi;
// Floating platforms: moves many platforms up and down slowly every frame, each at its own phase.// A small calculation called dozens of times every frame is expanded at the call sites to remove the cost of the call.//// Setup:// - Put this script on an empty object that moves the platforms together.// - Pass the Transforms of the platforms to move to platforms.public class GoalsInline : TsukimiBehaviour{ public Transform[] platforms; private Vector3[] home;
void Start() { home = new Vector3[platforms.Length]; for (int i = 0; i < platforms.Length; i++) home[i] = platforms[i].position; }
// A method with this attribute is expanded at its call sites whatever its size (it is not called as a method). // The program grows by what is expanded. Put it on small methods that are called often. [Inline] private float Bob(float time, int index) { return Mathf.Sin(time * 1.5f + index * 0.7f) * 0.25f; }
void Update() { float t = Time.time; for (int i = 0; i < platforms.Length; i++) platforms[i].position = home[i] + new Vector3(0f, Bob(t, i), 0f); }}Note
- A method with this attribute is expanded at its call sites, so the more places call it, the larger the program becomes.
Profiler
Section titled “Profiler”I want to find out what is slow
Tsukimi Profiler
I want to measure how many times it actually ran
Measure
Testing and analysis
Section titled “Testing and analysis”Testing
Section titled “Testing”I want to check for myself that nothing is broken
[TsukimiTest]
Example
using Tsukimi;
// Vending machine: insert coins, and if they reach the price, sell one. Otherwise do nothing.//// Setup:// - Put this script on the vending machine object.// - Call Insert from the coin button and Buy from the buy button.public partial class GoalsTestVending : TsukimiBehaviour{ public int price = 120; public int coins; public int sold;
public void Insert(int amount) { coins = coins + amount; }
public void Buy() { if (coins < price) return; coins = coins - price; sold = sold + 1; }}using Tsukimi;
// Tries the vending machine above without starting Unity. Each test runs on a new instance.public partial class GoalsTestVending{ // A public void method with no arguments and [TsukimiTest] is one test. [TsukimiTest] public void NotEnoughMoneySellsNothing() { Insert(100); Buy(); Assert.AreEqual(0, sold); Assert.AreEqual(100, coins); // the inserted coins stay }
[TsukimiTest] public void EnoughMoneySellsOneAndKeepsTheChange() { Insert(100); Insert(50); Buy(); Assert.AreEqual(1, sold); Assert.AreEqual(30, coins); }}Note
- Start-up events such as
Startdo not run automatically in a test. When you need one, writeStart()in the test body.
I want to know which file to put tests in
Name.Tests.cs
Example
using Tsukimi;
// Code lock door: once 4 digits have been pressed, opens if the number matches. After the fourth digit the input is cleared.//// Setup:// - Put this script on the door's keypad. Call Press(digit) from each key.// - Write the tests in test-keypad.Tests.cs next to it (the same name as this file plus .Tests.cs).// - Both files continue the same class, so both have partial.public partial class GoalsTestKeypad : TsukimiBehaviour{ public int code = 4271; public bool open; private int typed; private int count;
public void Press(int digit) { typed = typed * 10 + digit; count = count + 1; if (count < 4) return; open = typed == code; typed = 0; count = 0; }}using Tsukimi;
// A file named "original file name.Tests.cs" is left out of compilation.// The tests never end up in the world's program.public partial class GoalsTestKeypad{ [TsukimiTest] public void OpensWithTheRightCode() { Press(4); Press(2); Press(7); Press(1); Assert.IsTrue(open); }
[TsukimiTest] public void InputIsClearedAfterFourDigits() { Press(1); Press(1); Press(1); Press(1); Assert.IsFalse(open); // It is the rest of the same class, so private fields can be used directly. Assert.AreEqual(0, typed); Assert.AreEqual(0, count); }}Note
- Tests written in the Behaviour’s own file are included in the program uploaded to the world (warning
TUKI0117).
I want to check that a condition holds
Assert.IsTrue(charge <= 100)
Example
using Tsukimi;
// Charging stand: charges the battery each time it is placed. Stops so it does not go over 100.//// Setup:// - Put this script on the charging stand object. Call Charge(amount) when a battery is placed.public partial class GoalsTestCharge : TsukimiBehaviour{ public int charge;
public void Charge(int amount) { charge = charge + amount; if (charge > 100) charge = 100; }}using Tsukimi;
public partial class GoalsTestCharge{ [TsukimiTest] public void NeverGoesOverTheLimit() { Charge(70); Charge(70); // Check ranges with IsTrue / IsFalse, which take the condition as it is. Assert.IsTrue(charge <= 100); Assert.IsFalse(charge < 0); }}Note
- Only a true or false value appears in the result. To keep what was compared with what, use
Assert.AreEqual.
I want to check that two values are the same
Assert.AreEqual(1, count)
Example
using Tsukimi;
// Combo score: each hit adds 10 points × the combo (up to 3). A miss resets the combo to 0.//// Setup:// - Put this script on the target object. Call Hit on a hit and Miss on a miss.public partial class GoalsTestCombo : TsukimiBehaviour{ public int score; private int combo;
public void Hit() { if (combo < 3) combo = combo + 1; score = score + 10 * combo; }
public void Miss() { combo = 0; }}using Tsukimi;
public partial class GoalsTestCombo{ [TsukimiTest] public void ComboStopsAtThree() { Hit(); Hit(); Hit(); Hit(); // The first argument is the expected value and the second is the actual value. Swapping them swaps the wording of the result. Assert.AreEqual(10 + 20 + 30 + 30, score); }
[TsukimiTest] public void AMissStartsOverFromOne() { Hit(); Hit(); Miss(); Hit(); Assert.AreEqual(10 + 20 + 10, score); }}Note
- Comparing
floatvalues directly also compares the error from each calculation. For real numbers, check that the difference is small withAssert.IsTrue.
I want to check whether a reference is null
Assert.IsNull(current)
Example
using Tsukimi;
// Race winner: keeps only the name of the first player to reach the goal. Later players do not overwrite it.//// Setup:// - Put this script on the goal object. Call Finish(name) when someone reaches the goal.public partial class GoalsTestWinner : TsukimiBehaviour{ public string winner;
public void Finish(string name) { if (winner == null) winner = name; }}using Tsukimi;
public partial class GoalsTestWinner{ [TsukimiTest] public void OnlyTheFirstOneStays() { // Check whether a reference is empty with IsNull / IsNotNull. Assert.IsNull(winner); Finish("Aoi"); Finish("Ren"); Assert.IsNotNull(winner); Assert.AreEqual("Aoi", winner); }}Note
- A destroyed Unity object can be in a state different from C#
null, so the result may not match your intuition.
I want a reason recorded when a check fails
Assert.IsTrue(charge > 0, "使い切っている")
Example
using Tsukimi;
// Tries left: each attempt uses one. At 0 nothing is used and no attempt is possible.//// Setup:// - Put this script on the game's reception object. Call TryPlay to make an attempt.public partial class GoalsTestTries : TsukimiBehaviour{ public int left = 3; public int played;
public void TryPlay() { if (left <= 0) return; left = left - 1; played = played + 1; }}using Tsukimi;
public partial class GoalsTestTries{ [TsukimiTest] public void NoAttemptsAfterUsingThemAllUp() { TryPlay(); TryPlay(); TryPlay(); TryPlay(); // The string at the end appears in the result as it is when the check does not hold. // When comparing several values of the same type, it shows which one was off. Assert.AreEqual(0, left, "tries left should stop at 0"); Assert.AreEqual(3, played, "the fourth attempt should not be allowed"); }}I want to check what a kernel computed
static float NextHeight(float now, ...)
Example
using UnityEngine;using Tsukimi;
// Cooling heat pattern: every frame, each cell's heat is mixed with its neighbors and cools a little (computed on the GPU).// The mixing formula is split out into a static method, called from both the kernel and the test.//// Setup:// - Put this script on the board that shows the pattern. Pass the board's Renderer to display.public partial class GoalsTestKernel : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D heat; private GpuBuffer2D next;
// A static method that takes only float, int, bool, and Vector2 can also be called from a test. public static float Cool(float self, float around) { return Mathf.Max(0f, self * 0.6f + around * 0.4f - 0.01f); }
[Kernel] static Color4 Step(KernelId id, GpuBuffer2D prev) { float around = (prev[id.Offset(1, 0)].R + prev[id.Offset(-1, 0)].R + prev[id.Offset(0, 1)].R + prev[id.Offset(0, -1)].R) * 0.25f; return new Color4(Cool(prev[id].R, around), 0f, 0f, 1f); }
void Start() { heat = Gpu.Buffer(64, 64); next = Gpu.Buffer(64, 64); }
void Update() { Gpu.Run(nameof(Step), next, heat); GpuBuffer2D t = heat; heat = next; next = t; Gpu.Show(heat, display); }}using UnityEngine;using Tsukimi;
// The kernel itself ([Kernel]) cannot be called from a test. Check the formula that was split out.public partial class GoalsTestKernel{ [TsukimiTest] public void HeatMixesWithNeighborsAndCoolsALittle() { // Real numbers carry calculation error, so instead of comparing directly with AreEqual, check that the difference is small. Assert.IsTrue(Mathf.Abs(Cool(1f, 0f) - 0.59f) < 0.0001f); Assert.IsTrue(Mathf.Abs(Cool(0.5f, 0.5f) - 0.49f) < 0.0001f); }
[TsukimiTest] public void DoesNotGoBelowZeroOnceCold() { Assert.AreEqual(0f, Cool(0f, 0f)); }}Note
- Methods with
[Kernel], and methods that takeColor4orKernelId, cannot be called from a test. - If the split-out method is also called from the Behaviour, it is converted to Udon as well, so the instruction count goes up.
I want to check that synced values arrive, on my own
Mimic.Join()
Example
using Tsukimi;using VRC.SDKBase;
// Everyone's best score: when your score beats the current best, become the owner, rewrite it, and send it to everyone.//// Setup:// - Put this script on the score board object. Call Submit(score) after playing.[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public partial class GoalsTestSync : TsukimiBehaviour{ [UdonSynced] public int best;
public void Submit(int score) { if (score <= best) return; Networking.SetOwner(Networking.LocalPlayer, gameObject); best = score; RequestSerialization(); }}using Tsukimi;using VRC.SDKBase;
// Puts 2 players in one test and checks that a sent value reaches the other one.public partial class GoalsTestSync{ [TsukimiTest] public void AnotherPlayersBestArrives() { VRCPlayerApi me = Mimic.Join(); // the first one is yourself VRCPlayerApi other = Mimic.Join(); Mimic.Become(other); // from here on, run on other's screen Submit(50); Mimic.Become(me); Assert.AreEqual(0, best); // sending alone has not delivered it yet Mimic.Deliver(); // delivered to everyone except the sender Assert.AreEqual(50, best); }}Note
Mimicdoes not reproduce network delay or how often values are sent. What it can confirm is the result of running the steps as written.SendCustomNetworkEventis only recorded and does not reach the other player.
I want to check that nothing breaks when the arrival order changes
Mimic.Explore()
Example
using Tsukimi;using VRC.SDKBase;
// Round display: syncs the round number and the time left in that round, and builds the display text when received.//// Setup:// - Put this script on the host object. Call NextRound to move to the next round.[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public partial class GoalsTestExplore : TsukimiBehaviour{ [UdonSynced] public int round; [UdonSynced] public int seconds; public string label = "";
public void NextRound() { Networking.SetOwner(Networking.LocalPlayer, gameObject); round = round + 1; seconds = 60; RequestSerialization(); Show(); }
// Called when received. Both values have already been written. public override void OnDeserialization() { Show(); }
private void Show() { label = "Round " + round + " / " + seconds + "s"; }}using Tsukimi;using VRC.SDKBase;
public partial class GoalsTestExplore{ [TsukimiTest] public void DisplayMatchesInWhateverOrderValuesArrive() { // Write it as the first statement of the test. It swaps every order in which the 2 synced values can be written, // and runs this test once for each order. Mimic.Explore(); VRCPlayerApi me = Mimic.Join(); VRCPlayerApi host = Mimic.Join(); Mimic.Become(host); NextRound(); Mimic.Become(me); Mimic.Deliver(); Assert.AreEqual("Round 1 / 60s", label); }}Note
- Write
Mimic.Explore()as the first statement of the test. It swaps only the places this tool knows about.
I want to test with another behaviour wired up
public Lamp lamp;
I want to catch a test that has grown too expensive
[CostLimit(steps: 544)]
Example
using Tsukimi;
// Board reset: clears the whole 8×8 board. It runs every game, so we want to watch that it has not become heavy.//// Setup:// - Put this script on the object that holds the board. cells has a length of 64.// - Call Clear when starting a new game.public partial class GoalsTestCostLimit : TsukimiBehaviour{ public int[] cells = new int[64]; public int stones;
public void Clear() { for (int i = 0; i < cells.Length; i++) cells[i] = 0; stones = 0; }}using Tsukimi;
public partial class GoalsTestCostLimit{ // If it goes over the limit, this test fails even when every Assert holds. // The number was copied from the steps in the result of one run without a limit. [TsukimiTest] [CostLimit(steps: 1812)] public void ResetHasNotBecomeHeavy() { cells[5] = 2; stones = 1; Clear(); Assert.AreEqual(0, cells[5]); Assert.AreEqual(0, stones); }}Note
- Going over the limit fails the test even when every
Assertholds.
I want to run the tests
Tsukimi Tests
I want the results written to a file
Library/Tsukimi/last-test-run.json
Static analysis
Section titled “Static analysis”I want to ask the machine whether a form compiles, before writing it
spec
I want to ask the machine what an error means
diagnostics
Exporting to U#
Section titled “Exporting to U#”Exporting to U#
Section titled “Exporting to U#”I want to export into a U# project
Export as UdonSharp