UniRxを1日勉強してみた感想
結論
まず最初に結論を。ゲーム開発がメインの私的にはObserveEveryValueChanged, Timer, AddTo(安全のため必須)あたりを使えれば十分かなぁという感想。実は、気づいていないすごい便利な使い方とかあるかもしれないので、オススメの使い方がありましたら教えて頂けると嬉しいです。
あと、ここに載せたの以外にも色々とあるらしいのですが、全然キャッチアップできていません(><)
説明
UniRxとはneueccさんが開発されている Reactive Extensions をUnity用に実装した+アルファなライブラリです。
Reactive Extensionsとは、非同期な処理を関数型プログラミングっぽい感じでかけるライブラリです。
例えば、3秒後に○○したいとか、0.5秒間隔で○○したいとか、特定の変数の値が変わった時に○○したいっていうのが簡潔に掛けます。
以下、細かい説明。
Timer
ちょっと遅延させて実行してくれる。今までは遅延させたい時はInvokeを使っていたけど、いちいちそのために関数に切り出してたので、これは便利。
例
var timer = Observable.Timer( System.TimeSpan.FromSeconds( 3 ) ); timer.Subscribe( _ => Debug.Log( "Hello!" ) );//3秒後にプリント
Delay, DelayFrame
ちょっと遅延させて実行してくれる、その2。
こちらは、修飾的(?)なものなので、Observableなオブジェクトに大して後ろからくっつける。
例
var delay = Observable.Return(Unit.Default).Delay( System.TimeSpan.FromSeconds( 3 ) ); delay.Subscribe( _ => Debug.Log( "Delayed log" ) );//3秒後にプリント
var delay = Observable.Return(Unit.Default).DelayFrame( 120 ); delay.Subscribe( _ => Debug.Log( "Delayed log" ) );//120Frame後にプリント
Interval
定期的に実行してくれる。ゲームで制限時間とか表示する時に使えそう。
例
var interval = Observable.Interval( System.TimeSpan.FromSeconds( 1 ) ); interval.Subscribe( x => Debug.Log( ( x + 1 ).ToString() + "秒" ) );//1秒,2秒,3秒...
EveryUpdate
毎フレーム実行してくれる。
例
var every = Observable.EveryUpdate();
every.Subscribe( _ => Debug.Log( "Hello!" ) );
たいていUpdate関数内に掛けば済むので使い所があまり思い浮かばない。
遅延させた後に毎フレーム実行したいときとか?
例
var every = Observable.EveryUpdate(); var delayed_every = every.Delay( System.TimeSpan.FromSeconds( 3 ) ); delayed_every.Subscribe( _ => Debug.Log( "Hello!" ) );
ObserveEveryValueChanged
値が変わった時に処理が走る。使いやすい。他のと違ってgameObjectの拡張メソッドになっているので注意が必要。
例
public int hoge; void Start () { var change = gameObject.ObserveEveryValueChanged( _ => hoge ); change.Subscribe( x => Debug.Log(x) );//hogeの値が変わると、呼ばれる }
ReactiveProperty
ObserveEveryValueChangedのラッパーみたいなの。個人的にはObserveEveryValueChangedの方が使いやすくない?って思う。
例:スコアが変わったら、UI上の表記を変えてる
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UniRx; public class Test : MonoBehaviour { [SerializeField] Text scoreText; ReactiveProperty<int> score = new ReactiveProperty<int>(); // Use this for initialization void Start () { score.Subscribe( x => scoreText.text = x.ToString() ).AddTo( this ); } // Update is called once per frame void Update () { score.Value += 1; } }
OnClickAsObservable
ボタンをクリックしたときの処理が書ける。
例
var on_click = button.OnClickAsObservable(); on_click.Subscribe( _ => Debug.Log("clicked") );//クリックするとclickedがプリントされる
しかし、
button.onClick.AddListener( () => Debug.Log("clicked") );
で同じことができるので、どういうときに有効なのかが今のところ分からない。
Range
引数で指定した値の範囲分、処理を回してくれます。連番の配列を作る時に良いかも。例えば100個あるステージ名の配列を作るとか。
例
const int cMaxStage = 100; string[] stage_names = new string[cMaxStage]; var range = Observable.Range(0,cMaxStage); range.Subscribe( x => stage_names[x] = "stage" + x );
for文を使うよりもかっこいい... っと言えなくもない。
これだけだと、イマイチだけど、範囲指定の処理をいろんな場所で行うなら、一回Rangeを作っておいて、色んな場所で回すってのがスマートにできそう。
Where
処理を行うものを条件でフィルタリングする。if文掛けば済むことが多いけど、こっちの場合フィルターしたものを変数として持ち運べるのが便利。
例
var even_range = range.Where( x => x % 2 == 0 ); even_range.Subscribe( x => stage_names[x] = "stage" + x ); even_range.Subscribe( x => Debug.Log( stage_names[x] ) );//stage0, stage2, stage4 と出力される
Return
値を返すだけのオブザーバブルなオブジェクト。今のところ使用例が思いつかないけど、複雑なことをしたいときの道具として使うんじゃなかろうか。
var ret = Observable.Return( 10 ); ret.Subscribe( x => Debug.Log( x ) );//10
兎にも角にもなんでも良いからオブザーバブルなオブジェクトを作りたいときってときは、引数にUnit.Defaultを取るのが通例の模様。
var ret = Observable.Return( Unit.Default );
ret.Subscribe( x => Debug.Log( x ) );//()
購読停止
Subcribeで走っている処理を止めたい時は、IDisposableのDisposeを呼ぶ。
(using System;してないと、IDisposableなんてありませんよーっていうエラーになるので注意。)
var every = Observable.EveryUpdate();
IDisposable disposer = every.Subscribe( _ => Debug.Log( "Hello!" ) );
disposer.Dispose();
AddTo
AddToの説明の前にUniRxのSubscribeの仕組みについて。
Subscribeを呼ぶとDon't destroyなオブジェクトができるので、UniRxのSubscribeは、おそらくそこにコルーチンを登録しているのだと思う。
そこで、問題なのがコルーチンを登録したままGameObject等を破棄した場合。
別な言い方をすると購読が停止されないままに購読者が死んじゃった場合。
この場合、コルーチンだけ残って処理を続けようとするので、処理的に無駄だったり、死んでしまった購読者にアクセスしようとするからNullアクセス例外が発生したりする。
そういうのを回避するためにあるのが、AddTo。
Subscribe( ... ).AddTo( this )
ってしておけば、thisが死んだ時に登録しておいた処理も破棄してくれる。基本的にSubscribeとAddToはペアで呼ぶものと覚えておいたほうが良さそう。
this以外にも渡せるので、寿命をともにしたいものをAddToすると便利そう。
Unityで作ったアンドロイドアプリが吐くログを確認する方法
にあるadbというコマンドを使うことでログを見ることができます。
Android SDKへのパスは、Android Studioを立ち上げて、「Android Studio」->「Preference」->「Android SDK」として、上の方にあるAndroid SDK Locationというところに書かれています。
自分の場合は、.bash_profileに
を追記して使っています。パスを通した後、
adb logcat
でログを見られます。しかしこれだと、実機側から吐き出されるありとあらゆるログが見えてしまいます。
adb logcat | grep Unity
とすることで、Unityで作ったアプリの吐くログのみを見ることができます。
typeface animatorとoutlineの併用で、ちょっとハマった
Typeface Animatorという超絶簡単にオシャレにテキストを動かせるAssetがあります。
https://www.assetstore.unity3d.com/en/#!/content/37445
先日、こちらのアセットとOutlineコンポーネントと一緒に使ってたらうまく動かなくてハマったのでメモ。
どういうふうになっていたかというと下のような感じにアニメーションしてしまう。
本当は↓のようになって欲しい。
これはコンポーネントの順番を入れ替えれば良いだけでした。
Outline
Typeface Animator
の順になっていたのを...
Typeface Animator
Outline
の順にすればOK
ちなみに、Shadowコンポーネントでも同じようなことが起きるようです。
CosやSinをテーブル引きにする効果
-
-
- -
-
追記 2017/2/8 23:20
各所からツッコミを受けました。
テーブル引きの恩恵を受けるようなコードは、現在の実践環境ではあまりなく、普通に関数を使ったほうが有利というのが説が多いです。
以下のテストはキャッシュが効きやすい状態のテストなので一応、テーブル引きが勝っていますが、結構特殊な状況です。
ほんとうは、ちゃんと実際のゲーム中で計測するべきなんですけど、すいませんm(_ _)m
速度的なメリットは怪しいけどテーブル引きのメリットは、
通信量を落とせるとか(角度をfloatの32bitじゃなく8bitとか16bitで持ちたいとき)
値が実行環境に依存しないとか
というメリットがあると言われ、自分も納得しました。
-
-
- -
-
CosやSinをテーブル引きにする効果
ゲーム制作において、高速化のためにCosやSinといった関数をテーブル引きにするというのは、よくある話である。
自分のゲーム制作環境でも少しでも処理を軽くするためにテーブル引きSin,Cosを作ってみようかと思ったのだけれど、そもそもどれぐらい処理の高速化が見込めるのか、Unity上で試してみた。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestScript : MonoBehaviour { public int loopCount = 10000000; System.Diagnostics.Stopwatch stopWatch = new System.Diagnostics.Stopwatch(); public int tableSize = 360; public const float PI2 = Mathf.PI * 2.0f; float[] cosTable; void Start () { cosTable = new float[ tableSize ]; for( int i = 0; i < tableSize; ++i ) { float theta = (float)i / tableSize * PI2; cosTable[i] = Mathf.Cos( theta ); } } [ContextMenu("Test Mathf")] void TestMathf () { stopWatch.Reset(); stopWatch.Start(); float sum = 0.0f; for( int i = 0; i < loopCount; ++i ) { sum += Mathf.Cos( (float)i ); } stopWatch.Stop(); Debug.Log( "time : " + stopWatch.Elapsed + " sum, " + sum ); } [ContextMenu("Test Cos Table")] void TestCosTable () { stopWatch.Reset(); stopWatch.Start(); float sum = 0.0f; for( int i = 0; i < loopCount; ++i ) { sum += getCosFromTable( (float)i ); } stopWatch.Stop(); Debug.Log( "time : " + stopWatch.Elapsed + " sum, " + sum ); } int getCosTableIndex ( float theta ) { return (int)( theta / PI2 * tableSize ) % tableSize; } float getCosFromTable ( float theta ) { int index = getCosTableIndex( theta ); return cosTable[ index ]; } }
1000万回、Cosを計算して加算した結果、自分のMacbook Pro上で、Mathf.Cosは0.552秒、tableを使ったCosは0.453秒とTableを使ったほうが20%ぐらい高速だった。
もっと差が出るかと思っていたのだけど、そうでもなかった。
tableを使った方は精度がイマイチなのでご利用は計画的に。
UnityException: Unable to install APK! Installation failed. See the Console for details
UnityでAndroid版をBuild And Run しようとした時に、突如として表題の例外が発生するようになって困った。
こちらに解決策が↑
Player Settings -> Other Settingsで、Bundle Version Codeをあげたらインストールできた。
たまにUnityが落ちて、意図せずBundle Version Codeが下がっていることがあるので注意。