値が変わったときに実行される処理を、呼び出す場所ではなく依存の側に書けます。属性を付けたフィールドが変わると、そのフィールドを見ている処理が実行されます。
| 属性 | 付ける先 | いつ実行されるか |
|---|
[Reactive] | フィールド | 変化を追う対象。代入するのは、そのクラスのメソッドが自分のフィールドへ書くときだけにする(外から読むのはよい) |
[Computed] | プロパティ | 他の値から導かれる値。値は取っておかず、読むたびに getter で計算し直す(効果を実行するかを決めるときにも呼ぶ) |
[Effect] | メソッド | 依存が変わったときに実行される。起動時にも 1 回実行されるので、起動した直後から見た目を合わせたいときはこちら |
[On(...)] | メソッド | トリガーが変わった瞬間に実行される。起動時には実行されないので、変わった瞬間だけを拾いたいときはこちら |
変化を追うフィールドと、それを見る処理です。触るとフィールドが変化し、色を塗る処理が実行されます。呼び出しはどこにも書いていません。
public class ReactiveFieldAndEffect : TsukimiBehaviour
// 変化を追うフィールド。private にする。
[Reactive] private int charge = 100;
// 依存が変わったら走る。起動時にも 1 回走る。
Renderer r = GetComponent<Renderer>();
if (r != null) r.material.color = charge > 0 ? Color.cyan : Color.gray;
public override void Interact()
if (charge < 0) charge = 0;
Lit は 2 つのフィールドから導かれます。効果が見ているのは Lit なので、charge が 100 から 80 へ変化しても Lit は変わらず、塗り替えは起きません。
public class ReactiveComputed : TsukimiBehaviour
[Reactive] private int charge = 100;
[Reactive] private bool power = true;
// 他のフィールドから導かれる値。元が変わったときに更新される。
[Computed] private bool Lit => power && charge > 0;
Renderer r = GetComponent<Renderer>();
if (r != null) r.material.color = Lit ? Color.cyan : Color.gray;
public void TogglePower()
引数は書かなくても構いません。書くならトリガーと同じ順序・同じ型で全部書きます。受け取るのは変わる直前の値です。
public class ReactiveOnTrigger : TsukimiBehaviour
[Reactive] private int charge = 100;
// トリガーが変わった瞬間に走る。起動時には走らない。
// 引数は書かなくてもよく、書くならトリガーと同じ順・同じ型で全部書く。
private void OnCharge(int prevCharge)
Debug.Log(prevCharge + " -> " + charge);
public override void Interact()
括弧に名前を並べると、書いたものだけを見ます。unrelated が変わっても実行されません。
public class ReactiveEffectDependencies : TsukimiBehaviour
[Reactive] private int width = 2;
[Reactive] private int height = 3;
[Reactive] private int unrelated;
// 依存を書くと、書いたものだけを見る。unrelated が動いても走らない。
[Effect(nameof(width), nameof(height))]
Debug.Log(width * height);
unrelated = unrelated + 1;
[Effect] はクラスにも付けられます。Calls に書いた名前のメソッドが効果になり、指されるのは普通のメソッドなので、宣言の側には何も書きません。
// 効果をクラスの側に書く。Calls が指すのは普通のメソッドで、宣言の側には何も書かない。
// 依存も書ける: [Effect(nameof(level), Calls = nameof(ShowLevel))]
// 指した名前のメソッドが無いときはエラーになる。
[Effect(Calls = nameof(ReactiveClassEffect.ShowLevel))]
public class ReactiveClassEffect : TsukimiBehaviour
[Reactive] private int level;
public override void Interact()
| |
|---|
名前を書かない効果と [Computed] | 本体が直接読んだ、このクラスの [Reactive] のフィールドと [Computed] のプロパティが依存です。呼んだメソッドの中で読んだ値は依存になりません |
| ほかのインスタンスとほかのクラスの値 | 依存になりません(next.count など)。その値が変わっても効果は実行されないので、TUKI0119 の警告が出ます |
| 名前を書いた効果 | 書いた名前が依存の全部です。本体が読んでいても、書いていない値の変化では実行されず、TUKI0119 の警告がその値を名指します。その値を名前に足すか、名前を全部消して推論に任せます |
| 書ける名前 | このクラスの [Reactive] のフィールドか [Computed] のプロパティです。ほかの名前(ほかの効果の名前を含みます)は TUKI0133 です |
クラスに付けた [Effect] の Calls | 指すメソッドは public でもかまいません。名前を書かなければ、そのメソッドが直接読んだ値が依存です。同じメソッドを 2 つ以上の属性で指すと、書いた名前を全部見ます |
| 型 | 判定 |
|---|
| 数値・真偽・自分で宣言した列挙型・文字列 | そのまま比べる |
NaN を持つ float | != で比べるので、そのフィールドを書きうるエントリポイントが終わるたびに、書いたかどうかによらず変わったと見る |
値型(Unity の値型のほか System.DateTime や decimal など) | 型付きの Equals で比べる。== と違い誤差を握り潰さない |
| 参照型 | 同じインスタンスを指しているかで見る |
| 自分で宣言した struct・タプル | 成分ごとに、この表の規則で比べる。どれかの成分が変わっていれば、効果は 1 回実行される。成分への代入(cell.count = 1・cell.count++)も、全体の代入(cell = new Cell())も、そのフィールドへの書き込みになる |
| |
|---|
ColorBlock | 自分の == で比べます。色をごく小さな誤差の範囲で等しいと見るので、色のごく小さな変化は変わったと見ません |
Quaternion と Color | 成分の比較が float の Equals になるので、NaN どうしを等しいと見ます。NaN を持つフィールドに NaN を書き直しても、変わったと見ません(Vector2・Vector3・Vector4・Bounds・Matrix4x4 は、float と同じく変わったと見ます) |
| |
|---|
| エントリポイントの終わり | フィールドを書いたエントリポイントが終わるとき、入った時点の値と比べて変わっていれば、効果が 1 回実行されます。同じ値を書き直しただけなら実行されず、何度書いても 1 回です。エントリポイントから呼んだ private のメソッドの中で書いても同じです |
| 同じクラスの別の public メソッドの呼び出し | 呼ばれたメソッドの本体がそのまま実行されるので、効果は外側のエントリポイントの終わりで 1 回実行されます |
| 書き込みに数える書き方 | 代入のほか、struct のフィールドを変えるメソッドの呼び出し(tally.Grow())・このクラスのインデクサとプロパティへの設定・base. での基底のメソッドの呼び出しも書き込みです |
| 効果からの書き込み | 効果は [Reactive] のフィールドへ書けます。本体に書いても、呼んだメソッド・プロパティの setter・インデクサ・base. の呼び出し・out と ref の引数・struct のフィールドを変えるメソッドの中で書いても同じで、その値を読む効果は同じ更新の中で後から実行されます。書き込みが輪になると TUKI0112 です |
public の [Effect] | コンパイラが効果として呼ぶときはほかの効果と同じに実行され、外から呼んだときはエントリポイントとして終わりで 1 回効果を実行します |
| コンパイラが足すエントリポイント | [FieldChangeCallback] のフィールドに SetProgramVariable か同期で値が届いたときに VRChat が実行する _onVarChange_ とフィールド名をつないだエントリポイント(setter を呼ぶ)、async FrameTask のメソッドの await の後を実行するエントリポイント、ほかの Behaviour が代入するプロパティの setter も、ほかのエントリポイントと同じように効果を実行します。同期するフィールドに値が届いたときは、_onVarChange_ の中では効果を実行しません(同期で届いた値) |
自分で書いた後の SetProgramVariable | SetProgramVariable はその場で _onVarChange_ のエントリポイントを実行するので、効果はその中で 1 回、呼んだエントリポイントの終わりでもう 1 回実行されます(自分へ SendCustomEvent を呼んだ場合と同じです) |
| 実行される場所の確認 | inspect の facts.reactive.firing の sites に、効果を実行するエントリポイントの名前が並びます。コンパイラが足したエントリポイントには、どのコードをいつ実行するかを示す via が付きます |
| テストのメソッド | テストのメソッドもエントリポイントです(テスト) |
| |
|---|
| 受信の順 | VRChat は 1 回の受信の同期するフィールドを 1 つずつ書き、書くたびにそのフィールドの [FieldChangeCallback] を実行し、最後の 1 つの後で OnDeserialization を実行します。setter の中では、ほかの同期するフィールドがまだ前の値のことがあります |
| 効果の実行 | setter が [Reactive] のフィールドを書くフィールドに値が届くと、効果は受信の終わり(OnDeserialization の終わり)で 1 回だけ実行され、途中の状態を見ません。2 つの届いたフィールドを読む効果も 1 回で、両方の新しい値を見ます |
自分で書いた OnDeserialization | 本体は同じ更新の一部で、効果は本体の後で実行されます。本体の中では、届いた値は書かれていますが、その効果はまだ実行されていません。OnDeserialization を書いていなければ、T# が足します |
[On] | 受信の前の値を、変わる直前の値として受け取ります。最初の受信でも実行されます |
SetProgramVariable との区別 | どちらも同じ [FieldChangeCallback] を実行するので、所有者かどうかで判別します(値を送るのは所有者なので、所有者はふつう受信しません)。所有者の PC で SetProgramVariable で書くと、効果はその場で _onVarChange_ の終わりに実行されます。所有者でない PC で書くと届いた値と同じに扱い、効果は次の OnDeserialization の終わりか、それが先に来なければ 1 フレーム後に実行されます |
保存から戻した値(isFromStorage) | 所有者の PC に届くので、書き込みと同じに扱います。効果は [FieldChangeCallback] ごとに、その終わりで実行されます |
| |
|---|
| 循環した依存 | 同じ Behaviour の中で循環していると、コンパイル時のエラーになります。自分のトリガーへ書き戻す形(呼んだメソッドや setter を通した書き戻しも)もこれに当たります |
| 直し方 | 生の値と丸めた値を別のフィールドに分けます。書き込む先をトリガーにしなければ循環しません |
[Reactive] private int hp;
// これはエラーになる。hp を読んで hp を書いているので、循環している。
| |
|---|
| エラーになる書き込み | [Reactive] のフィールドへの、ほかのクラスからの代入・同じクラスの別のインスタンスへの代入(next.count = 1)・SetProgramVariable に名前を書いた書き込み(そのクラスの中で自分へ書く場合も)は TUKI0134 です。値だけが変わり、効果が実行されないためです |
| 直し方 | そのクラスに代入するメソッドを書き、外からはそれを呼びます(counter.Bump()・counter.SendCustomEvent("Bump")) |
[FieldChangeCallback] も付けたフィールド | 名前での書き込みは _onVarChange_ と setter を通るので効果が実行され、エラーになりません(名前を変数で渡す場合も同じです)。ほかのクラスからの代入と別のインスタンスへの代入は、setter を通らないのでエラーのままです |
| 書き方 | 番号 | 直し方 |
|---|
1 つのメソッドに付けた [Effect] と [On] | TUKI0137 | どちらか 1 つにする(変わった瞬間に起こす処理なら [On]、今の状態を反映する処理なら [Effect]) |
Unity か VRChat が呼ぶイベント(Start・Update・Interact など)への属性 | TUKI0137 | コンパイラがそのイベントを自分で呼ぶことになるため。処理を private のメソッドへ移して属性を付け、イベントからはそのメソッドを呼ぶ |
virtual・abstract・override のメソッドへの属性 | TUKI0137 | 属性を付けたメソッドを virtual でない private のメソッドにする。派生クラスで変えたい処理は、そこから属性の無い virtual のメソッドとして呼ぶ |
| 値を返すメソッドへの属性 | TUKI0137 | 戻り値を void にする。値が必要なら別のメソッドか [Computed] のプロパティで求める |
引数を取るメソッドへの [Effect] | TUKI0137 | 引数を外してフィールドを直接読む。変わる直前の値が必要なら [On(nameof(...))] にする |
[On] のメソッドの中での [Reactive] のフィールドへの代入(呼んだメソッドとプロパティの setter の中の代入も) | TUKI0137 | [On] は [Effect] の後に実行されるので、そのフィールドを読む効果がこの変化で実行し直されない。値がほかの値から決まるなら [Computed] にし、持っておく必要があるなら [Effect] の中で代入する |
トリガーと合わない [On] の引数 | TUKI0137 | 引数を書かないか、トリガーと同じ順序・同じ型で全部書く |
クラスに付けた [Effect] の Calls が指す、上の形のメソッド | TUKI0137 | 指すメソッドを、引数と戻り値の無い、イベントでも virtual でもないメソッドにする |
名前の無い [On] | TUKI0133 | 変化で実行したいフィールドかプロパティの名前を書く |
[On] か [Effect] に書いた、[Reactive] のフィールドでも [Computed] のプロパティでもない名前 | TUKI0133 | フィールドなら [Reactive] を付け、そうでなければ名前を直す |
クラスに付けた [Effect] の、そのクラスのメソッドを指さない Calls(書いていない場合も) | TUKI0133 | Calls = nameof(Show) のように、同じクラスのメソッドを指す |
| Behaviour の外(struct・record・ほかのクラスのメンバー・ローカル関数・アクセサー)に付けた属性 | TUKI0133 | 属性は Behaviour 自身のフィールド・プロパティ・メソッドにだけ付ける |
public class Bell : TsukimiBehaviour
[Reactive] private int rings;
// これはエラーになる。[On] は変わったときだけ、[Effect] は起動時にも実行されるので、どちらか 1 つにする。
[Effect, On(nameof(rings))]
public override void Interact() { rings = rings + 1; }
次の 4 つはコンパイルを止めず、実行してもエラーになりません(どれも「書いたとおりに動いていない」形なので、警告が唯一の手がかりです)。
| 番号 | 形 | 出る条件 |
|---|
TUKI0119 | 読んだ値が変わっても実行し直されない効果 | [Effect] を付けた効果(か [Computed] を読む効果)で、次のどれかのときに出ます。依存を書くエントリポイントが 1 つも見つからない(起動時の 1 回は実行されるので、書いた側からは動いているように見えます)・ほかのインスタンスかほかのクラスの値を読んでいる・名前を書いた効果が書いていない値を読んでいる。メッセージがどれかを言います |
TUKI0113 | どこからも実行されない効果 | [On(...)] を付けた効果で、そのトリガーがどのエントリポイントでも変わらないときに出ます |
TUKI0115 | 同期するフィールドに付いた属性 | 伝播が入るのは値を書いたエントリポイントだけなので、ネットワーク越しに届いた値では効果が実行されません。宣言の書き方についての警告なので、OnDeserialization から同じ更新を呼んでも消えません。そのフィールドに [FieldChangeCallback] を付け、setter でフィールドへ代入すると、届いた値は setter を通って効果が実行され、警告も出ません。それ以外は、属性を残して OnDeserialization から同じ更新を呼ぶか、同期するフィールドから属性を外して更新を自分で書きます |
TUKI0111 | 伝播を組み立てられない | 属性は読めたが、変化を伝える処理を 1 つも置けなかったときに出ます(比べ方の無い型の [Reactive] など。非対応の表)。そのクラスの効果は自動では実行されません |
| 形 | 今どうなるか |
|---|
| Behaviour をまたいだ依存の循環 | 検出していない(同じ Behaviour の中だけを見る) |
| 同期するフィールドをトリガーにする | 届いた値では効果が実行されないので、受信側で効果が反映する値は送信側と一致しないことがある(TUKI0115)。そのフィールドに [FieldChangeCallback] を付け、setter でフィールドへ代入すれば、届いた値でも効果が実行される |
比べ方の無い型(配列・DataList・DataDictionary・KeyCode などの実行環境の列挙型)の [Reactive] と、その型の成分を持つ struct の [Reactive] | 伝播を 1 つも置かず、そのクラスの効果は自動では実行されない。TUKI0111 の警告がそのフィールドか成分を名指す |