やりたいことから探す
やりたいことから、その書き方を説明しているページへ飛ぶための一覧です。
目の前のものを使ったときに反応したい
void Interact()
例
using UnityEngine;using Tsukimi;
// 照明のスイッチ: 触るたびに、部屋の明かりが点いたり消えたりする。//// 前提:// - このスクリプトは、スイッチにするオブジェクトに付ける。// - 同じオブジェクトに Collider が要る(Collider が無いと触れない)。// - 点けたり消したりする Light を、インスペクタの roomLight に渡す。// - 明かりは、触った人の画面でだけ切り替わる(同期はしていない)。public class GoalsInteract : TsukimiBehaviour{ public Light roomLight;
// このオブジェクトを触ったときに、触った人の画面でだけ呼ばれる。 // 触る操作は、デスクトップなら狙って左クリック。 public override void Interact() { roomLight.enabled = !roomLight.enabled; // 点いていれば消し、消えていれば点ける }}注意
- 呼ばれるのは触った人の画面だけです。全員の画面を同じように変えるには同期が必要です。
トリガーの入力を拾いたい
void InputUse(bool value, VRC.Udon.Common.UdonInputEventArgs args)
例
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
// 合図の音: 「使う」ボタンを押した瞬間に、効果音を鳴らす(何も持っていなくても鳴る)。//// 前提:// - このスクリプトは、どこか 1 つのオブジェクトに付ける。// - 鳴らす AudioSource を、インスペクタの signal に渡す。// - 音は、ボタンを押した人の画面でだけ鳴る。public class GoalsInputUse : TsukimiBehaviour{ public AudioSource signal;
// 「使う」ボタンを押したとき(value が true)と、離したとき(value が false)に、押した人の画面で呼ばれる。 // 「使う」ボタンは、デスクトップなら左クリック、VR なら多くはトリガー。 // メニューを開いている間は届かない。 public override void InputUse(bool value, UdonInputEventArgs args) { if (!value) return; // 離したときは何もしない signal.Play(); }}注意
- Use のボタンは、デスクトップでは左クリック、コントローラの多くではトリガーです。
- メニューを開いている間は届きません。開いた時点で「離した」が届き、閉じても「押した」は届きません。
ジャンプを拾いたい
void InputJump(bool value, VRC.Udon.Common.UdonInputEventArgs args)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.Udon.Common;
// 二段ジャンプ: 空中でもう 1 回ジャンプボタンを押すと、もう 1 段跳べる。//// 前提:// - このスクリプトは、どこか 1 つのオブジェクトに付ける。// - 2 段目の跳ぶ強さは、インスペクタの secondJump で変えられる。public class GoalsInputJump : TsukimiBehaviour{ public float secondJump = 4f;
private bool usedSecondJump;
// ジャンプボタンを押したとき(value が true)と離したとき(value が false)に、押した人の画面で呼ばれる。 // ジャンプボタンは、デスクトップならスペース、VR なら多くはコントローラーの面のボタン。 public override void InputJump(bool value, UdonInputEventArgs args) { if (!value) return;
VRCPlayerApi me = Networking.LocalPlayer; if (me.IsPlayerGrounded()) { usedSecondJump = false; // 地面にいるときの 1 段目は、いつものジャンプに任せる return; } if (usedSecondJump) return; // 2 段目は空中で 1 回だけ
// 今の横向きの勢いは残し、上向きの速さだけを入れ直す。 Vector3 v = me.GetVelocity(); me.SetVelocity(new Vector3(v.x, secondJump, v.z)); usedSecondJump = true; }}注意
- メニューを開いている間は届きません。開いた時点で「離した」が届き、閉じても「押した」は届きません。
スティックの倒し具合を読みたい
void InputMoveHorizontal(float value, VRC.Udon.Common.UdonInputEventArgs args)
例
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
// ハンドル: 左右の移動の入力で、乗り物のモデルを左右に向ける。//// 前提:// - このスクリプトは、向きを変える乗り物のオブジェクトに付ける。// - 1 秒に回る角度は、インスペクタの turnSpeed で変えられる。// - 向きは入力した人の画面でだけ変わる(ほかの人にも見せるなら VRC Object Sync などで同期する)。public class GoalsInputMoveHorizontal : TsukimiBehaviour{ public float turnSpeed = 90f;
private float steer; // 最後に届いた左右の入力(-1〜1)
// 左右の移動の入力が届いたときに、入力した人の画面で呼ばれる。value は -1(左)〜 1(右)。 // デスクトップなら A / D キー(-1・0・1 のどれか)、VR ならスティック。 public override void InputMoveHorizontal(float value, UdonInputEventArgs args) { steer = value; // 回すのは Update で、ここでは入力を覚えておくだけ }
void Update() { transform.Rotate(0f, steer * turnSpeed * Time.deltaTime, 0f); }}注意
- 値はおおむね -1〜1 です。デスクトップではキーの入力なので、整数だけが来ます。
- メニューを開いている間は届きません。
視点操作の入力を読みたい
void InputLookHorizontal(float value, VRC.Udon.Common.UdonInputEventArgs args)
例
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
// 見回すカメラの台: 視点を左右に動かす入力で、監視カメラの台を左右に振る。//// 前提:// - このスクリプトは、左右に振る台のオブジェクトに付ける。// - 振れる範囲(左右それぞれの角度)と、1 秒に回る角度をインスペクタで決める。public class GoalsInputLookHorizontal : TsukimiBehaviour{ public float maxAngle = 60f; public float turnSpeed = 45f;
private float look; // 最後に届いた左右の視点の入力(-1〜1) private float angle; // 今の台の向き(正面が 0)
// 視点を左右に動かす入力が届いたときに、入力した人の画面で呼ばれる。value は -1(左)〜 1(右)。 // デスクトップならマウスの左右の動き、VR ならスティック。 public override void InputLookHorizontal(float value, UdonInputEventArgs args) { look = value; }
void Update() { // 振れる範囲を超えないように、角度を挟んでから向ける。 angle = Mathf.Clamp(angle + look * turnSpeed * Time.deltaTime, -maxAngle, maxAngle); transform.localRotation = Quaternion.Euler(0f, angle, 0f); }}注意
- 値はおおむね -1〜1 です。デスクトップでは整数だけが来ます。
- メニューを開いている間は届きません。
キーボードのキーを拾いたい
Input.GetKeyDown(KeyCode.Space)
例
using UnityEngine;using Tsukimi;
// デスクトップの近道キー: F キーで、手元のライトを点けたり消したりする。//// 前提:// - このスクリプトは、どこか 1 つのオブジェクトに付ける。// - 点けたり消したりする Light を、インスペクタの handLight に渡す。// - キーボードの入力なので、VR では届かない(VR の操作は入力のイベントで受け取る)。public class GoalsInputKey : TsukimiBehaviour{ public Light handLight;
// 毎フレーム、F キーが押された瞬間かを見る。 void Update() { if (Input.GetKeyDown(KeyCode.F)) { handLight.enabled = !handLight.enabled; } }}持たれたときに反応したい
void OnPickup()
例
using UnityEngine;using Tsukimi;using VRC.SDK3.Components;
// 懐中電灯: 持ち上げたら明かりが点き、放したら消える。//// 前提:// - このスクリプトは、持ち上げる物(懐中電灯)に付ける。// - 同じオブジェクトに VRC Pickup と Collider と Rigidbody が要る(VRC Pickup は下の属性で付く)。// - 明かりの Light をインスペクタの beam に、スイッチの音の AudioSource を click に渡す。// Light は最初は切っておく。// - 明かりと音は、持ち上げた人の画面でだけ変わる(ほかの人にも見せる形は同期のページにある)。[RequireComponent(typeof(VRCPickup))]public class GoalsOnPickup : TsukimiBehaviour{ public Light beam; public AudioSource click;
// このオブジェクトを持ち上げたプレイヤーの画面でだけ呼ばれる。 // 持ち上げる操作は、デスクトップなら左クリック、VR なら掴むボタン(多くはグリップ)。 public override void OnPickup() { beam.enabled = true; // 明かりを点ける click.Play(); // スイッチの音を鳴らす }
// 持っていたプレイヤーが手を離したときに、その人の画面でだけ呼ばれる。 public override void OnDrop() { beam.enabled = false; // 明かりを消す }}注意
- 呼ばれるのは持った人の画面だけです。
- VRC Pickup を付けたオブジェクトには、Rigidbody と Collider が必要です。
放されたときに反応したい
void OnDrop()
例
using UnityEngine;using Tsukimi;using VRC.SDK3.Components;
// 放してから数秒たつと、最初に置いた場所へ戻る。//// 前提:// - このスクリプトは、持ち上げる物に付ける。// - 同じオブジェクトに VRC Pickup と Collider と Rigidbody が要る(VRC Pickup は下の属性で付く)。// - 戻した位置をほかの人にも見せるなら、同じオブジェクトに VRC Object Sync も付ける。// - 戻るまでの秒数は、インスペクタの returnSeconds で変えられる。[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() { // 使う部品を 1 度だけ取り出し、最初に置いた場所と向きを覚えておく。 pickup = GetComponent<VRCPickup>(); body = GetComponent<Rigidbody>(); homePosition = transform.position; homeRotation = transform.rotation; }
// 持っていたプレイヤーが手を離したときに、その人の画面でだけ呼ばれる。 // 手を離す操作は、デスクトップなら右クリック。 public override void OnDrop() { // returnSeconds 秒あとに ReturnHome を呼ぶ。 SendCustomEventDelayedSeconds(nameof(ReturnHome), returnSeconds); }
public void ReturnHome() { // 待っている間に誰かが持ち直していたら、戻さない。 if (pickup.IsHeld) return;
// 投げたときの勢いを消してから、元の場所と向きへ戻す。 body.velocity = Vector3.zero; body.angularVelocity = Vector3.zero; transform.SetPositionAndRotation(homePosition, homeRotation); }}注意
- 呼ばれるのは放した人の画面だけです。
持っているあいだの操作を拾いたい
void OnPickupUseDown()
例
using UnityEngine;using Tsukimi;using VRC.SDK3.Components;
// 水鉄砲: 持った状態で「使う」ボタンを押している間だけ、水と音が出る。//// 前提:// - このスクリプトは、持ち上げる物(水鉄砲)に付ける。// - 同じオブジェクトに VRC Pickup と Collider と Rigidbody が要る(VRC Pickup は下の属性で付く)。// - VRC Pickup の Auto Hold を入れておく。入れないと、デスクトップでは「使う」ボタンが届かない。// - 水の Particle System をインスペクタの water に、音の AudioSource を sound に渡す。// どちらも Play On Awake は切っておく。// - 水と音は、持っている人の画面でだけ出る(ほかの人にも見せる形は同期のページにある)。[RequireComponent(typeof(VRCPickup))]public class GoalsOnPickupUseDown : TsukimiBehaviour{ public ParticleSystem water; public AudioSource sound;
// このオブジェクトを持っているプレイヤーが「使う」ボタンを押した瞬間に、その人の画面でだけ呼ばれる。 // 「使う」ボタンは、デスクトップなら左クリック、VR ならトリガー。 public override void OnPickupUseDown() { water.Play(); sound.Play(); }
// 「使う」ボタンを離した瞬間に呼ばれる。 public override void OnPickupUseUp() { StopWater(); }
// ボタンを押したまま手を離したときも止める(出しっぱなしにしないため)。 public override void OnDrop() { StopWater(); }
private void StopWater() { water.Stop(); sound.Stop(); }}注意
- デスクトップでは、VRC Pickup の Auto Hold を有効にしないと呼ばれません。
- 呼ばれるのは押した瞬間の 1 回です。離したときは
OnPickupUseUpが呼ばれます。
PhysBone を掴まれたときに反応したい
void OnPhysBoneGrabbed(VRC.Dynamics.PhysBoneGrabbedInfo physBoneInfo)
例
using UnityEngine;using Tsukimi;using VRC.Dynamics;using VRC.SDK3.Dynamics.PhysBone.Components;
// 揺れるロープ: PhysBone を掴んでいる間だけ、きしむ音が鳴る。//// 前提:// - このスクリプトは、VRC Phys Bone を付けたオブジェクト(ワールドに置いたロープや旗)と// 同じオブジェクトに付ける(VRC Phys Bone は下の属性で付く)。// - VRC Phys Bone の Allow Grabbing を入れておく。// - きしむ音の AudioSource をインスペクタの creak に渡す。Loop を入れ、Play On Awake は切っておく。[RequireComponent(typeof(VRCPhysBone))]public class GoalsOnPhysBoneGrabbed : TsukimiBehaviour{ public AudioSource creak;
// この PhysBone をプレイヤーが掴んだときに呼ばれる。 public override void OnPhysBoneGrabbed(PhysBoneGrabbedInfo physBoneInfo) { creak.Play(); }
// 掴んでいた手が離れたときに呼ばれる。 public override void OnPhysBoneReleased(PhysBoneReleasedInfo physBoneInfo) { creak.Stop(); }}注意
- 届くのは、PhysBone と同じオブジェクトに付けたプログラムだけです。
持たれている物を、手から離させたい
pickup.Drop()
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.Components;
// 持ち込み禁止の場所: 物を持ったままこの範囲に入ると、手から離させる。//// 前提:// - このスクリプトは、範囲にする空のオブジェクトに付ける。// - 同じオブジェクトに Collider を付け、Is Trigger を入れておく。// - 手から離させたい物の VRC Pickup を、インスペクタの pickup に渡す。public class GoalsDropOnEnter : TsukimiBehaviour{ public VRCPickup pickup;
// プレイヤーの体がこの範囲に入ったときに呼ばれる。 // 自分以外のプレイヤーが入ったときも、全員の画面で呼ばれる。 public override void OnPlayerTriggerEnter(VRCPlayerApi player) { // 入ってきたのが自分で、しかもその物を自分が持っているときだけ処理する。 if (!player.isLocal) return; if (pickup.currentPlayer != player) return;
pickup.Drop(); // 手から離させる }}プレイヤーとアバター
Section titled “プレイヤーとアバター”誰かが入ってきたときに反応したい
void OnPlayerJoined(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// 入室の案内板: 誰かが入ってきたら、その人の名前と、いまの人数を看板に出す。//// 前提:// - このスクリプトは、看板にする空のオブジェクトに付ける。// - 文字を出す TextMeshProUGUI(UI の Text - TextMeshPro)を、インスペクタの board に渡す。// - 看板の文字は、それぞれの人の画面で書き換わる(同期はしていない)。public class GoalsPlayerJoined : TsukimiBehaviour{ public TextMeshProUGUI board;
// プレイヤーがインスタンスに入ったときに、全員の画面で呼ばれる。 // 自分が入ったときは、先にいた人を含む全員ぶん(自分のぶんも)呼ばれる。 // ほかの人が入ったときは、入ってきたその人のぶんだけ呼ばれる。 public override void OnPlayerJoined(VRCPlayerApi player) { board.text = player.displayName + " が入りました(いま " + VRCPlayerApi.GetPlayerCount() + " 人)"; }}注意
- 自分が入ったときは、そこにいる全員(自分も含む)の分がまとめて呼ばれます。あとから来た人の分は 1 人ずつです。
誰かが出ていったときに反応したい
void OnPlayerLeft(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// 退室の案内板: 誰かが出ていったら、その人の名前を看板に出す。//// 前提:// - このスクリプトは、看板にする空のオブジェクトに付ける。// - 文字を出す TextMeshProUGUI を、インスペクタの board に渡す。public class GoalsPlayerLeft : TsukimiBehaviour{ public TextMeshProUGUI board;
// プレイヤーがインスタンスから出ていったときに呼ばれる。出ていった人が player に入る。 public override void OnPlayerLeft(VRCPlayerApi player) { board.text = player.displayName + " が出ていきました"; }}アバターが変わったときに反応したい
void OnAvatarChanged(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 鏡の高さ合わせ: 自分のアバターが変わったら、鏡の中心を目の高さへ動かす。//// 前提:// - このスクリプトは、どこか 1 つのオブジェクトに付ける。// - 高さを合わせたい鏡(VRC Mirror Reflection を付けたオブジェクト)の Transform を、インスペクタの mirror に渡す。// - 床の高さが 0 のワールドを想定している(目の高さは床からのメートル)。public class GoalsAvatarChanged : TsukimiBehaviour{ public Transform mirror;
// プレイヤーのアバターの読み込みが終わったときに呼ばれる。アバターが変わったプレイヤーが player に入る。 // ほかの人のアバターが変わったときも呼ばれるので、自分のときだけ処理する。 public override void OnAvatarChanged(VRCPlayerApi player) { if (!player.isLocal) return;
// いまのアバターの目の高さ(メートル)へ、鏡の高さだけを合わせる。 float eye = player.GetAvatarEyeHeightAsMeters(); Vector3 p = mirror.position; mirror.position = new Vector3(p.x, eye, p.z); }}注意
- ほかの人の分も、同期されるたびに届きます。自分の分だけを扱うなら
player.isLocalで分けます。
アバターの目の高さが変わったときに反応したい
void OnAvatarEyeHeightChanged(VRC.SDKBase.VRCPlayerApi player, float prevEyeHeightAsMeters)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 身長に合わせた歩く速さ: 自分の目の高さが変わったら、歩く速さと走る速さを身長に比例させる。//// 前提:// - このスクリプトは、どこか 1 つのオブジェクトに付ける。// - 基準にする目の高さと、そのときの速さを、インスペクタで決める。public class GoalsAvatarEyeHeightChanged : TsukimiBehaviour{ public float baseEyeHeight = 1.6f; // この目の高さ(メートル)のときに、下の速さになる public float baseWalkSpeed = 2f; public float baseRunSpeed = 4f;
// プレイヤーの目の高さが変わったときに呼ばれる(アバターを替えたとき・アバターの大きさを変えたとき)。 // prevEyeHeightAsMeters には、変わる前の目の高さが入る。 // ほかの人のぶんも、その人の新しい目の高さが届くたびに呼ばれるので、自分のときだけ処理する。 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); }}注意
- ほかの人の分も、同期されるたびに届きます。自分の分だけを扱うなら
player.isLocalで分けます。
ステーションに座ったときに反応したい
void OnStationEntered(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 運転席の案内: 自分が座ったら操作の案内を出し、立ったら消す。//// 前提:// - このスクリプトは、座る場所(VRC Station を付けたオブジェクト)に付ける(VRC Station は下の属性で付く)。// - 同じオブジェクトに Collider も要る(多くは Is Trigger を入れる)。触ると下の Interact が呼ばれて座る。// - 操作の案内(UI など)のオブジェクトを、インスペクタの guide に渡す。最初は非表示にしておく。[RequireComponent(typeof(VRC.SDK3.Components.VRCStation))]public class GoalsStationEntered : TsukimiBehaviour{ public GameObject guide;
// このオブジェクトを触ったときに、触った人の画面で呼ばれる。 // ステーションは触るだけでは座らないので、ここで自分をこのステーションに座らせる。 public override void Interact() { Networking.LocalPlayer.UseAttachedStation(); }
// このステーションにプレイヤーが座ったときに呼ばれる。座ったプレイヤーが player に入る。 // 案内は座った本人にだけ出したいので、座ったのが自分のときだけ表示する。 public override void OnStationEntered(VRCPlayerApi player) { if (player.isLocal) guide.SetActive(true); }
// このステーションからプレイヤーが立ったときに呼ばれる。 public override void OnStationExited(VRCPlayerApi player) { if (player.isLocal) guide.SetActive(false); }}注意
- ステーションは触っただけでは座りません。例のように
InteractでUseAttachedStation()を呼びます。
プレイヤーがリスポーンしたときに反応したい
void OnPlayerRespawn(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// やり直し: 自分がリスポーンしたら、自分の得点を 0 に戻す。//// 前提:// - このスクリプトは、得点を数えるオブジェクトに付ける。// - 得点を出す TextMeshProUGUI を、インスペクタの board に渡す。// - 得点はそれぞれの人の画面にだけある(同期はしていない)。public class GoalsPlayerRespawn : TsukimiBehaviour{ public TextMeshProUGUI board; private int score;
// プレイヤーがリスポーンしたときに呼ばれる(メニューの Respawn を押したとき)。 // リスポーンしたプレイヤーが player に入る。自分のときだけ得点を戻す。 public override void OnPlayerRespawn(VRCPlayerApi player) { if (!player.isLocal) return;
score = 0; board.text = "得点: " + score; }
// 得点を 1 増やす(ほかのスクリプトから SendCustomEvent で呼ぶ)。 public void AddPoint() { score++; board.text = "得点: " + score; }}同期とネットワーク
Section titled “同期とネットワーク”値を全員に同期したい
[UdonSynced] private int value;
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// みんなで数えるカウンター: 誰かが触ると 1 増え、全員の画面に同じ数が出る。//// 前提:// - このスクリプトは、触る対象(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(); }}オブジェクトのオーナーになりたい
Networking.SetOwner(Networking.LocalPlayer, gameObject)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// 持ち主の札: 触った人がこのオブジェクトのオーナーになり、札に今のオーナーの名前が出る。//// 前提:// - このスクリプトは、触る対象(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; }}送るきっかけを自分で出すか、自動で送るかを選びたい
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 回る看板: オーナーの画面で回した角度を、全員の画面に自動で送り続ける。//// 前提:// - このスクリプトは、回したいオブジェクトに付ける。// - 送り方は 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); }}送りすぎていないかの目安を知りたい
[NetworkCallable(100)]
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.UdonNetworkCalling;using VRC.Udon.Common.Interfaces;
// 拍手のボタン: 触ると全員の画面で拍手の音が鳴る。連打しても、1 秒に送る回数は上限までに抑えられる。//// 前提:// - このスクリプトは、触る対象(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(); }}注意
- 上限を超えて送った分は捨てられず、送り手の側で待たされてから順に送られます。
- 引数の無い
publicメソッドは[NetworkCallable]が無くても呼べますが、これは古い書き方との互換のためで、VRChat は勧めていません。
ほかの Behaviour の処理を呼びたい
other.SendCustomEvent("Ping")
例
using UnityEngine;using Tsukimi;using VRC.Udon;
// 呼び鈴: 触ると、別のオブジェクトに付いた Behaviour の Ring を呼ぶ。//// 前提:// - このスクリプトは、触る対象(Collider の付いたオブジェクト)に付ける。// - 呼ばれる側の Behaviour(public void Ring() を持つもの)を、インスペクタの bell に渡す。// - 呼ばれる側の型が決まっているなら、フィールドをその型で宣言して bell.Ring() と書くほうが、// 綴りの間違いがコンパイルで見つかる。名前で呼ぶのは、相手の型を決めずに差し替えたいときの形。public class GoalsCallOther : TsukimiBehaviour{ public UdonBehaviour bell;
// 触った人の画面で呼ばれる。相手の処理を名前で呼ぶ(自分の画面の中だけで走る)。 public override void Interact() { bell.SendCustomEvent("Ring"); }}みんなの側で同じ処理を呼びたい
SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ping))
例
using UnityEngine;using Tsukimi;using VRC.SDK3.UdonNetworkCalling;using VRC.Udon.Common.Interfaces;
// 鐘: 誰かが触ると、そのインスタンスにいる全員の画面で鐘が鳴る。//// 前提:// - このスクリプトは、触る対象(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(); }}注意
- 名前が
_で始まるメソッドは、ネットワークからは呼べません。ほかの人から呼ばれたくない処理はその名前にします。
少し遅らせて呼びたい
SendCustomEventDelayedSeconds(nameof(Ping), 1.5f)
例
using UnityEngine;using Tsukimi;
// 自動で閉まる扉: 触ると開き、3 秒たつと閉まる。//// 前提:// - このスクリプトは、触る対象(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); }}何フレームか後に呼びたい
SendCustomEventDelayedFrames(nameof(Ping), 10)
例
using UnityEngine;using Tsukimi;
// 押した合図: ボタンを触ると、10 フレームのあいだだけランプを点ける。//// 前提:// - このスクリプトは、触る対象(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); }}ほかの Behaviour の値を読みたい
(int)other.GetProgramVariable("count")
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.Udon;
// 点数の表示板: 別のオブジェクトに付いた Behaviour の score を読んで、毎フレーム表示する。//// 前提:// - 点数を持つ側の 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; }}ほかの Behaviour の値を書きたい
other.SetProgramVariable("count", 5)
例
using UnityEngine;using Tsukimi;using VRC.Udon;
// 難易度のボタン: 触ると、別のオブジェクトに付いた Behaviour の level を 3 にする。//// 前提:// - このスクリプトは、触る対象(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"); }}その値の型を知りたい
other.GetProgramVariableType("count")
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.Udon;
// 値の中身を見る板: 相手の Behaviour の value の型を確かめてから、型に合わせて表示する。//// 前提:// - 見たい側の 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 + " です"; } }}音・動画・外部のデータ
Section titled “音・動画・外部のデータ”触ったら音を鳴らしたい
source.Play()
例
using UnityEngine;using Tsukimi;
// 呼び鈴: 触ると鳴る。鳴っている間にもう一度触っても、最初から鳴らし直さない。//// 前提:// - このスクリプトは、呼び鈴にするオブジェクトに付ける。// - 同じオブジェクトに Collider が要る(Collider が無いと触れない)。// - 鳴らす AudioSource を、インスペクタの bell に渡す(鳴らす音は AudioSource の AudioClip に入れておく)。// - 音は、触った人の画面でだけ鳴る(全員に聞かせる形は同期のページにある)。public class GoalsAudioPlay : TsukimiBehaviour{ public AudioSource bell;
// このオブジェクトを触ったときに、触った人の画面でだけ呼ばれる。 public override void Interact() { if (bell.isPlaying) return; // 鳴っている間は鳴らし直さない bell.Play(); }}鳴っている音を止めずに、効果音を重ねたい
source.PlayOneShot(clip, 0.7f)
例
using UnityEngine;using Tsukimi;
// コインの音: 触るたびに音を重ねて鳴らす。連打しても前の音は途切れない。//// 前提:// - このスクリプトは、コインにするオブジェクトに付ける。// - 同じオブジェクトに Collider が要る。// - 鳴らす AudioSource をインスペクタの source に、鳴らす音を coin に渡す。// - 音は、触った人の画面でだけ鳴る。public class GoalsAudioOneShot : TsukimiBehaviour{ public AudioSource source; public AudioClip coin;
// このオブジェクトを触ったときに、触った人の画面でだけ呼ばれる。 public override void Interact() { // PlayOneShot は、鳴っている音を止めずに重ねて鳴らす。 // 毎回同じに聞こえないように、音量を 0.8〜1 の間で少し揺らす。 source.PlayOneShot(coin, Random.Range(0.8f, 1f)); }}再生が始まったときに反応したい
void OnVideoStart()
例
using UnityEngine;using Tsukimi;
// 上映の始まり: 動画が再生され始めたら、「読み込み中」の表示を消して、客席の照明を落とす。//// 前提:// - このスクリプトは、動画プレイヤー(VRC Unity Video Player か VRC AVPro Video Player)と同じオブジェクトに付ける。// 動画のイベントは、同じオブジェクトの動画プレイヤーからしか届かない。// - 「読み込み中」の表示をインスペクタの loading に、落とす照明の Light を houseLight に渡す。public class GoalsVideoStart : TsukimiBehaviour{ public GameObject loading; public Light houseLight;
// このオブジェクトの動画プレイヤーが、止まっている状態から再生を始めたときに呼ばれる。 public override void OnVideoStart() { loading.SetActive(false); houseLight.enabled = false; }}注意
- 届くのは、同じオブジェクトの動画プレイヤーからだけです。
再生が終わったときに反応したい
void OnVideoEnd()
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.Video.Components.Base;
// 連続再生: 動画が終わったら、次の動画を読み込んで再生する。最後まで行ったら最初に戻る。//// 前提:// - このスクリプトは、動画プレイヤー(VRC Unity Video Player か VRC AVPro Video Player)と同じオブジェクトに付ける。// - 同じ動画プレイヤーをインスペクタの player に渡し、流す動画の URL を playlist に並べる。// - 新しい URL を読み込めるのは、1 人あたり 5 秒に 1 回まで(すべての動画プレイヤーで合わせて)。// - ここでは読み込みを、それぞれの人の画面で行っている(全員で同じ動画にそろえる形は同期のページにある)。public class GoalsVideoEnd : TsukimiBehaviour{ public BaseVRCVideoPlayer player; public VRCUrl[] playlist;
private int current;
// このオブジェクトの動画プレイヤーの再生が終わったときに呼ばれる(最後まで再生したときと、止めたとき)。 public override void OnVideoEnd() { current = (current + 1) % playlist.Length; // 次の番号へ。最後の次は 0 に戻る player.PlayURL(playlist[current]); }}注意
- 届くのは、同じオブジェクトの動画プレイヤーからだけです。
- 最後まで再生したときのほか、操作で止めたときにも届きます。
再生に失敗したときに反応したい
void OnVideoError(VRC.SDK3.Components.Video.VideoError videoError)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDK3.Components.Video;
// 再生の失敗の案内: 動画を読み込めなかったら、理由に合わせた案内を画面に出す。//// 前提:// - このスクリプトは、動画プレイヤー(VRC Unity Video Player か VRC AVPro Video Player)と同じオブジェクトに付ける。// - 案内を出す TextMeshProUGUI を、インスペクタの status に渡す。public class GoalsVideoError : TsukimiBehaviour{ public TextMeshProUGUI status;
// このオブジェクトの動画プレイヤーが、動画の読み込みで失敗したときに呼ばれる。videoError に理由が入る。 public override void OnVideoError(VideoError videoError) { if (videoError == VideoError.RateLimited) status.text = "読み込みが早すぎます。5 秒ほど待ってから、もう一度試してください"; else if (videoError == VideoError.InvalidURL) status.text = "URL が正しくありません"; else if (videoError == VideoError.AccessDenied) status.text = "この URL は読み込めません"; else status.text = "動画を読み込めませんでした"; }}注意
- 届くのは、同じオブジェクトの動画プレイヤーからだけです。
- 失敗してすぐ読み直すときも、URL の読み込みは 1 人 5 秒に 1 回までです。
外から取ってきた画像を受け取りたい
void OnImageLoadSuccess(VRC.SDK3.Image.IVRCImageDownload result)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.SDK3.Image;using VRC.Udon.Common.Interfaces;
// URL のポスター: ワールドに入ったら、URL の画像を読み込んでポスターに貼り、「読み込み中」の表示を消す。//// 前提:// - このスクリプトは、ポスターの近くのどこか 1 つのオブジェクトに付ける。// - 画像の URL をインスペクタの imageUrl に、貼る先の Material を poster に、「読み込み中」の表示を loading に渡す。// - 読み込める画像は 2048×2048 まで。読み込みは 5 秒に 1 枚までで、超えた分は順番待ちになる。// - 決められたサイト以外の URL は、見る人が設定で「Allow Untrusted URLs」を入れていないと読み込めない。public class GoalsImageLoadSuccess : TsukimiBehaviour{ public VRCUrl imageUrl; public Material poster; public GameObject loading;
private VRCImageDownloader downloader;
void Start() { // 読み込み役はフィールドに持っておく(持っていないと途中で片付けられることがある)。 downloader = new VRCImageDownloader(); // 読み込めたら poster の主テクスチャに自動で貼られ、結果のイベントがこのスクリプトに届く。 downloader.DownloadImage(imageUrl, poster, (IUdonEventReceiver)this, null); }
// 画像を読み込めたときに呼ばれる。result.Result に読み込んだ Texture2D が入る。 public override void OnImageLoadSuccess(IVRCImageDownload result) { loading.SetActive(false); }
void OnDestroy() { downloader.Dispose(); // 読み込んだ画像のメモリを返す }}注意
- 画像は 2048×2048 までです。読み込みは 5 秒に 1 枚で、超えた分は順番待ちになります。
- 決められたサイト以外の URL は、見る人が「Allow Untrusted URLs」を有効にしていないと読み込めません。
- 読み込み役はフィールドに持ち、使い終えたら
Dispose()します。
外から取ってきた文字を受け取りたい
void OnStringLoadSuccess(VRC.SDK3.StringLoading.IVRCStringDownload result)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.StringLoading;using VRC.Udon.Common.Interfaces;
// お知らせ板: ワールドに入ったら、URL に置いた文章を読み込んで看板に出す(ワールドを上げ直さずに内容を変えられる)。//// 前提:// - このスクリプトは、看板にするオブジェクトに付ける。// - 文章を置いた URL をインスペクタの noticeUrl に、文字を出す TextMeshProUGUI を board に渡す。// - 読み込みは 5 秒に 1 本まで。決められたサイト(GitHub Pages・Gist・Pastebin など)以外は、// 見る人が設定で「Allow Untrusted URLs」を入れていないと読み込めない。public class GoalsStringLoadSuccess : TsukimiBehaviour{ public VRCUrl noticeUrl; public TextMeshProUGUI board;
void Start() { // 結果のイベントは、2 つ目の引数に渡したスクリプト(ここでは自分)に届く。 VRCStringDownloader.LoadUrl(noticeUrl, (IUdonEventReceiver)this); }
// 文章を読み込めたときに呼ばれる。result.Result に、UTF-8 として読んだ文字列が入る。 public override void OnStringLoadSuccess(IVRCStringDownload result) { board.text = result.Result; }}注意
- 読み込みは 5 秒に 1 本で、超えた分は順番待ちになります。
- 決められたサイト以外の URL は、見る人が「Allow Untrusted URLs」を有効にしていないと読み込めません。
取ってこられなかったときに反応したい
void OnImageLoadError(VRC.SDK3.Image.IVRCImageDownload result)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.Image;using VRC.Udon.Common.Interfaces;
// 読み込めなかったときの代わりの絵: URL の画像を読み込めなかったら、用意しておいた画像を貼り、理由を出す。//// 前提:// - このスクリプトは、ポスターの近くのどこか 1 つのオブジェクトに付ける。// - 画像の URL を imageUrl に、貼る先の Material を poster に、代わりの画像を fallback に、// 理由を出す TextMeshProUGUI を status に、インスペクタで渡す。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); }
// 画像を読み込めなかったときに呼ばれる。result.ErrorMessage に理由が入る。 public override void OnImageLoadError(IVRCImageDownload result) { poster.mainTexture = fallback; status.text = "画像を読み込めませんでした: " + result.ErrorMessage; }
void OnDestroy() { downloader.Dispose(); }}鍵盤が押されたときに反応したい
void MidiNoteOn(int channel, int number, int velocity)
例
using UnityEngine;using Tsukimi;
// 鍵盤で光る照明: 鍵盤を押すと、音の高さで色が、押した強さで明るさが変わる。//// 前提:// - シーンに VRC Midi Listener を置き、その Behaviour にこのスクリプトを付けたオブジェクトを指定する。// Active Events で Note On を入れておく(最初はどのイベントも入っていない)。// - 光らせる Light を、インスペクタの stageLight に渡す。// - 使われる MIDI 機器は、見つかった最初の 1 台(起動の引数 --midi=機器の名前 で選べる)。public class GoalsMidiNoteOn : TsukimiBehaviour{ public Light stageLight; public float maxIntensity = 3f;
// MIDI の Note On を受け取ったときに呼ばれる(鍵盤やボタンを押したとき・MIDI の再生)。 // channel は 0〜15、number は音の番号 0〜127(60 が真ん中のド)、velocity は押した強さ 0〜127。 public override void MidiNoteOn(int channel, int number, int velocity) { stageLight.color = Color.HSVToRGB(number / 127f, 1f, 1f); // 音の高さで色相を回す stageLight.intensity = velocity / 127f * maxIntensity; // 強く押すほど明るい }}注意
- VRC Midi Listener の Active Events は、最初はすべて切れています。使うイベントにチェックを入れます。
鍵盤が離されたときに反応したい
void MidiNoteOff(int channel, int number, int velocity)
例
using UnityEngine;using Tsukimi;
// 鍵盤を離したら消える照明: 押している間だけ点き、離すと消える。//// 前提:// - シーンに VRC Midi Listener を置き、その Behaviour にこのスクリプトを付けたオブジェクトを指定する。// Active Events で Note On と Note Off を入れておく。// - 点けたり消したりする Light を、インスペクタの keyLight に渡す。public class GoalsMidiNoteOff : TsukimiBehaviour{ public Light keyLight;
private int held; // いま押されている鍵盤の数
// MIDI の Note On を受け取ったときに呼ばれる。 public override void MidiNoteOn(int channel, int number, int velocity) { // Note On の velocity が 0 のものは「離した」の意味で送られることがある(MIDI の決まり)。 if (velocity == 0) { Release(); return; } held++; keyLight.enabled = true; }
// MIDI の Note Off を受け取ったときに呼ばれる(多くは鍵盤やボタンを離したとき)。 public override void MidiNoteOff(int channel, int number, int velocity) { Release(); }
private void Release() { if (held > 0) held--; if (held == 0) keyLight.enabled = false; // すべての鍵盤が離されたら消す }}注意
- 機器によっては、離したときに
MidiNoteOffではなく、velocity 0 のMidiNoteOnが届きます。 - VRC Midi Listener の Active Events は、最初はすべて切れています。使うイベントにチェックを入れます。
MIDI のコントロールチェンジを受け取りたい
void MidiControlChange(int channel, int number, int value)
例
using UnityEngine;using Tsukimi;
// つまみで明るさ: MIDI 機器の決めたつまみ(コントロール番号)を回すと、照明の明るさが変わる。//// 前提:// - シーンに VRC Midi Listener を置き、その Behaviour にこのスクリプトを付けたオブジェクトを指定する。// Active Events で Control Change を入れておく。// - 明るさを変える Light をインスペクタの roomLight に、使うつまみの番号を knob に入れる// (つまみの番号は機器ごとに違う。機器の説明書か、下の number を一度表示して確かめる)。public class GoalsMidiControlChange : TsukimiBehaviour{ public Light roomLight; public int knob = 1; public float maxIntensity = 2f;
// MIDI の Control Change を受け取ったときに呼ばれる(多くは機器のつまみやスライダーを動かしたとき)。 // channel は 0〜15、number はコントロール番号 0〜127、value は 0〜127。 public override void MidiControlChange(int channel, int number, int value) { if (number != knob) return; // 決めたつまみ以外は無視する roomLight.intensity = value / 127f * maxIntensity; }}注意
- VRC Midi Listener の Active Events は、最初はすべて切れています。使うイベントにチェックを入れます。
- 使われるのは、見つかった最初の 1 台だけです。
プレイヤーごとの値が戻ってきたときに反応したい
void OnPlayerRestored(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.Persistence;
// 自己ベストの保存: 前回までの最高得点を読み出して看板に出し、それを超えたら書き直す。//// 前提:// - このスクリプトは、得点を数えるオブジェクトに付ける。// - 最高得点を出す TextMeshProUGUI を、インスペクタの board に渡す。// - 書けるのは自分のデータだけ(ほかの人のデータは読めるが、書けない)。public class GoalsPlayerRestored : TsukimiBehaviour{ public TextMeshProUGUI board; public int lastScore; // 1 回遊び終わったときの得点(ほかのスクリプトが SetProgramVariable で入れる)
private bool restored; // 自分のデータが読める状態になったか private int best;
// プレイヤーの保存したデータが読み込まれたときに呼ばれる(自分を含め、プレイヤーごとに 1 回)。 // これより前にプレイヤーごとのデータを読み書きしてはいけない。 public override void OnPlayerRestored(VRCPlayerApi player) { if (!player.isLocal) return; // 自分のデータのときだけ扱う restored = true;
// 保存が無い(初めて来た)ときは false が返り、best は 0 のまま。 if (PlayerData.TryGetInt(player, "best", out int saved)) best = saved; board.text = "自己ベスト: " + best; }
// 1 回遊び終わったときに、ほかのスクリプトから SendCustomEvent で呼ぶ。 public void Finish() { if (!restored) return; // 読み込み前に書くと、前回の記録を上書きしてしまう if (lastScore <= best) return; best = lastScore; PlayerData.SetInt("best", best); board.text = "自己ベスト: " + best; }}注意
- このイベントが届く前に、その人の値を読み書きしません。
- 書き込めるのは自分のデータだけです。
容量が足りなくなったときに反応したい
void OnPlayerDataStorageExceeded(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// 保存の容量を超えたら: 以後の書き込みを止め、何が起きたかを画面に出す。//// 前提:// - このスクリプトは、プレイヤーごとのデータを書くスクリプトと同じオブジェクトに付ける。// - 知らせを出す TextMeshProUGUI を、インスペクタの notice に渡す。public class GoalsPlayerDataStorageExceeded : TsukimiBehaviour{ public TextMeshProUGUI notice;
[HideInInspector] public bool canSave = true; // 書く側のスクリプトは、書く前にこれを見る
// プレイヤーの保存したデータが、使える容量を超えたときに呼ばれる。 public override void OnPlayerDataStorageExceeded(VRCPlayerApi player) { if (!player.isLocal) return; canSave = false; notice.text = "保存の容量を超えました(使用 " + Networking.GetPlayerDataStorageUsage(player) + " / 上限 " + Networking.GetPlayerDataStorageLimit() + " バイト)"; }}注意
- 使っている量を調べる関数は、
PlayerDataではなくNetworkingの側にあります。量の単位はバイトです。
容量が残り少ないときに反応したい
void OnPlayerDataStorageWarning(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.SDK3.Persistence;
// 容量が残り少ないとき: 長くなった遊んだ記録を、新しい方の半分だけに縮めて書き直す。//// 前提:// - このスクリプトは、遊んだ記録("history" という名前の文字列)を書くスクリプトと同じオブジェクトに付ける。// - 知らせを出す TextMeshProUGUI を、インスペクタの notice に渡す。public class GoalsPlayerDataStorageWarning : TsukimiBehaviour{ public TextMeshProUGUI notice;
// プレイヤーの保存したデータが、使える容量に近づいたときに呼ばれる。 public override void OnPlayerDataStorageWarning(VRCPlayerApi player) { if (!player.isLocal) return; // 書けるのは自分のデータだけ
if (PlayerData.TryGetString(player, "history", out string history)) { // 記録は古い順に並んでいる想定。後ろの半分(新しい方)だけを残す。 PlayerData.SetString("history", history.Substring(history.Length / 2)); } notice.text = "保存の容量が残り少ないので、古い記録を減らしました"; }}注意
- 使っている量を調べる関数は、
PlayerDataではなくNetworkingの側にあります。量の単位はバイトです。
買われたときに反応したい
void OnPurchaseConfirmed(VRC.Economy.IProduct product, VRC.SDKBase.VRCPlayerApi player, bool purchasedNow)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;using VRC.Economy;
// 支援者の部屋: 決めた商品を買った人にだけ、特典の部屋の扉を開ける。その場で買ったときはお礼を出す。//// 前提:// - このスクリプトは、扉の近くのどこか 1 つのオブジェクトに付ける。// - 特典にする商品の ID をインスペクタの productId に、閉じておく扉を door に、お礼を出す TextMeshProUGUI を thanks に入れる。// - 扉が開くのは、買った本人の画面でだけ(ほかの人の画面では閉じたまま)。public class GoalsPurchaseConfirmed : TsukimiBehaviour{ public string productId; public GameObject door; public TextMeshProUGUI thanks;
// プレイヤーの買った記録が読み込まれて確かめられたときに呼ばれる // (自分が入ったとき・誰かが入ってきたとき・その場で買ったとき)。 // product は買った商品、player は買った人、purchasedNow はその場で買ったときに true(前に買った記録なら false)。 public override void OnPurchaseConfirmed(IProduct product, VRCPlayerApi player, bool purchasedNow) { if (!player.isLocal) return; // 自分が買ったものだけ扱う if (product.ID != productId) return; // 決めた商品のときだけ
door.SetActive(false); // 扉を消して、部屋へ入れるようにする if (purchasedNow) thanks.text = product.Name + " を買ってくれてありがとう"; }}注意
- 買ったときだけでなく、自分や誰かが入ってきて買った記録が確認されたときにも届きます。その場で買ったかは
purchasedNowで判別します。
期限が切れたときに反応したい
void OnPurchaseExpired(VRC.Economy.IProduct product, VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;using VRC.Economy;
// 期限つきの特典: 決めた商品の期限が切れたら、特典の部屋の扉を閉じ直す。//// 前提:// - このスクリプトは、扉の近くのどこか 1 つのオブジェクトに付ける(開ける側は、買ったときの例と同じ扉を使う)。// - 期限のある商品の ID をインスペクタの productId に、閉じ直す扉を door に入れる。public class GoalsPurchaseExpired : TsukimiBehaviour{ public string productId; public GameObject door;
// インスタンスにいる誰かの商品の期限が切れたのを、自分の側で見つけたときに呼ばれる。 // product は期限の切れた商品、player はその持ち主。 public override void OnPurchaseExpired(IProduct product, VRCPlayerApi player) { if (!player.isLocal) return; // 自分の商品のときだけ if (product.ID != productId) return;
door.SetActive(true); // 扉を戻して、部屋を閉じる }}注意
- インスタンスの誰かの期限切れを、自分の画面で見つけたときに届きます。
商品の一覧を受け取りたい
void OnListAvailableProducts(VRC.Economy.IProduct[] products)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.Economy;using VRC.Udon.Common.Interfaces;
// 商品の案内板: ワールドに入ったら、このワールドで売っている商品の名前と説明を看板に並べる。//// 前提:// - このスクリプトは、看板にするオブジェクトに付ける。// - 並べる先の TextMeshProUGUI を、インスペクタの board に渡す。public class GoalsListAvailableProducts : TsukimiBehaviour{ public TextMeshProUGUI board;
void Start() { // 結果は、渡したスクリプト(ここでは自分)の OnListAvailableProducts に届く。 Store.ListAvailableProducts((IUdonEventReceiver)this); }
// Store.ListAvailableProducts の結果が届いたときに呼ばれる。products にこのワールドの商品が全部入る。 public override void OnListAvailableProducts(IProduct[] products) { string text = ""; foreach (IProduct p in products) text += p.Name + " — " + p.Description + "\n"; board.text = text; }}注意
- 結果は、
ListAvailableProductsに渡したプログラムに届きます。
物を跳ね上げたい
body.AddForce(force, ForceMode.Impulse)
例
using UnityEngine;using Tsukimi;
// 物用のジャンプ台: 台の上の範囲に入ってきた物(Rigidbody 付き)を、上へ跳ね上げる。//// 前提:// - このスクリプトは、ジャンプ台の範囲にする Collider(Is Trigger)と同じオブジェクトに付ける。// - 跳ね上げる強さは power で決める。public class GoalsUnityForce : TsukimiBehaviour{ public float power = 6f;
// 何かの Collider がこの範囲に入ったときに呼ばれる。 void OnTriggerEnter(Collider other) { Rigidbody body = other.attachedRigidbody; if (body == null) return; // Rigidbody の無い物は動かせない // Impulse は、重さを考えた「一瞬の力」として加える。 body.AddForce(Vector3.up * power, ForceMode.Impulse); }}目の前に何があるかを調べたい
Physics.Raycast(origin, direction, out hit, 12.5f)
例
using UnityEngine;using Tsukimi;using TMPro;
// レーザー距離計: 装置の正面へ光線を飛ばし、当たった物までの距離と名前を表示する。//// 前提:// - このスクリプトは、距離計のオブジェクトに付ける。装置の正面(青い軸)の向きに測る。// - 結果を出す TextMeshProUGUI を readout に、当たった所に置く目印を marker に渡す。public class GoalsUnityRaycast : TsukimiBehaviour{ public TextMeshProUGUI readout; public Transform marker;
void Update() { RaycastHit hit; // 位置・向き・当たりの結果・届く距離(12.5 m)。当たれば true を返し、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 = "---"; } }}毎フレーム少しずつ動かしたい
transform.Translate(velocity * Time.deltaTime)
例
using UnityEngine;using Tsukimi;
// 往復するエレベーター: 床を毎フレーム少しずつ上へ動かし、上の端に着いたら下へ向きを変える。//// 前提:// - このスクリプトは、エレベーターの床のオブジェクトに付ける。// - 動く高さは travel(m)、速さは speed(1 秒あたり m)で決める。// - 床の動きは、それぞれの人の画面で計算する(同期はしていない)。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() { // Time.deltaTime(前のフレームからの秒数)を掛けると、フレームレートによらず同じ速さになる。 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; }}決まった場所と向きに置きたい
transform.SetPositionAndRotation(position, rotation)
例
using UnityEngine;using Tsukimi;
// 片付けボタン: 押すと、散らかった物を最初に置いてあった場所と向きへ戻し、動きも止める。//// 前提:// - このスクリプトは、片付けボタン(Collider 付き)に付ける。// - 戻す物(Rigidbody 付き)を 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++) { // 位置と向きを 1 回で決める。 items[i].transform.SetPositionAndRotation(homePosition[i], homeRotation[i]); items[i].velocity = Vector3.zero; items[i].angularVelocity = Vector3.zero; } }}アニメーション
Section titled “アニメーション”扉が開くアニメーションを再生したい
animator.SetTrigger("Open")
例
using UnityEngine;using Tsukimi;
// 宝箱: 触ると、ふたが開くアニメーションを 1 回再生する。//// 前提:// - このスクリプトは、宝箱のオブジェクト(Collider 付き)に付ける。宝箱の Animator を chest に渡す。// - Animator Controller に Trigger のパラメータ「Open」を作り、それで「開く」の状態へ移るようにしておく。public class GoalsUnityAnimatorTrigger : TsukimiBehaviour{ public Animator chest;
public override void Interact() { // Trigger は 1 回だけ立ち、遷移に使われると自動で下りる。 chest.SetTrigger("Open"); }}開いた状態と閉じた状態を切り替えたい
animator.SetBool("IsOpen", open)
例
using UnityEngine;using Tsukimi;
// 自動ドアの開閉: 触るたびに、開いた状態と閉じた状態を切り替える。//// 前提:// - このスクリプトは、ドアのボタン(Collider 付き)に付ける。ドアの Animator を door に渡す。// - Animator Controller に Bool のパラメータ「IsOpen」を作り、true で開いた状態、false で閉じた状態へ移るようにしておく。public class GoalsUnityAnimatorBool : TsukimiBehaviour{ public Animator door; private bool open;
public override void Interact() { open = !open; // Bool は、書き換えるまでその値のまま残る(Trigger と違って自動で下りない)。 door.SetBool("IsOpen", open); }}オブジェクト
Section titled “オブジェクト”物を出したり消したりしたい
door.SetActive(!door.activeSelf)
例
using UnityEngine;using Tsukimi;
// 隠し扉: 本棚のレバーを引くたびに、壁を消したり戻したりする。//// 前提:// - このスクリプトは、レバーのオブジェクト(Collider 付き)に付ける。消す壁のオブジェクトを wall に渡す。// - 壁の出し入れは、触った人の画面でだけ起きる(同期はしていない)。public class GoalsUnityActive : TsukimiBehaviour{ public GameObject wall;
public override void Interact() { // 無効にすると、見た目も当たり判定も消える。activeSelf は今の状態。 wall.SetActive(!wall.activeSelf); }}同じ物を増やしたい
Instantiate(prefab)
例
using UnityEngine;using Tsukimi;
// ボールの補充: 触るたびに、置き場へボールを 1 つ増やす。多くなりすぎないよう 10 個で止める。//// 前提:// - このスクリプトは、補充ボタン(Collider 付き)に付ける。// - 増やすボールのプレハブを ball に、出す場所の Transform を 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); // 位置と向きは、複製したあとで決める。 copy.transform.SetPositionAndRotation(spawnPoint.position, spawnPoint.rotation); }}注意
- 位置と向きを引数で渡す
Instantiate(prefab, position, rotation)は書けません。複製したあとでSetPositionAndRotationを呼びます。
画面に文字や数を出したい
label.text = "count: " + count
例
using System;using UnityEngine;using Tsukimi;using TMPro;
// 壁掛け時計: 見ている人の時刻(時と分)を、1 秒ごとに表示し直す。//// 前提:// - このスクリプトは、時計のオブジェクトに付ける。時刻を出す TextMeshProUGUI を 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; // 文字列を text に入れると、その文字が表示される。分は 2 桁にそろえる。 clock.text = now.Hour + ":" + (now.Minute < 10 ? "0" : "") + now.Minute; }}火花や煙を出したい
particles.Emit(12)
例
using UnityEngine;using Tsukimi;
// 鍛冶の金床: ハンマーで叩くたびに火花を飛ばす。強く叩くほど火花を多くする。//// 前提:// - このスクリプトは、金床のオブジェクト(Collider 付き)に付ける。火花の ParticleSystem を sparks に渡す。// - ParticleSystem の Emission は Rate を 0 にしておく(叩いたときにだけ出す)。public class GoalsUnityParticles : TsukimiBehaviour{ public ParticleSystem sparks;
// 何かがぶつかったときに呼ばれる。ぶつかった速さで火花の数を決める。 void OnCollisionEnter(Collision collision) { int count = Mathf.Clamp((int)(collision.relativeVelocity.magnitude * 5f), 3, 40); // その場で決まった数の粒を出す。 sparks.Emit(count); }}アバターの骨の最新の位置に合わせて動かしたい
void PostLateUpdate()
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 頭の上の目印: 自分の頭の骨の少し上に、目印のオブジェクトを付いて回らせる。//// 前提:// - このスクリプトは、目印にするオブジェクトに付ける。// - 目印は自分の画面にだけ出る(位置は同期していない)。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; }}リアクティブ
Section titled “リアクティブ”値が変わったら自動で反映したい
[Reactive]
例
using UnityEngine;using Tsukimi;using TMPro;
// 残弾の表示: 撃つたびに残りの数が減り、表示と銃の色が自動で追いつく。//// 前提:// - このスクリプトは、撃つ操作をするオブジェクト(Collider 付き)に付ける。// - 残りの数を出す TextMeshProUGUI を ammoText に、色を変える Renderer を body に渡す。// - 数は触った人の画面でだけ変わる(同期はしていない)。public class GoalsReactiveField : TsukimiBehaviour{ public TextMeshProUGUI ammoText; public Renderer body;
// 変化を追うフィールド。private にする。 [Reactive] private int ammo = 6;
// ammo が変わるたびに実行される。起動したときにも 1 回実行されるので、最初から表示が合う。 // 呼び出しはどこにも書いていない。 [Effect] private void ShowAmmo() { ammoText.text = ammo + " / 6"; body.material.color = ammo > 0 ? Color.white : Color.red; }
// 触るたびに 1 発撃つ。0 のときは、6 発へ詰め直す。 public override void Interact() { if (ammo > 0) ammo = ammo - 1; else ammo = 6; }}注意
- 同期するフィールドに付けると、ネットワーク越しに届いた値では効果が実行されません(警告
TUKI0115)。 - 効果の中で、自分が見ている値へ書き戻すと、循環した依存としてコンパイルのエラーになります。
ほかの値から導かれる値を置きたい
[Computed]
例
using UnityEngine;using Tsukimi;
// 2 つの鍵の扉: 左右のスイッチが両方入ったときだけ、扉が消えて通れるようになる。//// 前提:// - このスクリプトは、扉を管理する空のオブジェクトに付ける。// - 扉のオブジェクトを door に渡す。// - 左右のスイッチからは、それぞれ ToggleLeft / ToggleRight を呼ぶ// (例: スイッチの Interact から SendCustomEvent で呼ぶ)。public class GoalsReactiveComputed : TsukimiBehaviour{ public GameObject door;
[Reactive] private bool left; [Reactive] private bool right;
// left と right から導かれる値。どちらかが変わったときに計算し直される。 [Computed] private bool Open => left && right;
// 見ているのは Open だけ。left だけが入っても Open は false のままなので、この処理は実行されない。 [Effect] private void ApplyDoor() { door.SetActive(!Open); }
public void ToggleLeft() { left = !left; } public void ToggleRight() { right = !right; }}変わった瞬間だけ何かしたい
[On(nameof(charge))]
例
using UnityEngine;using Tsukimi;
// 得点の効果音: 得点が増えた瞬間にだけ音を鳴らす。減ったときや、起動したときには鳴らさない。//// 前提:// - このスクリプトは、得点を数えるオブジェクトに付ける。// - 鳴らす AudioSource を chime に渡す。// - 得点を足すときは AddPoint、やり直すときは ResetScore を呼ぶ。public class GoalsReactiveOn : TsukimiBehaviour{ public AudioSource chime;
[Reactive] private int score;
// score が変わった瞬間に実行される。起動したときには実行されない。 // 引数には、変わる直前の値が入る。 [On(nameof(score))] private void OnScoreChanged(int before) { if (score > before) chime.Play(); }
public void AddPoint() { score = score + 1; } public void ResetScore() { score = 0; }}注意
- 起動したときには実行されません。起動した直後から見た目を合わせたいときは
[Effect]を使います。
何に依存しているかを自分で書きたい
[Effect(nameof(width), nameof(height))]
例
using UnityEngine;using Tsukimi;
// 照明の調光: 明るさと色味が変わったときだけ、照明を設定し直す。// 同じ Behaviour にある別の値(点滅の回数)が変わっても、照明には触らない。//// 前提:// - このスクリプトは、照明を操作するパネルに付ける。// - 設定し直す Light を lamp に渡す。// - パネルのボタンから Brighter / Warmer / Blink を呼ぶ。public class GoalsReactiveEffectDeps : TsukimiBehaviour{ public Light lamp;
[Reactive] private float brightness = 1f; [Reactive] private float warmth; [Reactive] private int blinkCount;
// 括弧に書いた 2 つだけを見る。blinkCount が変わっても実行されない。 // 起動したときにも 1 回実行される。 [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; }}変わったかどうかの判定を知りたい
Equals
例
using UnityEngine;using Tsukimi;
// 目印の移動: 毎フレーム同じ場所を書いても、場所が本当に変わったときだけ目印を動かし、ログを出す。//// 前提:// - このスクリプトは、目印を管理するオブジェクトに付ける。// - 追いかける対象を target に、動かす目印を marker に渡す。public class GoalsReactiveEquals : TsukimiBehaviour{ public Transform target; public Transform marker;
[Reactive] private Vector3 spot;
// spot が「変わった」ときだけ実行される。Vector3 は型付きの Equals で比べるので、 // 同じ値を書き直しても実行されず、ごく小さな差でも変わったと見る(== のように誤差を握り潰さない)。 [Effect] private void MoveMarker() { marker.position = spot; Debug.Log("目印を " + spot + " へ動かした"); }
// 毎フレーム、対象の場所を書く。対象が止まっている間は、上の処理は実行されない。 private void Update() { spot = target.position; }}注意
QuaternionとColorは NaN どうしを等しいと見ます。ほかの値型は、NaN が入ると変わり続けたと見ます。
シェーダーを書かずに面の色を決めたい
[Surface] static Color4 M(SurfaceId id, ...)
例
using UnityEngine;using Tsukimi;
// 体力ゲージ: 板の左から、残りの割合のぶんだけを緑に塗り、残りを暗い灰色に塗る。// シェーダーのファイルは書かない。色を決める処理を C# で書く。//// 前提:// - このスクリプトは、ゲージを管理するオブジェクトに付ける。// - ゲージにする板(Quad など)の Renderer を gauge に渡す。// - 残りの割合(0〜1)は hp に入れる。ダメージを受けたら Hit を呼ぶ。public class GoalsSurfaceColor : TsukimiBehaviour{ public Renderer gauge; public float hp = 1f;
// 面のピクセルごとに呼ばれ、返した色がそのピクセルに出る。id.UV は面の上の位置(0〜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); }
// 値は Gpu.Show を呼ぶたびに渡す。毎フレーム呼んで、いまの hp で塗り直す。 void Update() { Gpu.Show(nameof(Bar), gauge, hp); }
public void Hit() { hp = Mathf.Max(0f, hp - 0.1f); }}注意
- この章は試験中です。書き方が変わることがあります。
Gpu.Showに渡す値は、呼んだときのものが使われます。値を変えたら、もう一度呼びます。
面の向きで陰影を付けたい
Vector3.Dot(id.Normal, toLight)
例
using UnityEngine;using Tsukimi;
// トゥーン調の石像: 光の当たり方を 3 段階に分けて塗る。明るい面・中間・影の 3 色だけで描く。//// 前提:// - このスクリプトは、石像のオブジェクトに付ける。石像の Renderer を statue に渡す。// - 色は baseColor(明るい面の色)で決める。影の側は同じ色を暗くして塗る。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) { // 光の向きは自分で決める(場に置かれたライトは読めない)。 Vector3 toLight = new Vector3(0.4f, 1f, 0.3f).normalized; // id.Normal は面の向き(世界座標・長さ 1)。光の向きと揃うほど 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); }}注意
- 場に置かれたライトは読めません。光の向きは自分で決めます。影も受けません。
見ている向きで見え方を変えたい
Vector3.Dot(id.Normal, id.ViewDir)
例
using UnityEngine;using Tsukimi;
// 幽霊の縁の光: 見ている人から見て、輪郭に近い(面が横を向いている)ところほど青白く光らせる。//// 前提:// - このスクリプトは、幽霊のオブジェクトに付ける。幽霊の Renderer を ghost に渡す。// - 光の強さは glow(0 で光らない)で決める。public class GoalsSurfaceRim : TsukimiBehaviour{ public Renderer ghost; public float glow = 1f;
[Surface] static Color4 Rim(SurfaceId id, float glow) { // id.ViewDir はそのピクセルから見ている人へ向かう向き。 // 面が見ている人を向いていると内積は 1 に近く、輪郭では 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); }}面に値を渡したい
static Color4 M(SurfaceId id, GpuBuffer2D buf, int n, bool b, Vector2 v, Vector3 w)
例
using UnityEngine;using Tsukimi;
// 流れる縞模様の床: 縞の本数・色・流れる速さを Behaviour から渡し、時間とともに縞を流す。//// 前提:// - このスクリプトは、床のオブジェクトに付ける。床の Renderer を floor に渡す。// - 縞の本数は stripes、色は color、横へ流れる速さは speed(UV の単位で 1 秒あたり)で決める。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;
// 引数には float のほかに int・bool・Vector2・Vector3・Vector4 と 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); }
// 値は呼ぶたびに渡す。Gpu.Show の引数の並びは、上のメソッドの引数(id の後ろ)と同じ順にする。 void Update() { Gpu.Show(nameof(Stripes), floor, stripes, color, speed * Time.time); }}注意
Gpu.Showに渡す値は、メソッドの引数(idの後ろ)と同じ位置に並べます。
頂点そのものを動かしたい
[Surface] static Vector3 M(VertexId v, ...)
例
using UnityEngine;using Tsukimi;
// 風に揺れる旗: 旗の頂点を時間で波打たせる。竿に付いている側(UV の x が 0)は動かさない。//// 前提:// - このスクリプトは、旗のオブジェクトに付ける。旗のメッシュ(細かく分割した Plane など)の Renderer を flag に渡す。// - 揺れの大きさは amplitude(メッシュ座標の単位)で決める。public class GoalsSurfaceVertex : TsukimiBehaviour{ public Renderer flag; public float amplitude = 0.2f;
// 頂点ごとに呼ばれ、返した位置へ頂点を動かす。v.Position と戻り値はメッシュ座標。 [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); }
// 同じ名前で色の側も書く(2 つで 1 つのシェーダーになる)。 [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); }}注意
- 頂点を動かしても、色の側で読む
id.Normalは元の向きのままです。 - 位置の側だけを書くとエラーになります。同じ名前で色の側も書きます。
ワールドのイベント
Section titled “ワールドのイベント”触れたときに反応したい
void OnContactEnter(VRC.Dynamics.ContactEnterInfo contactInfo)
例
using UnityEngine;using Tsukimi;using VRC.Dynamics;using VRC.SDKBase;
// ハイタッチの的: アバターの手が触れると音が鳴り、触れた人の名前をログに出す。//// 前提:// - このスクリプトは、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 + " が触れました"); }}注意
- 届くのは、VRC Contact Receiver と同じオブジェクトに付けたプログラムだけです。
プレイヤーが範囲に入ったときに反応したい
void OnPlayerTriggerEnter(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 入ると点く明かり: 自分がこの範囲に入ったら明かりを点け、出たら消す。//// 前提:// - このスクリプトは、範囲にするオブジェクトに付ける。そのオブジェクトの 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); }}注意
- インスタンスにいる誰が入っても呼ばれます。自分だけに反応させるなら
player.isLocalで分けます。
プレイヤーがぶつかったときに反応したい
void OnPlayerCollisionEnter(VRC.SDKBase.VRCPlayerApi player)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 当たると鳴る壁: プレイヤーがこの壁にぶつかったら音を鳴らす。//// 前提:// - このスクリプトは、壁にするオブジェクトに付ける。そのオブジェクトの Collider は Is Trigger を入れない// (入れると、ぶつかる代わりに通り抜け、呼ばれるのは OnPlayerTriggerEnter になる)。// - 鳴らす音を入れた AudioSource を、インスペクタの sound に渡す。public class GoalsPlayerCollisionEnter : TsukimiBehaviour{ public AudioSource sound;
// プレイヤーがこの Collider にぶつかったときに呼ばれる。ほかの人がぶつかったときも呼ばれる。 public override void OnPlayerCollisionEnter(VRCPlayerApi player) { sound.Play(); }}言語が変わったときに反応したい
void OnLanguageChanged(string language)
例
using UnityEngine;using Tsukimi;using TMPro;
// 言葉が切り替わる看板: 見ている人の表示言語が日本語なら日本語で、それ以外なら英語で出す。//// 前提:// - 文字を出す 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"; }}注意
- 入室したときにも 1 回呼ばれます。
Startで言語を読み直す必要はありません。
入力の種類が変わったときに反応したい
void OnInputMethodChanged(VRC.SDKBase.VRCInputMethod inputMethod)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// 操作の案内の出し分け: 画面を触って操作している人にはタッチ用の案内を、それ以外の人には通常の案内を出す。//// 前提:// - タッチ用の案内と通常の案内のオブジェクトを、インスペクタの 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); }}画質の設定が変わったときに反応したい
void OnVRCQualitySettingsChanged()
例
using UnityEngine;using Tsukimi;using VRC.SDK3.Rendering;
// 影の代わり: 見ている人の影の描画距離が短いときだけ、足元に丸い影の板を出す。//// 前提:// - 丸い影の板(地面に置いた半透明の円など)を、インスペクタの 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); }}注意
- 設定を変えている間に何度も続けて呼ばれることがあります。中の処理は軽くしておきます。
ドローンが範囲に入ったときに反応したい
void OnDroneTriggerEnter(VRC.SDKBase.VRCDroneApi drone)
例
using UnityEngine;using Tsukimi;using TMPro;using VRC.SDKBase;
// ドローンの関門: ドローンがこの輪を通ったら、飛ばしている人の名前を看板に出す。//// 前提:// - このスクリプトは、輪にするオブジェクトに付ける。そのオブジェクトの 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 + " が通過"; }}カーネルを書き始めたい
return new Color4(...)
例
using UnityEngine;using Tsukimi;
// 色が巡る看板: 板の全面を、時間とともに赤・緑・青へゆっくり移り変わる色で塗る(GPU で計算する)。//// 前提:// - このスクリプトは、看板にする板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuReturn : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D board;
void Start() { board = Gpu.Buffer(64, 64); // 横 64 × 縦 64 のセル }
// [Kernel] を付けたメソッドは、セルごとに 1 回ずつ GPU で実行される。 // 返した色が、そのセルに書かれる(書き込む手段はこれだけ)。 [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); // board の全セルで Paint を実行する Gpu.Show(board, display); // 結果を板に映す }}注意
- 書き込む手段はカーネルの戻り値だけです。ほかのセルへ代入すると、エラーになります(
CS0200)。
短いカーネルを 1 行で書きたい
static Color4 Step(...) => prev[id] * 0.5f
例
using UnityEngine;using Tsukimi;
// 残像: 触るたびに板を白く光らせ、そのあと毎フレーム少しずつ暗くして消していく。//// 前提:// - このスクリプトは、板のオブジェクト(Collider 付き)に付ける。板の Renderer を 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); }
// 本体が 1 つの式なら => で書ける。前のフレームの値を 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); // current を読んで next へ書く Gpu.Swap(ref current, ref next); // 次のフレームは、いま書いたほうを読む Gpu.Show(current, display); }}いま計算しているセルの値を読みたい
prev[id]
例
using UnityEngine;using Tsukimi;
// 消えていく足跡: 触るたびに板を明るくし、明るさを毎フレーム一定の量ずつ減らして、0 で止める。//// 前提:// - このスクリプトは、床の板のオブジェクト(Collider 付き)に付ける。板の Renderer を 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] で、いま計算しているセルの、前のフレームの値を読む。 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); }}注意
- 既定のバッファでは、成分は 8 ビット(256 段階)に丸まります。細かい値が必要なら
GpuFormat.Halfで作るか、Gpu.Pack16x2で詰めます。
色の成分を 1 つずつ取り出したい
c.R c.G c.B c.A
例
using UnityEngine;using Tsukimi;
// 白黒の監視カメラ: カメラの映像を、明るさだけの白黒にして映す。//// 前提:// - このスクリプトは、モニターの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 監視カメラ(Camera)の出力先の RenderTexture を 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・c.A で成分を 1 つずつ取り出せる。人の目に合わせた重みで明るさにする。 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); // カメラの映像をバッファへ写す Gpu.Run(nameof(Gray), gray, camera); Gpu.Show(gray, display); }}返す色を、成分から組み立てたい
new Color4(r, g, b, a)
例
using UnityEngine;using Tsukimi;
// 色を入れ替える鏡: 映った像の赤と青を入れ替えて、別の世界のような色で映す。//// 前提:// - このスクリプトは、鏡の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 鏡に映すカメラの出力先の RenderTexture を 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(赤, 緑, 青, 不透明度) で、返す色を成分から組み立てる。 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); }}黒や白をそのまま使いたい
Color4.White
例
using UnityEngine;using Tsukimi;
// 影絵: カメラの映像を、明るいところは白、暗いところは黒の 2 色だけにして映す。//// 前提:// - このスクリプトは、スクリーンの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 映すカメラの出力先の RenderTexture を feed に、白と黒の境目の明るさ(0〜1)を 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 と Color4.Black は、白と黒をそのまま返せる。 return y > threshold ? Color4.White : Color4.Black; }
void Update() { Gpu.Load(camera, feed); Gpu.Run(nameof(Silhouette), shadow, camera, threshold); Gpu.Show(shadow, display); }}いま計算しているセルの位置で、模様を変えたい
id.X id.Y
例
using UnityEngine;using Tsukimi;
// 市松模様の床: セルの位置から、8 セルごとに白と灰色が入れ替わる模様を作る。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。public class GoalsGpuCellPosition : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(64, 64); Gpu.Run(nameof(Checker), floor); // 模様は変わらないので、最初に 1 回だけ作る }
[Kernel] static Color4 Checker(KernelId id) { // id.X と id.Y は、いま計算しているセルの横と縦の位置(左下が 0, 0)。 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); }}隣のセルの値を見たい
prev[id.Offset(1, -1)]
例
using UnityEngine;using Tsukimi;
// 輪郭の線画: カメラの映像で、上下左右のセルと明るさが大きく違うところだけを白い線にする。//// 前提:// - このスクリプトは、スクリーンの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 映すカメラの出力先の RenderTexture を 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(横, 縦) で、いまのセルから見た相対位置のセルを指す。 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); }}端のセルで、バッファの外を読んだときの値を知りたい
prev[id.Offset(-1000, -1000)]
例
using UnityEngine;using Tsukimi;
// 右へ流れる絵の具: 毎フレーム、全体を 1 セルずつ右へずらす。左の端には端のセルの色が流れ込み続ける。//// 前提:// - このスクリプトは、板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。// - 流す絵を palette に渡す(最初に 1 回だけ写す)。触ると最初の絵に戻る。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) { // 左のセルを読む。左端(id.X が 0)では、バッファの外(-1)を指すことになるが、 // 外を指した添字は端の値に丸められるので、左端のセル自身の色が返る。範囲の検査は書かなくてよい。 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); }}注意
- 端で反対側へ回して読みたいときは
Wrapを使います。
小さな模様を敷き詰めて並べたい
prev.Wrap(id.Offset(1, 0))
例
using UnityEngine;using Tsukimi;
// タイル張りの床: 16×16 の小さなタイルの絵を、128×128 の床一面に繰り返して敷き詰める。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。// - タイル 1 枚の絵(16×16)を 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) { // 床のセルの位置で、小さいタイルを読む。Wrap は外を指したら反対側へ回すので、 // 位置 (20, 3) はタイルの (4, 3) になり、タイルが繰り返し並ぶ。 return tile.Wrap(id); }
void Update() { Gpu.Show(floor, display); }}注意
- 幅 64 のバッファで -1 を指すと、63 のセルが返ります。
拡大しても四角い粒が見えないようにしたい
prev.Smooth(position)
例
using UnityEngine;using Tsukimi;
// 温度の地図の拡大: 16×16 の粗い温度の地図を、256×256 の板へ、四角い粒が見えないように引き伸ばして映す。//// 前提:// - このスクリプトは、地図の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 粗い地図の絵(16×16)を 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); // 地図は変わらないので、最初に 1 回だけ引き伸ばす }
[Kernel] static Color4 Enlarge(KernelId id, GpuBuffer2D map) { // Smooth は、位置(0〜1)で指した値を、まわりのセルと混ぜて返す。 // 大きいほうのセルの位置を 0〜1 に直して、小さいほうの地図を指す。 Vector2 position = new Vector2(id.X / 256f, id.Y / 256f); return map.Smooth(position); }
void Update() { Gpu.Show(big, display); }}注意
- 4 つのセルを読むので、1 つを読むより費用がかかります。
セルごとにばらついた値が欲しい
Gpu.Random01(n)
例
using UnityEngine;using Tsukimi;
// テレビの砂嵐: セルごと・フレームごとにばらばらの明るさで塗る。//// 前提:// - このスクリプトは、テレビの画面の板のオブジェクトに付ける。板の Renderer を 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 は、同じ入力なら必ず同じ値(0〜1)を返す。 // セルの位置とフレームの番号を混ぜた数を渡して、セルごと・フレームごとに違う値にする。 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); }}注意
- 同じ入力なら必ず同じ値が返ります。フレームごとに変えたいときは、フレームの番号を入力に混ぜます。
なめらかな模様を作りたい
Gpu.Noise(v)
例
using UnityEngine;using Tsukimi;
// 流れる雲: なめらかなノイズで雲のまだら模様を作り、時間とともに横へ流す。//// 前提:// - このスクリプトは、空の板のオブジェクトに付ける。板の Renderer を 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 は、近い位置では近い値になる、なめらかなノイズ(0〜1)。 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); }}書き込み先のバッファの大きさを、カーネルの中で知りたい
Gpu.OutWidth
例
using UnityEngine;using Tsukimi;
// 夕焼けのグラデーション: 板の下から上へ、橙から紺へ変わる空を塗る。どの大きさのバッファに書いても同じ見た目になる。//// 前提:// - このスクリプトは、背景の板のオブジェクトに付ける。板の Renderer を display に渡す。// - バッファの大きさは width と height で決める。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 は、書き込み先のバッファの縦のセル数。 // 位置を 0〜1 に直すのに使えば、大きさを引数で渡さなくてよい。 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); }}1 つのセルに、細かい値を 2 つ詰めたい
Gpu.Pack16x2(v)
例
using UnityEngine;using Tsukimi;
// ゆっくり動く粒: 粒の位置(横と縦)を 1 つのセルに細かく詰めて持ち、毎フレームほんの少しずつ動かす。// 8 ビット(256 段階)のままだと、少しずつの動きは丸められて止まってしまう。//// 前提:// - このスクリプトは、粒をまとめて持つオブジェクトに付ける。// - 粒の数は 64×64(セル 1 つが粒 1 つ)。位置を見るときは positions を映すか読み戻す。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 で、詰めた 2 つの値(各 65536 段階)を取り出す。 Vector2 p = Gpu.Unpack16x2(prev[id]); p.x = Mathf.Repeat(p.x + 0.0002f, 1f); // 1 フレームに 0.0002 だけ右へ // Pack16x2 で、2 つの値を 1 つのセルに詰めて書く(カーネルの中でしか使えない)。 return Gpu.Pack16x2(p); }
void Update() { Gpu.Run(nameof(Drift), next, positions); Gpu.Swap(ref positions, ref next); Gpu.Show(positions, display); }}注意
Gpu.Pack16x2はカーネルの中でしか使えません。GpuFormat.Halfで作ったバッファでは使いません。
2 つの色のあいだの色が欲しい
Color4.Lerp(c, c2, h)
例
using UnityEngine;using Tsukimi;
// 熱の地図の色分け: 温度(0〜1)を、冷たい青から熱い赤までの色で塗る。//// 前提:// - このスクリプトは、地図の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 温度の絵(赤の成分が温度)を 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) は、t が 0 なら a、1 なら b、そのあいだはその割合で混ぜた色。 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); }}同じ計算を、カーネルの外の関数にまとめたい
Falloff(d)
例
using UnityEngine;using Tsukimi;
// スポットライトの輪: 板の中心から離れるほど暗くなる光の輪を、2 つのカーネルで同じ減り方を使って描く。// 減り方の式は 1 つの関数にまとめ、どちらのカーネルからも呼ぶ。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 光の色は warm(true で暖色)で切り替える。public class GoalsGpuHelper : TsukimiBehaviour{ public Renderer display; public bool warm = true; private GpuBuffer2D floor;
void Start() { floor = Gpu.Buffer(128, 128); }
// カーネルから呼ぶ補助の関数。[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); }}GPU で計算するためのバッファをまず作りたい
Gpu.Buffer(64, 64)
例
using UnityEngine;using Tsukimi;
// ライフゲーム: 128×128 のセルで、生きているセル(白)と死んでいるセル(黒)の世代を毎フレーム進める。//// 前提:// - このスクリプトは、盤の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。// - 触ると、ランダムな配置から始め直す。public class GoalsGpuHostBuffer : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D current; private GpuBuffer2D next; private int seed;
void Start() { // GPU で計算するためのバッファ。横と縦のセルの数で作る(1 セルに 4 成分)。 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); }}0〜1 の外の値や、もっと細かい刻みを持ちたい
Gpu.Buffer(64, 64, GpuFormat.Half)
例
using UnityEngine;using Tsukimi;
// 水面の波紋: 触るたびに中心へしずくを落とし、広がって跳ね返る波を計算する。// 波の高さは負にもなるので、0〜1 の外の値も持てる形式でバッファを作る。//// 前提:// - このスクリプトは、水面の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。public class GoalsGpuHostFormat : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D before; private GpuBuffer2D now; private GpuBuffer2D next; private GpuBuffer2D shown;
void Start() { // GpuFormat.Half で作ると、1 成分が 16 ビットの小数になり、負の値や 1 を超える値も持てる。 // 形式は書くときに決まっている必要がある(変数では渡せない)。 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); // -1〜1 を 0〜1 へ写して見せる 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); }}注意
- 65504 を超えた値は 65504 のまま留まります。エラーも警告も出ません。
- 8 ビットの画像を
Gpu.Loadで写すと、0〜1 の外の値は届く前に潰れます。
画像や動画やカメラの映像を GPU に渡したい
Gpu.Load(current, source)
例
using UnityEngine;using Tsukimi;
// 動画のすりガラス: 動画プレイヤーの映像を GPU に渡し、ぼかしてから映す。//// 前提:// - このスクリプトは、すりガラスの板のオブジェクトに付ける。板の Renderer を display に渡す。// - 動画プレイヤーの映像が出る RenderTexture を 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() { // 画像・動画・カメラの映像をバッファへ写す。大きさが違うときは拡大縮小して入る。 Gpu.Load(frame, video); Gpu.Run(nameof(Blur), blurred, frame); Gpu.Show(blurred, display); }}注意
- 画像を色ではなく数として使うときは、その画像のインポート設定で sRGB を切ります。
前のフレームの結果を、次のフレームの入力にして回したい
Gpu.Swap(ref a, ref b)
例
using UnityEngine;using Tsukimi;
// たき火の炎: 下の段から熱を吹き込み、毎フレーム「前のフレームの結果」を読んで上へ昇らせながら冷ます。//// 前提:// - このスクリプトは、炎の板のオブジェクトに付ける。板の Renderer を 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); // 読み元と書き込み先を入れ替える。次のフレームは、いま書いたほうを読む。 Gpu.Swap(ref current, ref next); Gpu.Run(nameof(Colorize), shown, current); Gpu.Show(shown, display); }}書いたカーネルを 1 回実行したい
Gpu.Run(nameof(Step), next, current)
例
using UnityEngine;using Tsukimi;
// 写真のネガ: 触るたびに 1 回だけ、写真の明暗を反転する(毎フレームは計算しない)。//// 前提:// - このスクリプトは、写真の板のオブジェクト(Collider 付き)に付ける。板の Renderer を display に渡す。// - 写真のテクスチャを 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() { // 書き込み先・読み元の順に渡す。呼んだ 1 回ぶんだけ、全セルで Invert が実行される。 Gpu.Run(nameof(Invert), next, current); Gpu.Swap(ref current, ref next); Gpu.Show(current, display); }}時間のように毎フレーム変わる値を、カーネルへ渡したい
Gpu.Run(nameof(Step), next, current, phase)
例
using UnityEngine;using Tsukimi;
// 脈打つ光の輪: 時間と半径をカーネルへ渡し、中心から広がっては消える輪を描く。//// 前提:// - このスクリプトは、床の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 輪の最大の半径(セルの数)は radius で決める。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() { // 書き込み先のあとに並べた値が、カーネルの第 2 引数以降へ順番どおりに渡る。 Gpu.Run(nameof(Ring), floor, Time.time, radius); Gpu.Show(floor, display); }}実行するカーネルを名前の文字列で選びたい
Gpu.Run("Step", next, current)
例
using UnityEngine;using Tsukimi;
// フィルターの切り替え: ボタンを押すたびに、カメラの映像にかけるフィルターを順に切り替える。//// 前提:// - このスクリプトは、切り替えボタン(Collider 付き)に付ける。映す板の Renderer を display に渡す。// - カメラの出力先の RenderTexture を feed に渡す。public class GoalsGpuHostString : TsukimiBehaviour{ public Renderer display; public Texture feed; private GpuBuffer2D camera; private GpuBuffer2D filtered; private int current; // 0: そのまま 1: セピア 2: 暗視
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); // カーネルは名前の文字列でも指せる。名前は文字列の定数で書く(変数や式で組み立てた名前は書けない)。 // 綴りを間違えるとコンパイルで断られる。nameof で書けば、C# の側でも気づける。 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); }}注意
nameofを使うと、綴りを間違えたときに C# の側で気づけます。- 名前は文字列の定数か
nameofで書きます。変数や式で組み立てた名前は書けません。
1 つの Behaviour で、カーネルを使い分けたい
Gpu.Run(nameof(Fade), current, next)
例
using UnityEngine;using Tsukimi;using VRC.SDKBase;
// お絵描きボード: 自分がボードの前にいる間、手の位置にペンで点を描き、消しボタンで全部消す。// 描くカーネルと消すカーネルを 1 つの Behaviour に置いて使い分ける。//// 前提:// - このスクリプトは、ボードの板のオブジェクト(Collider 付き・大きさ 1×1 の Quad)に付ける。// - 板の Renderer を display に渡す。触ると全部消える。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() { // 右手の人差し指の位置を、板の上の位置(セルの座標)へ直す。 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); }}重みのようなたくさんの数を、カーネルへ渡したい
Gpu.Run(nameof(Blend), next, current, weights)
例
using UnityEngine;using Tsukimi;
// ドット絵のパレット: 写真の明るさを 16 段に分け、それぞれを表(パレット)の色に置き換える。//// 前提:// - このスクリプトは、額縁の板のオブジェクトに付ける。板の Renderer を display に、写真を photo に渡す。// - palette には 16 色を入れる(x,y,z が赤・緑・青)。長さが 16 でないと、実行は何も起きない。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); // 小さく読んで、ドット絵らしくする dotted = Gpu.Buffer(64, 64); Gpu.Load(src, photo); }
// 表の引数には、大きさを [Capacity(16)] のように書く(2 の冪・上限 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); }}注意
- 渡した配列の長さが
[Capacity]に書いた数と違うと、その実行は何も起きません。 - 1 つのカーネルが取る表の大きさの合計は 4000 までです。
カーネルへ渡す表の大きさを決めたい
[Capacity(4)] Vector4[] w
例
using UnityEngine;using Tsukimi;
// 空の色の移り変わり: 朝・昼・夕・夜の 4 色だけを表で渡し、時刻に合わせて空の色を作る。//// 前提:// - このスクリプトは、空の板のオブジェクトに付ける。板の Renderer を display に渡す。// - 1 日の長さ(秒)は dayLength で決める。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); // 朝 colors[1] = new Vector4(0.4f, 0.7f, 1f, 1f); // 昼 colors[2] = new Vector4(1f, 0.45f, 0.2f, 1f); // 夕 colors[3] = new Vector4(0.05f, 0.05f, 0.2f, 1f); // 夜 }
// 4 つだけの小さい表も同じ書き方で渡せる。 [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); }}読み元のバッファを使わずに、最初の状態を書き込みたい
Gpu.Run(nameof(Seed), board, 0.25f)
例
using UnityEngine;using Tsukimi;
// 雪原の最初の状態: 起動したときに、でこぼこの雪の高さを 1 回だけ書き込む。読み元のバッファは要らない。//// 前提:// - このスクリプトは、雪原の板のオブジェクトに付ける。板の Renderer を display に渡す。// - でこぼこの強さは bumpiness で決める。public class GoalsGpuHostNoBuffer : TsukimiBehaviour{ public Renderer display; public float bumpiness = 0.3f; private GpuBuffer2D snow;
void Start() { snow = Gpu.Buffer(128, 128); // 読み元を渡さずに実行する。担当するセルは書き込み先(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); }}全部のセルの合計や平均を出したい
Gpu.Reduce(nameof(Brighter), middle, full)
例
using UnityEngine;using Tsukimi;
// 明るさの自動調整: カメラの映像の平均の明るさを求め、暗いときは明るく、明るいときは暗く補正して映す。// 全セルの明るさを足し合わせて 1 セルにまとめ、セルの数で割って平均にする。//// 前提:// - このスクリプトは、モニターの板のオブジェクトに付ける。板の Renderer を display に渡す。// - カメラの出力先の RenderTexture を 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); // 足し合わせると 1 を超えるので、0〜1 の外の値も持てる形式にする。 middle = Gpu.Buffer(8, 8, GpuFormat.Half); total = Gpu.Buffer(1, 1, GpuFormat.Half); corrected = Gpu.Buffer(64, 64); }
// 2 つの値を 1 つにまとめる方法。[Reduce] を付ける。ここでは足し合わせる。 [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); // 書き込み先の 1 セルに、読み元のうちそのセルが受け持つ範囲をまとめた値が入る。 // 段は自分で並べる(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); }}注意
- 段は自分で並べます。書いた回数が、そのまま実行の回数になります。
成分ごとの最大や最小を知りたい
Gpu.Max(peak, full)
例
using UnityEngine;using Tsukimi;
// 暗い部屋の見やすさ調整: カメラの映像の、いちばん明るい値といちばん暗い値を調べ、その幅いっぱいに引き伸ばして映す。//// 前提:// - このスクリプトは、モニターの板のオブジェクトに付ける。板の Renderer を display に渡す。// - カメラの出力先の RenderTexture を 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); // 成分ごとに、全セルの最大と最小を 1 セルへまとめる。書き込み先は呼びごとに別のバッファにする。 Gpu.Max(peak, full); Gpu.Min(floor, full); Gpu.Run(nameof(Stretch), stretched, full, peak, floor); Gpu.Show(stretched, display); }}注意
- 同じバッファへ
Gpu.MaxとGpu.Minを続けると、後の呼び出しが前の結果を上書きします。
計算した結果を、そのままオブジェクトに映したい
Gpu.Show(current, display)
例
using UnityEngine;using Tsukimi;
// 走る光のネオン管: 管の長さに沿って光の粒が流れる模様を計算し、そのまま管の見た目に映す。//// 前提:// - このスクリプトは、ネオン管のオブジェクトに付ける。管の Renderer を tube に渡す。// - 管のメッシュの UV は、横(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); // 計算したバッファを、その Renderer の見た目としてそのまま映す。 Gpu.Show(lights, tube); }}計算した結果を、Unity や VRChat の API へ渡したい
Gpu.Texture(current)
例
using UnityEngine;using Tsukimi;
// 光る模様の服: 計算した模様を、別のオブジェクトのマテリアルの発光(Emission)の絵として渡す。//// 前提:// - このスクリプトは、模様を管理するオブジェクトに付ける。// - 光らせるオブジェクトの Renderer を target に渡す。そのマテリアルは Standard で、Emission を有効にしておく。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 で、バッファを Unity のテクスチャとして取り出す(複製ではなくバッファそのもの)。 target.material.SetTexture("_EmissionMap", Gpu.Texture(pattern)); }}注意
- 返るのはバッファそのもので、複製ではありません。
自分で用意した RenderTexture へ、計算した結果を写したい
VRCGraphics.Blit(Gpu.Texture(current), target)
例
using UnityEngine;using VRC.SDKBase;using Tsukimi;
// 計算した絵を UI に出す: 波の模様を計算し、UI の RawImage が表示している RenderTexture へ毎フレーム写す。//// 前提:// - このスクリプトは、模様を管理するオブジェクトに付ける。// - 写す先の RenderTexture を screen に渡す。UI の RawImage の Texture にも同じものを設定しておく。// - screen は Color Space を Linear(sRGB を外す)、Filter Mode を 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); // 自分で用意した RenderTexture へ写す。 VRCGraphics.Blit(Gpu.Texture(wave), screen); }}注意
- 写す先は sRGB を外して(
RenderTextureReadWrite.Linear)作ります。既定では sRGB が付き、カーネルが返した値がそのまま入りません。
自分で用意した RenderTexture を、そのまま書き込み先にしたい
public GpuBuffer2D target;
例
using UnityEngine;using Tsukimi;
// ミニマップへ直接書く: 自分で用意した RenderTexture を書き込み先にして、カーネルの結果をそこへ直接書く。//// 前提:// - このスクリプトは、ミニマップを管理するオブジェクトに付ける。// - インスペクタの map に、ミニマップの RenderTexture を挿す(Color Space は Linear、Filter Mode は Point)。// - 地形の高さの絵を terrain に渡す。public class GoalsGpuHostSlot : TsukimiBehaviour{ // public の GpuBuffer2D は、自分で用意した RenderTexture を挿す口になる。 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); // 挿した RenderTexture へ直接書く }}注意
- 挿した RenderTexture の設定はそのまま使われます。sRGB を外し、Filter Mode を Point にしておきます。
計算した結果を、数として Udon 側で読みたい
VRCAsyncGPUReadback.Request(Gpu.Texture(current), ...)
例
using UnityEngine;using VRC.SDK3.Rendering;using VRC.Udon.Common.Interfaces;using Tsukimi;using TMPro;
// スポイト: 絵の真ん中のセルの色を GPU から読み戻し、その値を数字で表示する。//// 前提:// - このスクリプトは、スポイトの装置のオブジェクトに付ける。// - 調べる絵を picture に、数字を出す TextMeshProUGUI を readout に渡す。public class GoalsGpuHostReadback : TsukimiBehaviour{ public Texture picture; public TextMeshProUGUI readout; private GpuBuffer2D buf; private byte[] cell = new byte[4]; // 1 セル = R, G, B, A の 4 バイト private bool waiting;
void Start() { buf = Gpu.Buffer(64, 64); }
void Update() { // 頼んだ答えが返るまでは、次を頼まない(バッファも書き換えない)。 if (waiting) return; Gpu.Load(buf, picture); waiting = true; // 真ん中(x = 32, y = 32)の 1 セルだけを頼む。 VRCAsyncGPUReadback.Request(Gpu.Texture(buf), 0, 32, 1, 32, 1, 0, 1, TextureFormat.RGBA32, (IUdonEventReceiver)this); }
// GPU の仕事が終わってから呼ばれる(頼んだその場では返らない)。 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]; // 0〜255 で届く }}注意
- 頼んだバッファは、答えが返るまでそのままにします。上書きすると届く中身が変わります(エラーにはなりません)。
- 受け皿の大きさと頼む形式が揃っていないと、エラーも警告も出ないまま受け皿の後ろが 0 のままになります。
学習済みモデル
Section titled “学習済みモデル”学習済みモデルを Udon で動かしたい
[Onnx("gesture.onnx")]
学習済みモデルを GPU で動かしたい
[OnnxGpu("filter.onnx")]
重い処理を何フレームかに分けて進めたい
async FrameTask M()
例
using UnityEngine;using Tsukimi;using TMPro;
// 得点表の並べ替え: 記録を高い順に並べ替える。1 フレームでやると止まって見えるので、// 1 周ごとに次のフレームへ分けて進め、終わったら上位 3 つを表に出す。//// 前提:// - このスクリプトは、得点表のオブジェクトに付ける。// - scores に記録が入っている。表を出す TextMeshProUGUI を board に渡す。// - 並べ替えを始めるときは Rank を呼ぶ。public class GoalsAsyncHeavySort : TsukimiBehaviour{ public int[] scores; public TextMeshProUGUI board;
// await の位置で中断して Udon へ戻り、続きは次のフレームから再開する。 // 呼び出した側は終わりを待たず、すぐ次の文へ進む。 private async FrameTask SortDescending() { board.text = "集計中…"; 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(); // 1 周ぶん進めたら、残りは次のフレームへ } board.text = "1 位 " + scores[0] + "\n2 位 " + scores[1] + "\n3 位 " + scores[2]; }
public void Rank() { SortDescending(); }}注意
- 処理は並列には実行されません。フレームをまたいで少しずつ進むだけです。
何フレーム待つかを指定したい
await Async.Frames(30)
例
using UnityEngine;using Tsukimi;using TMPro;
// 競争のスタート: 触ると 3・2・1 と数えてから「GO」を出し、ゲートを開ける。//// 前提:// - このスクリプトは、スタートのボタン(Collider 付き)に付ける。// - 数字を出す TextMeshProUGUI を sign に、開けるゲートのオブジェクトを 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); // 60 フレーム待つ(秒ではなくフレームで数える) } sign.text = "GO"; gate.SetActive(false); }
public override void Interact() { Countdown(); }}注意
- 待つ単位はフレームだけで、秒では待てません。フレームレートが違う人の画面では、待つ長さも違います。
同時に実行される本数を抑えたい
[MaxTasks(4)]
例
using UnityEngine;using Tsukimi;
// 打ち上げ花火: 触るたびに 1 発上げる。空に同時に出せるのは 4 発まで。//// 前提:// - このスクリプトは、発射台のオブジェクト(Collider 付き)に付ける。// - 花火に使う Light を 4 つ、lights に渡す(1 発ごとに 1 つを光らせて消す)。public class GoalsAsyncMaxTasks : TsukimiBehaviour{ public Light[] lights; private int next;
// 同じメソッドを 4 本まで同時に進められる。それぞれが自分の引数と途中の値を持つ。 // 4 本とも動いている間に呼んだ分は始まらない。 [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; // 30 フレームかけて暗くする await Async.Frame(); } l.enabled = false; }
public override void Interact() { // 4 発とも上がっている最中なら始まらない。始まったときだけ、次の明かりへ進める。 FrameTask t = Burst(next); if (t.Started) next = (next + 1) % lights.Length; }}注意
- 本体は本数のぶん複製されるので、本数を増やすほどプログラムが大きくなります。
もう始まっているかを知りたい
FrameTask t = M(); if (!t.Started)
例
using UnityEngine;using Tsukimi;using TMPro;
// 連打しても重ならない扉: 開け閉めの動きの最中に触られたら、動かさずに案内だけ出す。//// 前提:// - このスクリプトは、扉のボタン(Collider 付き)に付ける。// - 動かす扉の Transform を door に、案内を出す TextMeshProUGUI を status に渡す。public class GoalsAsyncStarted : TsukimiBehaviour{ public Transform door; public TextMeshProUGUI status; private bool open;
// 本数を書いていないので、同時に進められるのは 1 本だけ。 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(); // 前の動きがまだ終わっていないときは始まらず、Started が false になる。 if (!t.Started) status.text = "扉が動いている最中です"; }}注意
- 前の実行が終わっていない間の呼び出しは始まらず、
Startedは false になります。
長いループを分けて回したい
if (i % 256 == 255) await Async.Frame();
例
using UnityEngine;using Tsukimi;using TMPro;
// 陣取りの集計: 64×64 マスの盤面を数えるのを、256 マスごとに次のフレームへ分けて進める。// 1 フレームで全部数えると、その間ワールドが止まって見える。//// 前提:// - このスクリプトは、盤面を持つオブジェクトに付ける。// - cells には各マスの持ち主(0 は空き・1 は赤・2 は青)が入っている。長さは 4096。// - 結果を出す TextMeshProUGUI を result に渡す。数え始めるときは Tally を呼ぶ。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++; // 256 周ごとに中断する。i と途中の数は、中断をまたいで値を保つ。 if (i % 256 == 255) await Async.Frame(); } result.text = "赤 " + red + " / 青 " + blue; }
public void Tally() { Count(); }}跨ぐメソッドに値を渡したい
async FrameTask Fade(int frames, int target)
例
using UnityEngine;using Tsukimi;
// BGM の音量フェード: 範囲に入ると音量を上げ、出ると下げる。長さと目標の音量を引数で渡す。//// 前提:// - このスクリプトは、BGM を鳴らす範囲の Collider(Is Trigger)と同じオブジェクトに付ける。// - 鳴らしている AudioSource を bgm に渡す。public class GoalsAsyncArgs : TsukimiBehaviour{ public AudioSource bgm; private int latest;
// 引数 id・frames・target は、await のあとでも呼んだときの値のまま読める。 // 入ってすぐ出たときは 2 本が同時に進むので、新しいほうだけが音量を動かす。 [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; // 後から呼ばれたフェードに譲る bgm.volume = start + (target - start) * f / frames; await Async.Frame(); } }
// 自分がこの範囲に入ったときと出たときに、自分の画面で音量を変える。 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); }}別の跨ぐメソッドの終わりを待ちたい
await Fade(30)
例
using UnityEngine;using Tsukimi;
// 暗転して部屋を入れ替える: 触ると照明を落としきってから部屋を入れ替え、そのあと明るく戻す。//// 前提:// - このスクリプトは、切り替えのボタン(Collider 付き)に付ける。// - 部屋を照らす Light を roomLight に、入れ替える 2 つの部屋のオブジェクトを dayRoom と nightRoom に渡す。// - 入れ替えは触った人の画面でだけ起きる(同期はしていない)。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(); } }
// await を付けて呼ぶと、相手が終わるまで待ってから次の文へ進む。 private async FrameTask Switch() { await Fade(1f, 0f); // 暗くなりきるまで待つ bool night = !nightRoom.activeSelf; dayRoom.SetActive(!night); nightRoom.SetActive(night); await Fade(0f, 1f); // 明るく戻す }
public override void Interact() { Switch(); }}イベントから跨ぐ処理を始めたい
async FrameTask Start()
例
using UnityEngine;using Tsukimi;
// 開場の演出: ワールドに入ると、通路の明かりが手前から順に 1 つずつ点いていく。//// 前提:// - このスクリプトは、通路の明かりを管理する空のオブジェクトに付ける。// - 点ける順に並べた明かりのオブジェクトを lamps に渡す(最初はどれも無効にしておく)。public class GoalsAsyncStart : TsukimiBehaviour{ public GameObject[] lamps;
// Start をフレームを跨ぐ形にできる。Udon はいつもどおり Start を呼び、 // 中断した続きは後のフレームから再開する。 private async FrameTask Start() { foreach (GameObject lamp in lamps) { lamp.SetActive(true); await Async.Frames(15); } }}注意
Updateのように毎フレーム呼ばれるイベントを跨がせると、2 回目以降は前の実行が終わっていないので始まりません。Interact()のように基底から継ぐイベントは、戻り値を変えられないので跨がせられません。
小さいメソッドの呼び出しの手間を無くしたい
[Inline]
例
using UnityEngine;using Tsukimi;
// 浮かぶ足場: たくさんの足場を、毎フレームそれぞれの位相でゆっくり上下させる。// 毎フレーム何十回も呼ぶ小さい計算を、呼び出しの場所へ展開して、呼び出しの手間を無くす。//// 前提:// - このスクリプトは、足場をまとめて動かす空のオブジェクトに付ける。// - 動かす足場の Transform を 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; }
// 付けたメソッドは、大きさによらず呼び出しの場所へ展開される(メソッドとしては呼ばれない)。 // 展開したぶん、プログラムは大きくなる。小さくて何度も呼ぶものに付ける。 [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); }}注意
- 付けたメソッドは呼び出しの場所へ展開されるので、呼ぶ場所が多いほどプログラムが大きくなります。
プロファイラ
Section titled “プロファイラ”どこが重いかを調べたい
Tsukimi Profiler
実際に何回実行されたかを測りたい
Measure
テストと解析
Section titled “テストと解析”壊れていないか、自分で試したい
[TsukimiTest]
例
using Tsukimi;
// 自動販売機: 硬貨を入れて、値段に届いていれば 1 本売る。足りなければ何もしない。//// 前提:// - このスクリプトは、自動販売機のオブジェクトに付ける。// - 硬貨を入れるボタンから Insert を、買うボタンから Buy を呼ぶ。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;
// 上の自動販売機を、Unity を起動せずに試す。テストは 1 本ずつ新しいインスタンスで実行される。public partial class GoalsTestVending{ // [TsukimiTest] を付けた、引数の無い public void のメソッドが 1 本のテストになる。 [TsukimiTest] public void お金が足りないと売れない() { Insert(100); Buy(); Assert.AreEqual(0, sold); Assert.AreEqual(100, coins); // 入れた硬貨はそのまま残る }
[TsukimiTest] public void 足りると1本売れておつりが残る() { Insert(100); Insert(50); Buy(); Assert.AreEqual(1, sold); Assert.AreEqual(30, coins); }}注意
Startなどの起動時のイベントは、テストでは自動で実行されません。必要なときはテストの本文でStart()と書きます。
テストをどのファイルに書けばよいか知りたい
名前.Tests.cs
例
using Tsukimi;
// 暗証番号の扉: 4 桁を押し終えたところで、番号が合っていれば開ける。押し終えたら入力は空に戻る。//// 前提:// - このスクリプトは、扉のテンキーに付ける。各キーから Press(数字) を呼ぶ。// - テストは隣の test-keypad.Tests.cs に書く(このファイルと同じ名前+.Tests.cs)。// - テスト側と同じクラスを続けて書くので、どちらにも 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;
// 名前が「元のファイル名.Tests.cs」のファイルはコンパイルの対象から外れる。// テストがワールドのプログラムに混ざることはない。public partial class GoalsTestKeypad{ [TsukimiTest] public void 正しい番号で開く() { Press(4); Press(2); Press(7); Press(1); Assert.IsTrue(open); }
[TsukimiTest] public void 押し終えたら入力が空に戻る() { Press(1); Press(1); Press(1); Press(1); Assert.IsFalse(open); // 同じクラスの続きなので、private のフィールドにもそのまま触れる。 Assert.AreEqual(0, typed); Assert.AreEqual(0, count); }}注意
- テストを Behaviour の側のファイルに書くと、ワールドにアップロードされるプログラムに含まれます(警告
TUKI0117)。
条件が成り立っていることを確認したい
Assert.IsTrue(charge <= 100)
例
using Tsukimi;
// 充電台: 置くたびに電池を充電する。100 を超えないように止める。//// 前提:// - このスクリプトは、充電台のオブジェクトに付ける。電池を置いたら Charge(量) を呼ぶ。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 何度充電しても上限を超えない() { Charge(70); Charge(70); // 範囲の確認は、条件をそのまま書ける IsTrue / IsFalse で書く。 Assert.IsTrue(charge <= 100); Assert.IsFalse(charge < 0); }}注意
- 結果に出るのは真偽の値だけです。何と何を比べたかを残したいときは
Assert.AreEqualを使います。
2 つの値が同じであることを確認したい
Assert.AreEqual(1, count)
例
using Tsukimi;
// 連続ヒットの得点: 当てるたびに 10 点 × 連続数(3 まで)を足す。外すと連続数が 0 に戻る。//// 前提:// - このスクリプトは、的のオブジェクトに付ける。当たったら Hit、外れたら 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 連続数は3で止まる() { Hit(); Hit(); Hit(); Hit(); // 第 1 引数が期待した値、第 2 引数が実際の値。逆にすると結果の文面が逆になる。 Assert.AreEqual(10 + 20 + 30 + 30, score); }
[TsukimiTest] public void 外すと1からやり直し() { Hit(); Hit(); Miss(); Hit(); Assert.AreEqual(10 + 20 + 10, score); }}注意
floatを直に比べると、計算のたびに出る誤差までそのまま比べます。実数は、差が小さいことをAssert.IsTrueで確認します。
参照が空かどうかを確認したい
Assert.IsNull(current)
例
using Tsukimi;
// 競争の勝者: 最初にゴールした人の名前だけを残す。あとから来た人では書き換えない。//// 前提:// - このスクリプトは、ゴールのオブジェクトに付ける。ゴールした人がいたら Finish(名前) を呼ぶ。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 最初の人だけが残る() { // 参照が空かどうかは IsNull / IsNotNull で確認する。 Assert.IsNull(winner); Finish("Aoi"); Finish("Ren"); Assert.IsNotNull(winner); Assert.AreEqual("Aoi", winner); }}注意
- 破棄済みの Unity のオブジェクトは C# の
nullと違う状態を取りうるので、結果が直感と食い違うことがあります。
失敗したときに理由を残したい
Assert.IsTrue(charge > 0, "使い切っている")
例
using Tsukimi;
// 残り回数: 挑戦するたびに 1 つ減らす。0 のときは減らさず、挑戦もできない。//// 前提:// - このスクリプトは、ゲームの受付のオブジェクトに付ける。挑戦するときは TryPlay を呼ぶ。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 回数を使い切ると挑戦できない() { TryPlay(); TryPlay(); TryPlay(); TryPlay(); // 末尾の文字列は、成り立たなかったときにそのまま結果に出る。 // 同じ型の値を並べて比べるときに、どれが外れたかが分かる。 Assert.AreEqual(0, left, "残り回数は 0 で止まるはず"); Assert.AreEqual(3, played, "4 回目は挑戦できないはず"); }}カーネルの計算結果を確認したい
static float NextHeight(float now, ...)
例
using UnityEngine;using Tsukimi;
// 冷めていく熱の模様: 毎フレーム、各セルの熱をまわりと混ぜて少しずつ冷ます(GPU で計算する)。// 混ぜ方の式は static のメソッドに切り出して、カーネルとテストの両方から呼ぶ。//// 前提:// - このスクリプトは、模様を映す板のオブジェクトに付ける。板の Renderer を display に渡す。public partial class GoalsTestKernel : TsukimiBehaviour{ public Renderer display; private GpuBuffer2D heat; private GpuBuffer2D next;
// float・int・bool・Vector2 だけを受け取る static のメソッドは、テストからも呼べる。 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;
// カーネル([Kernel])そのものはテストから呼べない。切り出した式を確かめる。public partial class GoalsTestKernel{ [TsukimiTest] public void 熱はまわりと混ざって少し冷める() { // 実数は計算の誤差があるので、AreEqual で直に比べず、差が小さいことを確かめる。 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 冷めきったら0より下がらない() { Assert.AreEqual(0f, Cool(0f, 0f)); }}注意
[Kernel]のメソッドと、Color4やKernelIdを受け取るメソッドは、テストから呼べません。- 切り出したメソッドを Behaviour 側からも呼ぶと、そのメソッドも Udon へ変換されるので命令数が増えます。
同期が正しく届くことを、1 人で確認したい
Mimic.Join()
例
using Tsukimi;using VRC.SDKBase;
// みんなの最高記録: 自分の記録が今の最高を超えたら、所有者になって書き換え、全員へ送る。//// 前提:// - このスクリプトは、記録板のオブジェクトに付ける。遊び終えたら Submit(得点) を呼ぶ。[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;
// 1 本のテストの中に 2 人を置いて、送った値が相手に届くまでを確かめる。public partial class GoalsTestSync{ [TsukimiTest] public void ほかの人の最高記録が届く() { VRCPlayerApi me = Mimic.Join(); // 最初の 1 人が自分 VRCPlayerApi other = Mimic.Join(); Mimic.Become(other); // ここからは other の画面で実行する Submit(50); Mimic.Become(me); Assert.AreEqual(0, best); // 送っただけでは、まだ届いていない Mimic.Deliver(); // 送った人以外の全員に届く Assert.AreEqual(50, best); }}注意
Mimicは通信の遅れや送信の頻度を再現しません。確認できるのは、書いた手順どおりに実行した結果までです。SendCustomNetworkEventは記録されるだけで、相手には届きません。
受け取る順番が入れ替わっても壊れないことを確認したい
Mimic.Explore()
例
using Tsukimi;using VRC.SDKBase;
// ラウンドの表示: ラウンドの番号と、そのラウンドの残り時間を同期し、受け取ったら表示の文字を作る。//// 前提:// - このスクリプトは、進行役のオブジェクトに付ける。次のラウンドへ進めるときは NextRound を呼ぶ。[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(); }
// 受け取ったときに呼ばれる。2 つの値はどちらも書き終わっている。 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 どの順で届いても表示が揃う() { // テストの最初の文に書く。2 つの同期する値がどの順で書き込まれるかを全部入れ替えて、 // それぞれの順でこのテストを実行する。 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); }}注意
Mimic.Explore()はテストの最初の文に書きます。入れ替えるのは、このツールが知っている箇所だけです。
別の Behaviour と繋いだ状態で試したい
public Lamp lamp;
テストが重くなりすぎていないか見張りたい
[CostLimit(steps: 544)]
例
using Tsukimi;
// 盤面のリセット: 8×8 の盤面を全部空に戻す。遊びのたびに呼ぶので、重くなっていないかを見張りたい。//// 前提:// - このスクリプトは、盤面を持つオブジェクトに付ける。cells の長さは 64。// - 新しい遊びを始めるときに Clear を呼ぶ。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{ // 上限を超えたら、Assert が全部成り立っていてもこのテストは失敗になる。 // 数は、上限を書かずに 1 度実行して、結果に付いた steps を写したもの。 [TsukimiTest] [CostLimit(steps: 1812)] public void リセットは重くなっていない() { cells[5] = 2; stones = 1; Clear(); Assert.AreEqual(0, cells[5]); Assert.AreEqual(0, stones); }}注意
- 上限を超えると、
Assertが全部成り立っていてもそのテストは失敗になります。
テストを実行したい
Tsukimi Tests
結果をファイルで受け取りたい
Library/Tsukimi/last-test-run.json
出たエラーの意味を機械へ聞きたい
diagnostics
U# への書き出し
Section titled “U# への書き出し”U# へ出す
Section titled “U# へ出す”U# のプロジェクトへ出したい
Export as UdonSharp
出したコードがどうなるか知りたい
UdonSharp