はじめに
こんにちは、QualiArts Advent Calendar 2019、14日目の記事になります。
直近はUnityの開発していますが、その前はずっとウェブのフロントをやっていたため、本記事はVue.jsの(個人的に)目玉機能であるリアクティブシステムをUnityで再現してみるという話について書きます。
※日本語がネイティブではなくて、文書が読みづい可能性がありますので、御了承してください
Vue.jsのリアクティブシステム
Vue.jsの事が知らない方に簡単の例↓を出します。詳しく知りたい方はVue.js公式でどうぞ
var app = new Vue({
el: '#app',
data: {
familyName: "佐藤",
givenName: "太郎"
},
computed: {
fullName() {
console.log('[実行されたよ]');
return this.familyName + this.givenName;
}
}
});
console.log(app.fullName);
// [実行されたよ]
// 佐藤太郎
app.familyName = '鈴木';
console.log(app.fullName);
// [実行されたよ]
// 鈴木太郎
console.log(app.fullName);
// 鈴木太郎
data
に入る物は全てリアクティブプロパティであり、computed
に入る物は算出プロパティであります
リアクティブプロパティ:アクティブデータの源泉であり、値の更新が監視できます
算出プロパティ:基本的に呼び出さない限り実行されない、かつ依存しているリアクティブプロパティが変わらない限り再実行もされない仕様であります
裏には遅延処理とキャッシュが自動的に行われるため、使う側は直感的なコードが書けて、無駄の再実行も自動的に管理してくれる素晴らしい機能です
Unityのプロパティ周り(C#)
C#言語にはプロパティというのあります、上記の機能をざっくり再現してみましょう
public class App
{
public string familyName = "佐藤";
public string givenName = "太郎";
string _fullName;
public string FullName
{
get
{
var value = familyName + givenName;
if (value != _fullName)
{
_fullName = value;
}
return _fullName;
}
}
}
厳密に一緒ではないですが、familyName + givenName
の再実行は避けれない事になります。例えば、
familyName
とgivenName
の更新フラグを追加し、それぞれの更新チェックをする処理をすれば実現できますが、コード量はおそらく2倍になるでしょう。
その他のやり方
- 上記の比較・キャッシュ処理をメソッド化し、コードを簡略する
- INotifyPropertyChanged プロパティ更新のイベント発火で検知する
- UniRxのReactiveProperty プロパティをObservable化して、更新購読する
- など
欲望
色んなやり方は既にありますが、せっかくなのでVue.jsに近い形で再現できないかというのはきっかけです。例えば↓のような感じです。
public class App
{
public string familyName = "佐藤";
public string givenName = "太郎";
public string FullName => familyName + giveName;
}
IL修正 との出会い
色々調べていた所、INotifyPropertyChanged
のinterface実装を自動化したライブラリー (Fody/PropertyChanged)を見つけました。
Fody
自体はMono.cecil
のラッパーで、アセンブリのILを修正したりできるライブラリーです。
これを使えばできるじゃないかと思ってissues調べたら、どうやらUnityは未対応のようです。
むむ、もうちょっとググったり、github内検索したりしてみたら、Unityに対応するライブラリー (ByronMayne/Weaver)が出てきました。ちゃんとUnityのコンパイル後にフックで実行されますので、これを採用する事にしました。
プロトタイプを作ってみる
IL周りを実装する前に、Vue.jsのリアクティブ実装を参考しながら、C#のベースクラスを作成してみました。
ざっくりの原理
- あるWatcher(A君)がいます。
- このA君があるWatcher(B君)に依存して、B君の値を取得します。
- この時、B君が依存する他のWatcher(C君)がいれば、A君もC君に依存するような関係性を持たせるようにします。
- 依存がなくなるまで再帰的に続きます。
この原理に基づいてプロトタイプを作成しました。
IL修正 でコード簡略化する
その後、[Reactive]
と[Computed]
2種類のAttributeを用意し、AttributeをつけたらWatcherのインスタンスの生成処理やGetter/Setterの処理委託をILで自動挿入するようにしました。
public class App
{
[Reactive]
public string FamilyName { get; set; } = "佐藤";
[Reactive]
public string GivenName { get; set; } = "太郎";
[Computed]
public string FullName => FamilyName + GivenName;
}
↑のコードはIL挿入後、↓という風に展開されます
public class App
{
Wacther<string> _familyName;
public string FamilyName
{
get
{
if (_familyName == null)
{
_familyName = new Watcher<string>();
}
return _familyName.Get();
}
set
{
if (_familyName == null)
{
_familyName = new Watcher<string>();
}
_familyName.Set();
}
}
Wacther<string> _givenName;
public string GivenName
{
get
{
if (_givenName == null)
{
_givenName = new Watcher<string>();
}
return _givenName.Get();
}
set
{
if (_givenName == null)
{
_givenName = new Watcher<string>();
}
_givenName.Set();
}
}
Wacther<string> _fullName;
public string FullName
{
get
{
if (_fullName == null)
{
_fullName = new Watcher<string>(() =>
{
return FamilyName.Get() + GivenName.Get();
});
}
return _fullName.Get();
}
}
}
これで動ける最低限のプロトタイプは完成しましたが、実用までにはまだ改善と追加機能が必要です。必須的な機能例:
・ Setterの通知をまとめて次のTickで行う(重複実行を防ぐ、循環参照の検知)
・ WatcherにIDをつけて、通知はID順で実行する(実行順番の保証)
終わりに
Viewにバインディングする機能はまだないので、実質全然使えないプロトタイプですが、UnityでもVue.js風の書き方ができそうという事が証明できたじゃないかなと思っています。
C#初心者だったので、IL周りの試行錯誤を通してC#の裏はこんな感じなんだを知れたし、コンパイラはいかに便利かも知れたので、個人的に面白い体験と思いました。
上記のプロトタイプ実装はgithubに公開していますので、興味ある方ぜひこちらどうぞ。
https://github.com/thammin/kaki-watcher
以上、14日目の記事でした。引き続き今後のカレンダー投稿を宜しくお願いします。