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で作ったアンドロイドアプリが吐くログを確認する方法

Android SDKへのパス/platform-tools

にあるadbというコマンドを使うことでログを見ることができます。


Android SDKへのパスは、Android Studioを立ち上げて、「Android Studio」->「Preference」->「Android SDK」として、上の方にあるAndroid SDK Locationというところに書かれています。

f:id:wkpn:20170116202059p:plain

自分の場合は、.bash_profileに

export PATH=/Android SDKへのパス/platform-tools:$PATH

を追記して使っています。パスを通した後、

adb logcat

でログを見られます。しかしこれだと、実機側から吐き出されるありとあらゆるログが見えてしまいます。

adb logcat | grep Unity

とすることで、Unityで作ったアプリの吐くログのみを見ることができます。

【追記】

/Library/Android/sdk/tools/monitor
を使うとGUIで見られる。
最初Deviceが認識されなくて見られなかったけど、monitor立ち上げてからDeviceを接続し直したらいけた。

typeface animatorとoutlineの併用で、ちょっとハマった

Typeface Animatorという超絶簡単にオシャレにテキストを動かせるAssetがあります。

https://www.assetstore.unity3d.com/en/#!/content/37445

先日、こちらのアセットとOutlineコンポーネントと一緒に使ってたらうまく動かなくてハマったのでメモ。

どういうふうになっていたかというと下のような感じにアニメーションしてしまう。

f:id:wkpn:20170208204245g:plain

本当は↓のようになって欲しい。

f:id:wkpn:20170208204314g:plain

これはコンポーネントの順番を入れ替えれば良いだけでした。

f:id:wkpn:20170208204352p:plain
Outline
Typeface Animator
の順になっていたのを...

f:id:wkpn:20170208204404p:plain
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上で試してみた。

github.com

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 しようとした時に、突如として表題の例外が発生するようになって困った。

answers.unity3d.com

こちらに解決策が↑

Player Settings -> Other Settingsで、Bundle Version Codeをあげたらインストールできた。

たまにUnityが落ちて、意図せずBundle Version Codeが下がっていることがあるので注意。

use of undeclared identifier 'Unity' とか 'UI' のエラー

UnityでiOS版のビルド時に、Scripting BackendをMono2xにしてStriping Levelを何某かにしていると、以下のようにXCode側でエラーが出ることがあるもよう。

f:id:wkpn:20170116200832p:plain

とりあえずstripping levelをdisabledにすれば、一応回避できる。

Mono2xでのビルドは、基本デバッグ用途でしか無いので、とりあえずこれで良いかな。

XCodeでビルドした時に「Appの有効なaps-environmentエンタイトルメント文字列が見つかりません」というエラーが出る

XCodeでビルドした時に「Appの有効なaps-environmentエンタイトルメント文字列が見つかりません」というエラーが出た。

これは証明書にPush Notificationが無いかららしい。

最近のXCodeは「Automatically manage signing」にチェックを入れておくと証明書を勝手に作ってくれるので、Capabilityの設定部分でPush Notification項目をONにすれば勝手に証明書側も更新してくれる。

f:id:wkpn:20170116200204p:plain