unity study

Unityでサクッとモックを作れるようになることを目指して、基本的なテクニックを抑えていく過程を残すブログのつもりだったけど、今はただ自分のトラブルシューティングメモになってるブログ

数値をカウントアップする処理

数値をカウントアップしながらプレゼンテーションする、というのはよくある。
条件として下記の通り。
・一定時間ごとに特定の値ずつ増える。
・最大でもn秒でカウントアップ処理が終わってほしい。
・カウントアップする最小値を指定したい。
・カウントアップする値は、全体としてn秒に収まるように調整する。

var after = 9999;
var before = 111;
// 足し込む最小単位
var addMin = 10;
// 最小時間
var minSec = 0.1f;
// 最大所要時間
var maxSec = 5.0f;
// 所要時間
var revise = Mathf.Max(addMin, after - before);
var time = Mathf.Min(maxSec, revise / addMin * minSec);
// 1回あたりの足し込み量
var add = (long)System.Math.Floor(revise / (time / minSec));
Debug.Log("カウントアップ処理 " + before + " から " + after + " まで, " + minSec + " 秒間隔で " + time + " 秒かけて " + add + " ずつ増やしていく");

var cnt = 0;
Observable.Interval(TimeSpan.FromSeconds(minSec))
	.TakeUntil(Observable.Timer(TimeSpan.FromSeconds(time)))
	.Subscribe(_ => {
		before = Math.Min(after, before + add * cnt++);
		Debug.Log(before);
	}, () => {
		// 端数の場合を考慮し、ストリーム終了時に最終値を出力
		Debug.Log(after);
	});

微妙にちょっとおかしいかも。

【追記】
もうちょい直した。

数値をカウントアップさせる時によくやる処理

あるストリームが発火したタイミングで、別のストリームの最後の値を流すようなストリーム

あるタイミングで発火するstreamAとstreamBがあり、streamAが発火したタイミングでstreamBの最後の値を流したい。
しかしstreamBが発火しても値は流れてこない。
というようなことをやりたいのだが、後者の条件が厄介でZipLatestでもないしCombineLatestでもないし、どうしたものかと考えた結果。

streamA.Select(_ => Time.frameCount)
	.CombineLatest(streamB.Select(x => new Tuple<int, string>(Time.frameCount, x)), (a, b) => a < b.Item1 ? null : b.Item2)
	.Where(x => x != null)
	.Subscribe(x => {});

それぞれのストリームの発生時刻を流して比較することで、どっちから発火したものかを判断して流す流さないを制御するようにした。

2つ並列処理完了待ち合わせをUniRxを使って書いてみたもの


2つのコルーチンの処理完了待ち合わせをUniRxを使って書いてみたもの

コルーチンをIObservableにして、zipして待ち合わせして、それをStartAsCoroutine()でyield returnできるようにしている。
回りくどいことやってるような気もしなくもないが、スコープの広いフラグ変数を使わなくていい分、コードはシンプルで良いと思う。

ついでに、ProcessA()が終わってからProcessB()をやって、そこまで終わるまで待つ場合はこう書く。

yield return Observable.SelectMany(streamA, streamB).StartAsCoroutine();

一度発火してから、一定時間以内に再発火しなかったら終了するストリーム

断続的に値が流れてくるストリームがあり、一定時間以内に値が来たら処理するが、一定時間以上値が流れて来なかったら終了する、という処理をしたいと思った。
例えばスコアを獲得する毎に値が流れてくるストリームがあるとして、これ自体は明確な終了がない。
しかし発火したタイミングで画面に表示を出したいが、表示するのは直近の1つだけにしたい。
更に表示から一定時間経過したら非表示にしたい。
というようなことをやる場合に、まぁ無理にストリームでやらなくても処理はできるのだが、スマートに書けないものか、と考えてみた。
で、こうなった。
例として、ボタン押下毎に発火するが、一定時間以上発火がなければ終了とするストリーム。

var tapStream = this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(0));
tapStream
	.Select(_ => Time.frameCount)
	.TakeUntil(tapStream.Throttle(TimeSpan.FromSeconds(1)))
	.DoOnCompleted(() => Debug.Log("complete"))
	.RepeatUntilDestroy(this)
	.Subscribe(x => Debug.Log("frameCount: " + x)));

このThrottleさせたストリームを条件に使う、ってのがなかなかパッと思い浮かばなかったりするんだよなぁ。
Bufferの条件に使ったりとか、うまく使うと色々やりたいことをスマートに書けるので便利。

【追記】
上記の場合はUpdateAsObservableに対してRepeatするからいいけど、ソースがReactivePropertyだともうちょっとなんか考えないとダメっぽいな。

PHPのimplode()みたいなことを、LINQを使って実現する

例えば
var ids = new int { 1, 2, 3};
みたいな内容を
1, 2, 3
とログ出力したいことがちょくちょくある。
PHPだと
implode(',', ids);
でいけるんだが、C#でスラっと書けんかな、と思ってLINQで表現。。
ids.Select(x => x.ToString()).Aggregate((x, y) => x + ", " + y);

idsがstringなら
string.Join(",", ids);
の方がシンプルなんだけどな。

UniRxを使ったフリック検出

以前にもフリック検出するストリームを書きたいと思って取り組んだことがあったが、その際のやつはどうにも綺麗にまとまらなかった。
Downされてから一定時間以内にUpしないものは検出しない、という条件を落としこむのに苦戦したんだったかな。
ふと思い立って再挑戦してみたが、今回のはわりと綺麗にまとまったように思う。
ポイントはSelectManyの部分だと思うけど、うまく説明できるほど理解しきれてもいないんだよなぁ。

var start = this.UpdateAsObservable()
	.Where(_ => Input.GetMouseButtonDown(0))
	.Select(_ => Input.mousePosition);
var end = this.UpdateAsObservable()
	// 一定時間以内にUpされないと検出しない
	.TakeUntil(Observable.Timer(TimeSpan.FromSeconds(1)))
	.Where(_ => Input.GetMouseButtonUp(0))
	.Select(_ => Input.mousePosition)
	.Take(1);

start.SelectMany(startPos => end.Select(endPos => startPos - endPos))
	.Subscribe(v => {
		// 一定距離以上動かさないとクリック扱い
		if (v.magnitude < 50) {
			Debug.Log("Click");
		} else if (Math.Abs(v.x) > Mathf.Abs(v.y)) {
			if (v.x > 0) {
				Debug.Log("Left");
			} else {
				Debug.Log("Right");
			}
		} else {
			if (v.y > 0) {
				Debug.Log("Down");
			} else {
				Debug.Log("Up");
			}
		}
	});

PhotonUnityNetworkingでOnJoinedRoom()前にOnPhotonPlayerPropertiesChanged(object[])が呼ばれる?

Unity5.3.5f1 + PUN v1.69で確認。
既存ルームにJoinした際に、OnJoinedRoom()よりも先に既にルームにいるプレイヤーのOnPhotonPlayerPropertiesChanged(object)が呼ばれているっぽい?
既にルームいるプレイヤーがSetCustomProperties()を呼んでいるかどうかに関わらず呼ばれる。
それだけならまぁいいんだけど(微妙に都合悪いけど)、困ったことにこの際に渡されるPhotonPlayerの情報が正しくないっぽい。
確かにIDとかは既存プレイヤーのものなんだが、isMasterClientがtrueのはずなのにfalseになっていたりする。
OnJoinedRoom()後に既存プレイヤー側からSetCustomProperties()を呼んでみると、その際のOnPhotonPlayerPropertiesChanged(object
)に渡されてくるものは正しい。
う〜ん、なんなんだろうこれ。
微妙に初期化途中のデータが流れてきているのか?
APIDoc見る限りでは、どうもそれらしい記述が見当たらんのだが。

PhotonRx使っているので、対応として下記のような感じにしてみた。
GitHub - TORISOUP/PhotonRx: PUNをUniRxで扱いやすくするライブラリ

this.OnPhotonPlayerPropertiesChangedAsObservable()
	.SkipUntil(this.OnJoinedRoomAsObservable())
	.TakeUntil(this.OnLeftRoomAsObservable())
	.Repeat()
	.Subscribe(x => {
	});

ちなみに OnPhotonCustomRoomPropertiesChanged() もちょっと怪しい気がする。