リアクティブプログラミング
値が変わったときに実行される処理を、呼び出す場所ではなく依存の側に書けます。属性を付けたフィールドが変わると、そのフィールドを見ている処理が実行されます。
4 つの属性
Section titled “4 つの属性”| 属性 | 付ける先 | いつ実行されるか |
|---|---|---|
[Reactive] | フィールド | 変化を追う対象。private にする |
[Computed] | プロパティ | 他のフィールドから導かれる値。元が変わったときに更新される |
[Effect] | メソッド | 依存が変わったときに実行される。起動時にも 1 回実行されるので、起動した直後から見た目を合わせたいときはこちら |
[On(...)] | メソッド | トリガーが変わった瞬間に実行される。起動時には実行されないので、変わった瞬間だけを拾いたいときはこちら |
属性を 2 つ置く
Section titled “属性を 2 つ置く”変化を追うフィールドと、それを見る処理です。触るとフィールドが変化し、色を塗る処理が実行されます。呼び出しはどこにも書いていません。
using UnityEngine;using Tsukimi;
public class ReactiveFieldAndEffect : TsukimiBehaviour{ // 変化を追うフィールド。private にする。 [Reactive] private int charge = 100;
// 依存が変わったら走る。起動時にも 1 回走る。 [Effect] private void ApplyLook() { Renderer r = GetComponent<Renderer>(); if (r != null) r.material.color = charge > 0 ? Color.cyan : Color.gray; }
public override void Interact() { charge = charge - 20; if (charge < 0) charge = 0; }}導かれる値を挟む
Section titled “導かれる値を挟む”Lit は 2 つのフィールドから導かれます。効果が見ているのは Lit なので、charge が 100 から 80 へ変化しても Lit は変わらず、塗り替えは起きません。
using UnityEngine;using Tsukimi;
public class ReactiveComputed : TsukimiBehaviour{ [Reactive] private int charge = 100; [Reactive] private bool power = true;
// 他のフィールドから導かれる値。元が変わったときに更新される。 [Computed] private bool Lit => power && charge > 0;
[Effect] private void ApplyLook() { Renderer r = GetComponent<Renderer>(); if (r != null) r.material.color = Lit ? Color.cyan : Color.gray; }
public void TogglePower() { power = !power; }}変わった瞬間だけ拾う
Section titled “変わった瞬間だけ拾う”引数は書かなくても構いません。書くならトリガーと同じ順序・同じ型で全部書きます。受け取るのは変わる直前の値です。
using UnityEngine;using Tsukimi;
public class ReactiveOnTrigger : TsukimiBehaviour{ [Reactive] private int charge = 100;
// トリガーが変わった瞬間に走る。起動時には走らない。 // 引数は書かなくてもよく、書くならトリガーと同じ順・同じ型で全部書く。 [On(nameof(charge))] private void OnCharge(int prevCharge) { Debug.Log(prevCharge + " -> " + charge); }
public override void Interact() { charge = charge - 20; }}依存を自分で書く
Section titled “依存を自分で書く”括弧に名前を並べると、書いたものだけを見ます。unrelated が変わっても実行されません。
using UnityEngine;using Tsukimi;
public class ReactiveEffectDependencies : TsukimiBehaviour{ [Reactive] private int width = 2; [Reactive] private int height = 3; [Reactive] private int unrelated;
// 依存を書くと、書いたものだけを見る。unrelated が動いても走らない。 [Effect(nameof(width), nameof(height))] private void ShowArea() { Debug.Log(width * height); }
public void Grow() { width = width + 1; unrelated = unrelated + 1; }}効果をクラスに書く
Section titled “効果をクラスに書く”[Effect] はクラスにも付けられます。Calls に書いた名前のメソッドが効果になり、指されるのは普通のメソッドなので、宣言の側には何も書きません。
using UnityEngine;using Tsukimi;
// 効果をクラスの側に書く。Calls が指すのは普通のメソッドで、宣言の側には何も書かない。// 依存も書ける: [Effect(nameof(level), Calls = nameof(ShowLevel))]// 指した名前のメソッドが無いときはエラーになる。[Effect(Calls = nameof(ReactiveClassEffect.ShowLevel))]public class ReactiveClassEffect : TsukimiBehaviour{ [Reactive] private int level;
public void ShowLevel() { Debug.Log(level); }
public override void Interact() { level = level + 1; }}変わったかどうかの判定
Section titled “変わったかどうかの判定”| 型 | 判定 |
|---|---|
| 数値・真偽・文字列 | そのまま比べる |
値型(Unity の値型のほか System.DateTime や decimal など) | 型付きの Equals で比べる。== と違い誤差を握り潰さない |
| 参照型 | 同じインスタンスを指しているかで見る |
Quaternion と Color | 成分の比較が float の Equals になるので、NaN どうしを等しいと見ます(他の値型は成分を == で比べるので、NaN が入ると変化し続けます) |
エラーになる形
Section titled “エラーになる形”| 循環した依存 | 同じ Behaviour の中で循環していると、コンパイル時のエラーになります。自分のトリガーへ書き戻す形もこれに当たります |
| 直し方 | 生の値と丸めた値を別のフィールドに分けます。書き込む先をトリガーにしなければ循環しません |
[Reactive] private int hp;
// これはエラーになる。hp を読んで hp を書いているので、循環している。[Effect]private void Clamp(){ if (hp > 100) hp = 100;}警告になる形
Section titled “警告になる形”次の 4 つはコンパイルを止めず、実行してもエラーになりません(どれも「書いたとおりに動いていない」形なので、警告が唯一の手がかりです)。
| 番号 | 形 | 出る条件 |
|---|---|---|
TUKI0119 | 起動時に 1 回だけ実行される効果 | [Effect] を付けた効果で、依存を書くエントリポイントが 1 つも見つからないときに出ます。起動時の 1 回は実行されるので、書いた側からは動いているように見えます |
TUKI0113 | どこからも実行されない効果 | [On(...)] を付けた効果で、そのトリガーがどのエントリポイントでも変わらないときに出ます |
TUKI0115 | 同期するフィールドに付いた属性 | 伝播が入るのは値を書いたエントリポイントだけなので、ネットワーク越しに届いた値では効果が実行されません。宣言の書き方についての警告なので、受信側で実行する経路を書いても消えません。属性を残すなら OnDeserialization から同じ更新を呼び、消すなら同期するフィールドから属性を外して更新を自分で書きます |
TUKI0111 | 伝播を組み立てられない | 属性は読めたが、変化を伝える処理を 1 つも置けなかったときに出ます。効果は自動では実行されません |
| 形 | 今どうなるか |
|---|---|
| Behaviour をまたいだ依存の循環 | 検出していない(同じ Behaviour の中だけを見る) |
| 同期するフィールドをトリガーにする | 受信側の値は送信側と一致しないことがある。同期の仕組みには変更検知が無い |