コンテンツにスキップ

リアクティブプログラミング

値が変わったときに実行される処理を、呼び出す場所ではなく依存の側に書けます。属性を付けたフィールドが変わると、そのフィールドを見ている処理が実行されます。

属性付ける先いつ実行されるか
[Reactive]フィールド変化を追う対象。private にする
[Computed]プロパティ他のフィールドから導かれる値。元が変わったときに更新される
[Effect]メソッド依存が変わったときに実行される。起動時にも 1 回実行されるので、起動した直後から見た目を合わせたいときはこちら
[On(...)]メソッドトリガーが変わった瞬間に実行される。起動時には実行されないので、変わった瞬間だけを拾いたいときはこちら

変化を追うフィールドと、それを見る処理です。触るとフィールドが変化し、色を塗る処理が実行されます。呼び出しはどこにも書いていません。

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;
}
}

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;
}
}

引数は書かなくても構いません。書くならトリガーと同じ順序・同じ型で全部書きます。受け取るのは変わる直前の値です。

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;
}
}

括弧に名前を並べると、書いたものだけを見ます。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;
}
}

[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;
}
}
型判定
数値・真偽・文字列そのまま比べる
値型(Unity の値型のほか System.DateTime や decimal など)型付きの Equals で比べる。== と違い誤差を握り潰さない
参照型同じインスタンスを指しているかで見る
Quaternion と Color成分の比較が float の Equals になるので、NaN どうしを等しいと見ます(他の値型は成分を == で比べるので、NaN が入ると変化し続けます)
循環した依存同じ Behaviour の中で循環していると、コンパイル時のエラーになります。自分のトリガーへ書き戻す形もこれに当たります
直し方生の値と丸めた値を別のフィールドに分けます。書き込む先をトリガーにしなければ循環しません
[Reactive] private int hp;
// これはエラーになる。hp を読んで hp を書いているので、循環している。
[Effect]
private void Clamp()
{
if (hp > 100) hp = 100;
}

次の 4 つはコンパイルを止めず、実行してもエラーになりません(どれも「書いたとおりに動いていない」形なので、警告が唯一の手がかりです)。

番号形出る条件
TUKI0119起動時に 1 回だけ実行される効果[Effect] を付けた効果で、依存を書くエントリポイントが 1 つも見つからないときに出ます。起動時の 1 回は実行されるので、書いた側からは動いているように見えます
TUKI0113どこからも実行されない効果[On(...)] を付けた効果で、そのトリガーがどのエントリポイントでも変わらないときに出ます
TUKI0115同期するフィールドに付いた属性伝播が入るのは値を書いたエントリポイントだけなので、ネットワーク越しに届いた値では効果が実行されません。宣言の書き方についての警告なので、受信側で実行する経路を書いても消えません。属性を残すなら OnDeserialization から同じ更新を呼び、消すなら同期するフィールドから属性を外して更新を自分で書きます
TUKI0111伝播を組み立てられない属性は読めたが、変化を伝える処理を 1 つも置けなかったときに出ます。効果は自動では実行されません
形今どうなるか
Behaviour をまたいだ依存の循環検出していない(同じ Behaviour の中だけを見る)
同期するフィールドをトリガーにする受信側の値は送信側と一致しないことがある。同期の仕組みには変更検知が無い