【GMS2】GMLでReactivePropertyを作ってみた【パッケージ公開】
はじめに
最近Unityでの開発を本格的に行っていたのですが、その時にUniRxのReactivePropertyって便利だなぁって思ったので、
似たような機能をGMLでも作れないかなーと思って作ってみました。
GUIとか作るときに使える機能だと思うので、パッケージの公開&軽く解説しようと思います。
ReactivePropertyとは?
ReactivePropertyはUnity向け拡張ライブラリである
UniRxに含まれている機能のひとつです。
ひとことで言うと値の変更を自動で通知してくれる変数のようなものです。
ReactivePropertyは、値が変わった瞬間に登録しておいた処理(コールバック関数)を呼び出すことができます。
例えば、
- HPが変わったらUIを更新する
- スコアが変わったらラベルを書き換える
- フラグが切り替わったら表示を変更する
といった処理を「値の変更」に直接ひもづけることができます。
var hp = new ReactiveProperty<int>(100);
// HPが変わったら自動でUI更新
hp.Subscribe(value =>
{
hpText.text = "HP : " + value;
});
// ダメージを与える
hp.Value -= 10;
今回作ったものは、そのReactivePropertyの考え方をGML向けに再現したものになります。それっぽく...。
使い方
インストール
こちらにパッケージを用意しました。
ダウンロードしてインポートをすれば使えるようになります。
ReactivePropertyを使った変数の宣言
まずはReactivePropertyを使って変数を宣言します。
テスト用のオブジェクトを作り、そこにコードを書いていきます。
今回はプレイヤー名とHPを宣言してみましょう。
// プレイヤー名
playerName = new ReactiveProperty("hoge");
// HP
playerHp = new ReactiveProperty(100);
これでReactivePropertyの変数宣言は終わりです。
コールバック関数の登録
次はコールバック関数を登録します。
ここに登録した関数は「値が変化したとき」に呼び出されます。
関数を登録するにはSubscribeという関数を使います。
reactiveProperty.Subscribe(_callback, [_skipInitial]);
| 引数名 | 型 | 説明 |
|---|---|---|
_callback |
Function | 値変更時に呼び出される関数 |
_skipInitial |
Bool (省略可) | 初回通知をスキップするかどうか(既定値: false) |
| 戻り値の型 | 説明 |
|---|---|
Struct.Subscription |
購読を管理するオブジェクト |
サンプルコード
// プレイヤー名が変わった時にログ出力
playerName.Subscribe(function(_value){
show_debug_message($"[playerName] {_value}");
});
// プレイヤーHPが変わった時にログ出力
playerHp.Subscribe(function(_value){
show_debug_message($"[playerHp] {_value}");
})
基本的な購読はこんな感じです。あとで条件などを追加する応用についてもまとめます。
購読?
よくReactivePropertyに限らず、コールバック関数を登録することを「購読(subscribe)」と言います。
これはReactiveX(Rx)というライブラリの思想に由来しています。
Rxでは「値の変化」や「イベントの発生」を“配信される情報”として扱い、それを受け取りたい側が「購読する」という形を取ります。
イメージとしては、ニュースやメルマガを購読するのと同じです。
- 値を発行する側(
Observable) - それを受け取る側(
Observer) - 受け取り登録をすること(
Subscribe)
ReactivePropertyのSubscribeも、この考え方から来ている名前です。
値を読み取る、変化させる
当然ですが、今回は値の変化を監視するためのプログラムですので、値を変化させる必要があります。
GMLにはゲッターとセッターが無いため、ReactivePropertyを使っている場合は関数を使って値の取得と反映を行う必要があります。
// 10ダメージ与える
if (keyboard_check_pressed(vk_space)) {
show_debug_message("[SPACE] key pressed");
var currentHp = playerHp.Get();
var nextHp = currentHp - 10;
playerHp.Set(nextHp);
}
// 名前を別の名前にする
if (keyboard_check_pressed(ord("A"))) {
show_debug_message("[A] key pressed");
playerName.Set("ああああ");
}
と、こんな感じでGet関数とSet関数を用います。
ゲッターとセッターがあればなぁ...。
実行してログを確認する
上記のコードを書いた上で実行し、ログを確認してみると
[SPACE] key pressed
[playerHp] 90
[A] key pressed
[playerName] ああああ
とログが表示されます。
値が変わったときに登録した関数が呼ばれていますね!
いつ使うの?
個人的には、GUI描画で使うケースが多いと思います。
GUIオブジェクトとプレイヤーオブジェクトが別になっている場合などであれば
playerHp = new ReactiveProperty(100);
playerName = new ReactiveProperty("hoge");
drawHp = 0;
drawName = "";
if(instance_exists(objPlayer)){
objPlayer.playerHp.Subscribe(function(_value){
drawHp = _value;
});
objPlayer.playerName.Subscribe(function(_value){
drawName = _value;
});
}
draw_text(16,8,drawName);
draw_text(16,16,string(drawHp));
というように書く事ができます。
このように書けば、毎回DrawEventでobjPlayerにアクセスする必要がなくなり、
描画処理とデータ更新処理を分離することができます。
CreateEventで一度購読しておけば、値が変わった時だけ通知を受け取る形になるため、
コードの見通しも良くなります。
例えば、今までだとこんな感じに書く必要がありましたね。
if(instance_exists(objPlayer)){
draw_text(16,8,objPlayer.playerName);
draw_text(16,16,string(objPlayer.playerHp));
}
このような書き方だと毎回描画イベントに入る度にプレイヤーにアクセスしており、余計な処理が多くなってしまいます。
ReactivePropertyを使えば値が変わった時のみプレイヤーオブジェクトから通知を受け取るだけになるので、処理がシンプルになります。
まとめ
今回は私が作ったGMLのReactiveProperty風のコードを紹介しました。
個人的には結構使いやすいなーと思いますが、UniRxに慣れていないとピンとこないかもしれません...。
ただ、こう言った感じで関数を登録して必要な時だけアクセスする仕組み自体は非常に便利なので、ぜひ活用していただけたらと思います。
次回はこれを使ったMVPパターンのGUI設計についてまとめようと思います!
この先に応用的な使い方についてまとめます。
応用(条件の追加)
ReactivePropertyにはSubscribeで購読させるだけでなく、さらに条件を追加した時のみしか通知しないようにする事ができます。
さらに、それらの条件をメソッドチェーンを使って組み合わせる事で、より限定した時のみ通知させるようにする事ができます。
ここからは応用として、私が用意した処理をサンプルコードと共に紹介します。
Where:条件の追加
条件を満たす値だけを通知します。
一番使う気がします。
// 偶数のみログに出力させる
counter
.Where(function(_val){ return (_val mod 2) == 0; })
.Subscribe(function(_val){
show_debug_message("even: " + string(_val));
});
Select:値の置換
値を変換してから通知します。
// 値を10倍にしてログ出力する
counter
.Select(function(_val){ return _val * 10; })
.Subscribe(function(_val){
show_debug_message("x10: " + string(_val));
});
Where + Select:組み合わせ
条件で絞り込んでから加工します。
このパターンもよく使います。
// HPが30未満になったら警告テキストに変換して通知する
playerHp
.Where(function(_val){ return _val < 30; })
.Select(function(_val){ return "HP低下: " + string(_val); })
.Subscribe(function(_val){
show_debug_message(_val);
});
Pairwise:前回値との比較
前回値と今回値の差分を取得できます。
変化前の値を表示したいときに使えます。
// HPの増減量をログに出力する
playerHp
.Pairwise()
.Subscribe(function(_pair){
var prev = _pair[0];
var curr = _pair[1];
show_debug_message("delta: " + string(curr - prev));
});
Scan:累積計算
値を累積し、その途中経過を通知します。
// counterの値を累積して合計を出力する
counter
.Scan(function(_acc, _val){ return _acc + _val; }, 0, true)
.Subscribe(function(_val){
show_debug_message("sum: " + string(_val));
});
Take:回数制限
指定回数だけ通知します。
数回だけ表示するポップアップUIとかに使えそう。
// 最初の5回だけログ出力する
counter
.Take(5)
.Subscribe(function(_val){
show_debug_message("take: " + string(_val));
});
DistinctUntilChanged:連続同値の除外
同じ値が連続した場合は通知しません。
ただ、Set関数を用いている場合はデフォルトで同じ値の場合は通知されません。
これはSetForce関数を使った場合でも同じ値で通知を行いたくない時に活用できます。
// counterを3で割った余りを出し、連続する同じ値は無視する
counter
.Select(function(_val){ return _val mod 3; })
.DistinctUntilChanged()
.Subscribe(function(_val){
show_debug_message("distinct: " + string(_val));
});
SkipLatestValueOnSubscribe:初回通知のスキップ
Subscribe直後の現在値通知をスキップします。
// 初回値は無視して、変更があった時だけ通知する
message
.SkipLatestValueOnSubscribe()
.Subscribe(function(_val){
show_debug_message("msg: " + string(_val));
});
CombineLatest:複数値の合成
複数のReactivePropertyの最新値を組み合わせます。
どれか1つでも値の変化があったら通知されます。
// scoreAとscoreBの合計を通知する
scoreA
.CombineLatest(scoreB, function(_a, _b){
return _a + _b;
})
.Subscribe(function(_val){
show_debug_message("total: " + string(_val));
});
Merge:ストリームの合流
複数の通知を1つにまとめます。
// scoreAまたはscoreBが変更されたら通知する
scoreA
.Merge(scoreB)
.Subscribe(function(_val){
show_debug_message("merged: " + string(_val));
});
演算子チェーン例
複数の演算子を連結する応用パターンです。
if文であれやこれやまとめるより、こちらの方が用途がわかりやすくで読みやすいと思います。
// 5の倍数だけ通し、100で割った余りを出し、連続同値を除外する
counter
.Where(function(_val){ return (_val mod 5) == 0 && _val > 0; })
.Select(function(_val){ return _val mod 100; })
.DistinctUntilChanged()
.Subscribe(function(_val){
show_debug_message("chained: " + string(_val));
});
Discussion