imog

主にUnityとかの事を書いています

RxJavaリアクティブプログラミング読んでる 1

3月からAndroidやることになったので、先週くらいからRxJavaリアクティブプログラミングを電車の中でちまちま読んでいる

https://www.amazon.co.jp/dp/4798149519

ReactiveExtensionsはUniRxで触ってたのでめちゃくちゃ詰まるということはないが、知らないことが色々書いてたのでログを残しとこうと思った。 間違いも結構あると思いますがご容赦ください。ツッコミはいただけると嬉しいです。

ReactiveStream

そもそもReactiveExtensionsではなく、ReactiveStreamの話をメインにしている。

http://www.reactive-streams.org/

.NETのReactiveExtensionsが非常に便利で様々な言語に移植されていき、Rxをベースとしたさまざまなフレームワークやライブラリが生まれてきたけど、 これらは内部実装が様々で、データストリームを扱うという目的は同じながら、使うライブラリで実装方法を変えねばならぬという問題が起きていたらしい。 そこで、データストリームの非同期通信の仕組みの実装方法をある程度共通化しましょうということでReactiveStreamというインタフェースを定めたらしい。

そして、ReactiveStreamのJava版がReactiveStreamJVM

https://github.com/reactive-streams/reactive-streams-jvm

4ファイルしかなくて、本当に最低限のインタフェースを設けてるだけだった。

RxJava 2.xは上記のインタフェースを実装している。

FlowableとSubscriber

この本ではストリーム流す側と購読する側を「生産者」「消費者」と読んでる。

RxJava1.xやUniRxの時は Observable が生産者で Observer が消費者。

RxJava2.xだと FlowableSubscriber がそれにあたる。

基本的には同じだったけど、大きな違いとして消費者側が購読のハンドリングができるかどうかと感じた。 前者だと、subscribeしたときにどのタイミングで止めるかというのは消費者側は操作しにくかった。

Take(n)などのオペレータで管理はできるけど、それは生産者側の処理なのでちょっと違う。 Disposeを呼び出して止めるはできたけど、それはストリーム外からの操作になるのでちょっと怖いという感じだった。 なので、購読を始めると生産者が流すのを終えるまで購読をやめない!となる。

しかし、ReactiveStreamはSubscriptionインタフェースを提供している。

https://github.com/reactive-streams/reactive-streams-jvm/blob/master/api/src/main/java/org/reactivestreams/Subscription.java

Subscriptionは生産者と消費者の間に立つインタフェースで、生産者にデータをリクエストする request と購読を解除する cancel を持っている。

これらをSubScriberが呼び出すことで、途中でも消費者側の都合で中止できる。これは良いものだと思う。ストリーム内で完結させやすくなりそう。

そういった理由か、2.x以降に実装されたsubscribeはIDisposableを返さなくなってる。 だけど、1.x時代のIDisposableを返すsubScribeは引き続き残っているみたい。

バックプレッシャー

本書で頻繁に出てくる言葉。かなり理解が怪しい。

消費者が通知を受け取れない状況のときにデータをどうするかという設定をバックプレッシャーと呼ぶみたい。 通知ができるまで貯めておくか、通知ができるまで破棄していくかなど。

受け取れない状態というのは例えば非同期で生産者が怒涛の勢いでデータを流してきてさばけないなど。

そういう意味だと非同期の時以外気にしない設定じゃないかなあと思ってる。

この辺、はじめの方からカジュアルに単語が出てきてるけど詳細はChapter3なのでまた後に書くことになりそう。

ちなみにやっとChapter1が終わりました。先は長い。

2016年振り返り

年の瀬なのでやってきたことを振り返る

OSS

小物でも良いので思いついたら作っとけみたいなノリで、最小限のアセットをいくつか作った。

SimplyPopup

https://github.com/adarapata/SimplyPopup

adarapata.hatenablog.com

簡単に使えるポップアップのスクリプト。自分で割と使ってる。

Mikko

https://github.com/adarapata/Mikko

adarapata.hatenablog.com

タッチ動作をエディタ、スマホスタンドアローンで共通化するやつ。 名前はミッコ。天下のクリスティー式です。

ただ、普通にEventSystems使えばだいたいなんとかなるので不要だなという感じになった。

ArborRx

https://github.com/adarapata/ArborRx

adarapata.hatenablog.com

ステートマシーンエディタのArbor2でObservableなスクリプトGUIで噛ませられる拡張。ちょっとやってみたくて作った。

これ作る際にエディタ拡張でReflection多用するという甘い誘惑に乗ってしまった。

LineTrace

https://github.com/adarapata/LineTrace

adarapata.hatenablog.com

だいぶ前に作ったライントレースシステムをゲームから切り離してみたやつ。ペパボアドカレで3Dゲームについて語れる機会があったので公開。

これ結構便利なので使ってみてほしい。

ハッカソンで作った系

リンク切れとか公式のレポートがないのとかあって、データ残ってるの少ない・・

Drive428

Gandhism | Global Game Jam®

GlobalGameJam2016でみんなで作ったやつ。 ハロウィンに悪魔達に支配された渋谷を車で爆走して轢いて回る平和主義ゲーム

この悪魔達、いつもは向かってくるけど車が走ってる時は逃げるという性格をしてます。

地味にRailsでランキングサーバ立ててオンラインでスコア競えるようにしてたけどサーバのお試し期間切れたのでもうできません。

VRHanasaka

adarapata.hatenablog.com

Oculus+Myoで花咲か爺さんとなって右手から桜を咲かせる波動を、左手から鬼を滅する波動を繰り出して花を咲かせるゲーム。

人生で初めてVRゲーム作りました。Myoのキャリブレーションがひたすら面倒だった。

お前が!ロボに!なるんだよ!

www.facebook.com

公式レポートがないんでFBの写真のみになってしまったが、11月のVRゲームジャムで作ったVRゲームです。

connpass.com

人間側とロボット側で協力して敵ロボットを倒す熱いゲームです。ロボット側は視界はOculusで見渡せるものの自分で移動ができないので、見える情報を人間に伝えて操作してもらいます。

また、ロボットはUnlimitedHandを装着しており敵ロボに攻撃されると電流が走ります。割と痛い。

VRゲーム2回目ですが、今回はやりたいことやれて満足です。

ジントリオーク

adarapata.hatenablog.com

八耐で作ったやつ。 スプラなんとかが流行ってたので陣取り面白いなと思い作ったやつ。

割とシステムは気に入ってるのでどっかのタイミングで作り直したい。

iOSアプリ

リビングデイブ

リビングデイブ

リビングデイブ

  • Hiroto Imoto
  • ゲーム
  • 無料

会社のお産合宿というイベントで公開したiOSゲーム。仕事を終えて退勤したいデイブが社員の目をかいくぐり退勤するゲームです。

何気に人生初のiOSアプリリリースでした。

ペパラボ

f:id:adarapata328:20161226223847p:plain こちらは社内限定で公開したもの。 弊社を退職される先輩にプレゼントとして作ったアプリです。

「ばいきんラボ」というiOSアプリと全く同じシステムを再現してイラストを差し替えました。 f:id:adarapata328:20161226223835p:plain

クリアするとみんなからの送別メッセージが見れる。 メッセージはゲームからapiサーバを叩いて順次アンロックして見れるような仕組みです。

f:id:adarapata328:20161226223841p:plain

f:id:adarapata328:20161226223839p:plain

思い立って二週間くらいで友人と二人で急ピッチで作った記憶ある。

イベント登壇など

UnityFukuoka12

speakerdeck.com

不定期でやってるUnityFukuokaでマルチシーンエディットについて話しました。 マルチシーンはいいぞ。

趣味系

所感

OSSが4つ、公開したゲームが5つ(非公開合わせるともっとある)。去年と比べると少しペースが上がったかなあという感じ。

ただOSSとかは後半に偏ってるのでどのタイミングでやる気になったかが丸わかりである。

目標にしていた「AppStoreにiOSアプリをリリースする」が達成できたので個人的には嬉しい。しかし、世に出してる個人ゲームアプリ開発者には3ヶ月に1本リリースとか尋常ならざる速度の人種がいたりするので、年一本はまだまだだなあという感じがする。

ペース上げるためには効率的な開発手法を覚えていかなくちゃいけないのでひたすら調べる、書く、考えるの繰り返しをするしかないなあと思う。

あと、外部に公開しているもののリンク切れが激しいので、自分の作ったものをまとめるポートフォリオサイト作らないと全然伝わらないなと思った。

来年やりたいこと

全部というか、どれか。

アプリ2本リリース

デイブシリーズとかで続きもの作っていくのは面白そう。 ついでに作るたびにどっかで内部実装の話とかしたい。

友達増やす

ぼっちみたいな言い方になってしまったけどそうではなく。

ゲーム開発者、ソーシャル上にはいっぱいいるが周りで探すとそうそういるものではない。 その数少ない人を探すためにイベントに顔出したり、UnityFukuokaとかで啓蒙活動していく。

同じ方向向いてる人を見ることで心の安寧が保たれる。

Unity案件引受ける

勉強会とかイベントに遊びに行くのを繰り返すうち、客観的に自分がどのくらいのスキルを持つのかが気になってきた。

弊社は副業OKなので、本業に支障のない範囲で、実際の仕事として挑戦してみたい。

ということでお仕事募集中です。

今年もお疲れ様でした。

Unityで3D横スクロールの挙動を考える

Unityで3D横スクロールの挙動を作る

これはpepabo Advent Calendar 2016 - Qiitaの15日目の記事です。

昨日は id:yutokyokutyoコードレビューするのが怖いと思っていたエンジニアが半年間コードレビューを経験して思った 10 のことでした。

ところでみなさんはロックマンXシリーズが好きですよね。

www.youtube.com

僕は大好きです。なのでみなさんも大好きです。

ロックマンXシリーズはX~X6まではドット絵の2D横スクロールアクションゲームでした。 X7から完全な3Dとなり、今までなかった手前や奥の概念が入りました。 それが不評だったかは定かではありませんが、X8は3Dだけども移動は旧来の左右のみの2D移動となりました。

こういうのを3D横スクロールと呼ぶらしいです。

3D横スクロールの良いところは前進後退の二択しかないので操作が簡単になる、且つ3D空間を動き回るので見栄えが良いという点だと思います。

今回はこの3D横スクロールのキャラの挙動を考えてみます。

説明はいいから使うぞという方はこちらのアセットをご利用ください。 github.com

後半もう一回リンク貼ります。

要件

  • 3D空間である
  • キャラクターは進むルートが決まっている
  • 進む方向は前・後の二種類

ざっくり考えるとこんな感じ。

キャラクターは進むルートが決まっている

これを実現するには、歩いてほしいルートの要所要所の位置情報を持たせておいて、AからBまで直進、到着したらBからCまで直進、CからD... といった感じにするのが一番わかりやすいかなと思います。

いわゆる WayPoint と呼ばれる位置情報の配列を用意して、それに沿って移動させるということです。 UnityだとWayPointを置く場合空のGameObjectを配置すると楽です。

linetrace 2 上記のgifで見える赤い点がWayPointで、それらを線で繋いでます。 Boxがひたすら点の位置に向かって進む、通過したら次の点まで直進してるのがわかります。

AからBまで向かうのにどの方向を向けばいいかはベクトルの減算(B-A)を正規化すれば求められます。

Unityなら以下の感じ

Vector3 a = transform.position;
Vector3 b = target.transform.position;
Vector3 direction = (b - a).normalized;

transform.LookAtでも同じことはできますがオブジェクトの向き自体が変わってしまうのでご注意を。

これで向きは定まったので、directionにスカラー値speedを乗算して目標地点に向かわせることができます。 で、接近したら次の目的地を渡して、directionの再計算。基本的にこれの繰り返しです。

  • 現在位置Aから目的地Bの向きを求めて移動する
  • 現在位置と目的地の距離がn以下になったら到着とみなす
  • 次の目的地Cを参照する。以降繰り返し。

一方通行であれば配列一つでなんとかなるので楽です。

進む方向は前・後の二種類

実際のゲームになると前進だけじゃなくて後ろに戻るパターンもあります。

一方通行だとA->B、B->Cだけ考えればよかったのですがB->A、C->Bも考える必要があります。 欲しいのは現在の自分からの、前方の目的地、後方の目的地情報です。 この辺をクラス化してまとめていくとよさそう。

class WayPoint {
    public Vector3 position;
    WayPoint next, prev;
}

自身の位置情報、その前後のWayPoint情報を持つシンプルなクラス。 ただ、個人的に理解しにくかったです。

というのも、A->Bのちょうど中間地点にいたときに自分はどこの点にいると言える?という点でもやってしまいました。

点から点へ移動するゲームであればWayPointの概念がわかりやすいのですが、ロックマンのようなゲームでは点はあくまで通過点に過ぎず、点と点を繋ぐ線の間を自由移動するようなイメージが強いです。

点と点の間で止まる可能性もあるし、点の間で反対方向のWayPointに向く場合もある。

というのを踏まえると、WayPointの概念をうまいこと包み込んで線で管理するクラスのがよいのでは。ということで下記のLineクラスを定義。

public class Line
{
    public Line next, prev;
    public Vector3 front, back;

    public Line(Vector3 f, Vector3 b)
    {
        front = f;
        back = b;
    }

    /// <summary>
    /// 任意の向きの目的地を返す
    /// </summary>
    /// <param name="d"></param>
    /// <returns></returns>
    public Vector3 GetWayPointByDirection(Direction d)
    {
        return d == Direction.front ? front : back;
    }

    /// <summary>
    /// 任意の向きの次の線を返す
    /// </summary>
    /// <param name="d"></param>
    /// <returns></returns>
    public Line GetNextLineByDirection(Direction d)
    {
        return d == Direction.front ? next : prev;
    }
}
public enum Direction
{
        front,
        back
}

WayPoint情報をfront,backに格納した、二点間の線を表すクラスです。 このクラスの仕事は、自身のLine上のキャラクタが前進・後退する場合にどこを目指せばいいのかというのを教えることです。 もらったWayPointに接近したら次のLineを取得する。の繰り返し。

実際にできたアセット

こんなものができました。

github.com

WayPointを置いたら、その方向にオブジェクトを移動させるアセットです。

使い方

先にUniRxをインポートしてください https://www.assetstore.unity3d.com/jp/#!/content/17276

  • LineManagerを適当なスクリプトに貼り付けて、インスペクタからWayPoints配列の数を設定します

  • WayPointのための空のオブジェクトを生成して、インスペクタからWayPointsの配列に入れます。この時点でシーン上では線が表示されます。

  • 動かしたいオブジェクトに DirectionController2d をアタッチします

  • 実際に移動するスクリプトを書きます。 例としてはこんな感じ。
using UnityEngine;
using LineTrace;

public class Move : MonoBehaviour
{
    public DirectionController2d controller;
    public float speed;

    void Update () {
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            // 向きを設定する
            controller.direction = Direction.back;
            transform.position += controller.forward*speed*Time.deltaTime;
        }
        else if (Input.GetKey(KeyCode.RightArrow))
        {
            // 向きを設定する
            controller.direction = Direction.front;
            transform.position += controller.forward * speed * Time.deltaTime;
        }
    }
}

キーボードの十字キーで右側、左側にいい感じに動いてくれます。

linetrace 2

実装側は DirectionController2d に向き direction を与えることと、現在の向きベクトル forward を取得する以外特に何もしなくていいです。

LineManager がWayPointからよしなに Line クラスを生成して、 DirectionController2d クラスが現在自身が乗っている Line を逐一チェックして更新してくれます。

ちなみに LineManagercycle にチェックを入れるとWayPointの始点と終点を繋ぐので無限にぐるぐるできます。

linetrace 2

DirectionController2dautoRotation チェックを入れると、目的地の方向に自動で回転します。回転速度は rotateSpeed の値を参照します。

欠点

このアセットは二点間の距離を調べるときに高さ(Y軸)を見ません。 なぜ高さを省いたかというと、高さまで考慮すると通過される可能性があったためです。

例えばWayPointAが次の目的地だったとして、Aを飛び越えるような感じで大きくジャンプした場合に、距離算出だけだと目的地にたどり着いてない判断をされる可能性があります。 そのためY軸を潰して、高さ情報抜きで接近したかを判別してます。

DirectionController2d の一部を抜粋

this.UpdateAsObservable()
     // 目的のポイントに接近したかどうか
      .Where(_ =>
          Mathf.Abs(Vector2.Distance(transform.position.XZ(),
          current.GetWayPointByDirection(mDirection).XZ())) < distance)
      .Subscribe(_ =>
      {
            var next = current.GetNextLineByDirection(mDirection);
            current = next ?? current;
            direction = mDirection;
      });

transform.position.XZ() はVector3クラスの拡張メソッドです。X,Zの値でVector2を作ります。 こんな感じでY軸を潰して距離算出、distance以下であれば次のLineを取得・・といったことをしています。

なので、このスクリプトは上に登っていくようなゲームには向いてないです。横スクロール用でお使いください。

その代わり、回転については処理が多少簡単になっています。

this.UpdateAsObservable()
    .Where(_ => forward != Vector3.zero)
    .Subscribe(_ =>
    {
        var arrivedForward = forward.XZ();
        var cross = transform.forward.XZ().Cross(arrivedForward);
        var dot = Vector2.Dot(transform.forward.XZ(), arrivedForward);

        // ほぼ目的の方向向いた場合に回転させない
        if (dot < 0.98F)
        {
            transform.Rotate(Vector3.down, rotateSpeed * Mathf.Sign(cross) * Time.deltaTime);
        }
    });

自身の現在の向きと向きたい方向ベクトルの外積 cross を出します。 二次元ベクトルの外積は便利で、A x B の時、ベクトルAから見てベクトルBが右側にあるのか、左側にあるのかを正・負で算出できます。 これで回転すべき方向がわかります。が、これだと自身が目的の方向を向いているのかわからないので内積 dot を出します。 正規化した二次元ベクトルの内積 A・B は二つのベクトルのなす角度を -1~1 で求められます。0が直交、1が並行、-1が真逆のベクトルということですね。

crossで向きはわかったので、dotが一定以下である場合に回転させる、ということをしています。

備考:カメラ

これで移動処理は概ね実装できますが、ゲームとなるとカメラもいい感じに追従して欲しくなります。 カメラも基本原理は同じで、WayPointを置いて動かすというスタンスで行けそうですが、キャラがどの位置にいるときにここにカメラがあってほしい、みたいなマッピングがあるので結構大変そうです。 また、アーティストが入ってくる部分でもあるので簡単に弄れるようにしたかったりもします。

割と大変な作業なので、僕はCameraPathAnimatorというアセットを使うことが多いです。

www.youtube.com

配置がかなりお手軽なのでかなりコスト削減できます。 対象を注視しながら移動ということもできるので横スクロール系だとこれがメインの使い方になりそう。 上記リンクもしくはここからアセットを購入すると僕に報酬が入るシステムなので皆様是非お買い求めください。

ちなみにWayPointシステムも、SimpleWayPointSystem という便利なアセットを使えばサクッと実装できます。こちらはルートをベジエ曲線とかスプライ曲線などで細かく設定できるのがポイントです。

じゃあこの記事はなんだったんでしょうね。

明日は @genkiroid の番です genkiroid.github.io

Unityでシーンをキャッシュする仕組み

2時間遅れの公開になります。遅れてしまった・・。

こちらはUnityアドベントカレンダー、12/5の記事です。

qiita.com

前回は凹さんの「Unity で Windows のデスクトップ画面をテクスチャとして表示するプラグインを作ってみた」でした。

tips.hecomi.com

シーンを保持したい問題

Unityでよく悩む問題として、シーン遷移時に前の状態に戻したいということがたびたびあります。

  • シーンAからシーンBに遷移
  • Bからキャンセル等の動作でAに戻る
  • Aのカーソル位置や状態が初期状態に戻る

RPGのフィールド画面 -> 戦闘画面 -> フィールド画面とか パーティゲームのキャラ選択 -> ステージ選択キャンセル -> キャラ選択に戻る

とか。 つまり、前の状態を保持したままシーン遷移を行いたくなることが多いです。 やり方としては、パッと2パターンくらい浮かびました。

  • シーン遷移時に必要な情報をシリアライズしてシーンを破棄、再読み込み時に流し込む
  • シーンを破棄せずまるごと非アクティブ化。再読み込み時にアクティブにする

前者の方がメモリを食わずに済むので便利そうですが、何をシリアライズするか具体的に決まっていないと作れなさそうな気がしました。 後者は汎用的に使える仕組みではありますが、シーンが残ったままなのでメモリが心配です。特にモバイルだと処理落ちしないかちょっと心配。

今回はこんなやり方どうよというところを紹介したいので、後者を実装します。

シーンのキャッシュ

結論から言うとこんな感じにできました。

以下のことができます。

  • 任意のシーンにロードして以前のシーンを非アクティブ化。キャッシュがある場合はシーンをアクティブ化。
  • 任意の複数のシーンをロードして合成、一つのシーンとしてキャッシュする

追加したクラスは2つで、シーンのロードを担当する SceneManagerWithCache と、シーンの状態を管理する SceneProperty です。

シーンの状態を持つSceneProperty

using UnityEngine;
using UniRx;

public class SceneProperty : MonoBehaviour
{
    public bool cacheable;
    public string sceneName { get { return gameObject.scene.name; } }

    private Subject<Unit> sceneEnabledStream = new Subject<Unit>();
    public IObservable<Unit> sceneEnabledAsObservable
    {
        get { return sceneEnabledStream.AsObservable(); }
    }

    private Subject<Unit> sceneDisabledStream = new Subject<Unit>();
    public IObservable<Unit> sceneDisabledAsObservable
    {
        get { return sceneDisabledStream.AsObservable(); }
    }

    public void Disable()
    {
        sceneDisabledStream.OnNext(Unit.Default);
        foreach (var root in gameObject.scene.GetRootGameObjects())
        {
            root.SetActive(false);
        }
    }

    public void Enable()
    {
        sceneEnabledStream.OnNext(Unit.Default);
        foreach (var root in gameObject.scene.GetRootGameObjects())
        {
            root.SetActive(true);
        }
    }
}

ScenePropertyはシーンのRootのオブジェクトに貼ります。 役割は以下です。

  • シーンがキャッシュできるかどうかの設定を持つ
  • シーンの有効化、無効化処理のインタフェース

例えば絶対に使いまわししないであろうシーン(スコアをサーバに送信したりとか)の場合はcacheableのチェックを外しておき、シーンが使われなくなった時点で削除するようにします。 Enable,Disableメソッドを外部から叩いて、シーン全体のアクティブ、非アクティブ化を行います。 Rootオブジェクトに貼り付けてるのに GetRootGameObjects でルートを取得するのは、後述の複数シーン合成に対応するためです。 ちなみに、アクティブ、非アクティブのタイミングでストリームを発行するので各種シーン側でスクリプトを噛ませてごにょごにょできます。

シーンのロードを担当する SceneManagerWithCache

using System.Collections;
using System.Linq;
using UnityEngine;
using UniRx;
using UnityEngine.SceneManagement;

public class SceneManagerWithCache : MonoBehaviour
{
    [SerializeField]
    private int sceneCacheCapacity = 3;

    public ReactiveCollection<SceneProperty> scenes = new ReactiveCollection<SceneProperty>();

    private void Awake()
    {
        var sceneAddAsObservable = scenes.ObserveAdd()
                                         .Publish()
                                         .RefCount();
        sceneAddAsObservable
            .Where(s => s.Index >= sceneCacheCapacity)
            .Subscribe(s =>
            {
                var unloadScene = scenes.First();
                scenes.Remove(unloadScene);
                SceneManager.UnloadSceneAsync(unloadScene.gameObject.scene);
            });

        sceneAddAsObservable
            .Select(currentScene => scenes.Where(s => !(s.cacheable | s.Equals(currentScene.Value))))
            .Where(unCachedScens => unCachedScens.Count() > 0)
            .Subscribe(unCachedScens =>
            {
                for(int i = 0; i < unCachedScens.Count(); i++)
                {
                    var s = unCachedScens.ToList()[i];
                    scenes.Remove(s);
                    SceneManager.UnloadSceneAsync(s.sceneName);
                }
            });
    }

    public IObservable<SceneProperty> LoadMergedScene(string baseSceneName, bool useCache, params string[] subSceneNames)
    {
        if (useCache)
        {
            IObservable<SceneProperty> propertyObservable = GetCacheScene(baseSceneName);
            if (propertyObservable != null)
            {
                return propertyObservable;
            }
        }

        return LoadScene(baseSceneName, useCache)
            .Take(1)
            .Select(p => p.gameObject.scene)
            .Concat(Observable.FromCoroutine<Scene>(observer => LoadSubSceneCoroutine(observer, subSceneNames)))
            .Do(loadedScene =>
            {
                var baseScene = SceneManager.GetSceneByName(baseSceneName);
                SceneManager.MergeScenes(loadedScene, baseScene);
            })
            .Select(_ => scenes.Last(s => s.sceneName == baseSceneName));
    }

    private IEnumerator LoadSubSceneCoroutine(IObserver<Scene> observer, params string[] subScenes)
    {
        foreach (var s in subScenes)
        {
            yield return SceneManager.LoadSceneAsync(s, LoadSceneMode.Additive)
                .AsObservable()
                .ToYieldInstruction();
            observer.OnNext(SceneManager.GetSceneByName(s));
        }
        observer.OnCompleted();
    }

    public IObservable<SceneProperty> LoadScene(string name, bool useCache = true)
    {
        if (useCache)
        {
            IObservable<SceneProperty> propertyObservable = GetCacheScene(name);
            if (propertyObservable != null)
            {
                return propertyObservable;
            }
        }

        return SceneLoadedAsObservable(name);
    }

    private IObservable<SceneProperty> GetCacheScene(string sceneName)
    {
        var scene = scenes.LastOrDefault(s => s.sceneName == sceneName && s.cacheable);
        if (scene == null)
        {
            return null;
        }
        scene.Enable();
        var beforeScene = scenes.Last();
        if (beforeScene != null)
        {
            beforeScene.Disable();
        }
        scenes.Remove(scene);
        scenes.Add(scene);
        return Observable.Return<SceneProperty>(scene);
    }

    private IObservable<SceneProperty> SceneLoadedAsObservable(string name)
    {
        return SceneManager.LoadSceneAsync(name, LoadSceneMode.Additive)
                  .AsObservable()
                  .Select(_ => SceneManager.GetSceneByName(name))
                  .Select(scene => scene.GetRootGameObjects().First().GetComponent<SceneProperty>())
                  .Do(property =>
                  {
                      var beforeScene = scenes.LastOrDefault();
                      if (beforeScene != null)
                      {
                          beforeScene.Disable();
                      }
                      scenes.Add(property);
                  });
    }
}

100行超えてつらい。

外部から呼べるメソッドは二つです。

  • LoadScene
  • LoadMergedScene

LoadSceneSceneManager.LoadScene と同じ感覚で、シーン名を指定してロードします。 通常時と違うのは、キャッシュを使うか否かbooleanで渡せるところです。 useCache = trueで渡すと、すでにシーンが非アクティブで存在する場合そちらを有効化します。 見つからなかった場合は通常のシーン読み込みが行われます。

内部的にはロード時は以下のフローです

  • キャッシュが無い場合SceneManager.LoadSceneAsync でシーンロード
  • キャッシュがある場合、該当シーンから ScenePropertyを取得して有効化
  • 直前のシーンを無効化
  • シーン管理リストに追加
  • キャッシュ数上限を超える or キャッシュしないシーンをリストから削除、 SceneManager.UnLoadAsyncヒエラルキーから削除

呼び出し元には IObservable<SceneProperty> を返して、ロード完了後にイベントを発行します。 完了後のなにかしらはシーン側にお任せします。

キャッシュ数上限は sceneCacheCapacity が保持しており、インスペクタ上から設定できます。

LoadMergedScene は、シーン名を指定してロードするのは同じですが、こちらは複数ロードすることができます。 ベースとなるシーンAと、可変長引数のシーン名配列を渡すことで、該当のシーンをすべてロードした上でシーンAに統合してくれます。

これは、1つのシーンを機能ごとに分割して管理をしている場合に有効です。 例えばデザイナと分担するためにシステムとUIをシーン分けて作業しているけど、実際に使うときは完全に統合したいんだよみたいなときとか。 どういうシチュエーションだろうか、というところは前回のエントリなどを見ていただけるとイメージがつかめるかもしれません。

adarapata.hatenablog.com

こちらも最終的な返り値は IObservable<SceneProperty> なので通常のシーン読み込みと同じように扱えます。

ちなみに呼び出し側はこんな感じ。

GetComponent<ObservableEventTrigger>()
    .OnPointerClickAsObservable()
    .Subscribe(_ => {
        var manager = FindObjectOfType<SceneManagerWithCache>();
        manager.LoadScene(sceneName).Subscribe(s => print("loaded: " + s.sceneName));
    }

とりあえず、この2つのスクリプトでキャッシュ機構は実現できそうです。

作ってみて

どのプロジェクトにも持っていけるので結構便利。 重さに関しては、少なくともPCで動かして処理落ちなどはないです。 ただモバイルは未検証。正直きついのでは・・と思う。

処理がかなりUniRxゴリゴリ書いているんだけど、Do多用して副作用多かったりだいぶ辛い感じなのでアドバイスいただきたいところ・・。

次は utibenkeiさんです。 qiita.com

UnityFukuoka12をやりました、発表しました。

5月くらいに11をやったので半年ぶり。

atnd.org

僕の発表はこちら speakerdeck.com

主に、マルチシーンを使うと便利だぞという話と、Prefabでの管理をSceneにできないかという話。

以下、スライドの補足。

今回なんでマルチシーンをテーマにしたかというとしたかというと実際にやってみたら楽だったから今後はスタンダードになるだろうと思ったのと、それにしてはその辺の話をした記事が調べても出てこなかったから。

このスライドはその辺の未来にもちょっと言及していたので共感できた。

www.slideshare.net

個人的にはPrefabは再利用するための仕組みであって、オブジェクトを保存する仕組みではないと考えている。 なので、複数のシーンにおいたりInstantiateしないのであれば直接シーンファイルの中だけに存在していた方が不要な小さいミスが減らせるんじゃないかなと思ってる。 例えばPrefab化しつつシーンに配置していたとき、以下の問題が起こりうる

  • PrefabとHierarchy上で微妙にパラメータが違ってることに気づかずウォォォ
  • 逆に誰かが知らずにPrefab側の値を変えてScene内で影響出ててウォォォ

小さいエラーで、修正もすぐできるんだけど頻発する可能性はあるし発生しないに越したことはない。 どちらも結局HierarchyとPrefabの二重管理が引き起こす問題だと思う。 特定のシーンでしか使わないならシーンにだけ存在した方が気持ちが楽になりそう。

多分この気持ちはみんな持っていてあまりPrefabを量産しない気概はあると思うんだけど、そうなると1シーンをエンジニア・デザイナが触る状況が発生してしまい、そこでコンフリクトしないように「Prefabにしてシーンに持っていきますね」ということになってるんだと思う。 そこで、じゃあそのPrefabをシーンにしませんか?という話をしたのが今回の発表ですた。

ただ、これを実現しようとしたときやはりシーン間のオブジェクトの依存関係をどうするかという問題が出てくる。 Prefabだとドラッグ&ドロップで参照できたのにどうしてもスクリプトを書かなくてはならなかったり。 ここをいい感じにできる方法がないものか。

ちなみに、リビングデイブでのシーン間のオブジェクトの依存は以下

  • ゾンビがデイブを追いかける(ゾンビはRoom, デイブはHumanControlUI)
  • デイブのライフが0になるとゲームオーバー(デイブはHumanControlUI、ゲームオーバー処理はMainGame)

依存先がどちらもユニークだったので、今回は FindObjectOfType で取ってきている。 これが複数のオブジェクトのうちの一個とかだとFindで決め打ちだのなんだのちょっと面倒な形になりそう。

ただ、個々のオブジェクトでFindだーとか書きまくってると管理に漏れが発生しそうなので一元管理した方がいいのだろうかとか思ったりした。どうやるかは考えてない。

シーン間依存問題とか絶対起きてるだろうからみんなのベストプラクティスをくれ!

ArborでUniRxを使いやすくするArborRxを作った

ということでArborRxというアセットをgithubに公開しました。

github.com

Arborとは

ケットシーウェアさんがアセットストアで販売しているアセットです。

https://www.assetstore.unity3d.com/jp/#!/content/47081

GUIで状態遷移を簡単に実装できるのが特徴です。 基本機能が揃っていているのと、結構ガシガシ自分でコード書けるので自分は愛用しています。

UniRxとは

UnityでReactiveExtensionsを実現するアセットです。

github.com

説明するとものすごく長くなるので割愛します。

ArborRxとは

その名の通り、ArborとUniRxを連携させるアセットです。 具体的に連携とは何かというと、まだ1機能しかないです。

  • GUI上でストリームを購読して遷移させる

リポジトリにも貼ってるやつですがこんな感じです。

ObserveTransitionは、同じState内のスクリプトを全部読んで IObservable<Unit> を返すメソッドを列挙します。 エディタ上で指定したメソッドが返すIObservable<Unit> をObserveTransitionが購読して、発行されたら任意のStateに遷移する仕組みです。 それまでもStateBehaviour内部でRxで書いてはいたけど、遷移条件が増えるほどTransition(state)を書き加えていく必要があったので、外出しできないかな?とやってみたのが今回です。 リフレクションする必要があったので若干面倒かと思ったが意外とさっくりできた。

これによって何が嬉しいかというと、StateBehaviourが遷移条件とか一切気にしなくて済むことです。 遷移はすべてObserveTransitionに任せて本来の処理だけを書いていけるので少し見晴らしがよくなります。 とはいえ、ストリームを定義するのは処理側なので実際のところはまだ気にする必要はあるんですけど。

今後はObserveTransitionにオペレータを付けれるようにしたいところ。 あとはUnit以外の型も許容するとか。個人的には通知してもらえればよいのでUnitでいいと思っており、やるかは微妙。

みなさまのプルリクお待ちしております。