Unity1週間ゲームジャムに参加した
これ
Unity 1週間ゲームジャム | ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
最近小さいやつも作ってなかったのでリハビリにやってみた。 結果、寿司が跳ねる奴が生まれた。
はねすし | ゲーム投稿サイト unityroom - Unityのゲームをアップロードして公開しよう
アイデア出し6日、開発6時間。
スクリプトはUniRx以外、外部アセットは使ってません。
反省点
しばらくやってなかったからか、アイデアが出ない。 どんなのが面白いかなあというところの想像力が欠如し始めたので大変に良くないなと思った。
「油で上がった力士が跳ね回る」という案はある程度掘り下げたあたりで意味がわからなくなったのでお蔵入りになりました。
こういうのは定期的にやらないとダメだなー。
でも跳ねたあたりからは楽しくなってきたので参加してよかったと思う。次もあったらやろう。
RxJavaでPresenterがViewを購読するスタイル
下記を見ながらMVPで書くぞ、という練習をしている。
その中で、ViewとPresenterを書いてるときに、このあたりの関係をストリームでやれたら気持ちいのかなあと思い試してた。
とりあえずは、TwitterをプロバイダにしたFirabeseのユーザ認証。 Presenterはこんな感じ
interface Presenter { fun startSubscribe() }
ビューはこんな感じ、
interface LoginView { fun twitterAuthObservable(): Observable<TwitterSession> fun showLoginSuccess(session : TwitterAuth) fun showLoginFailure(throwable : Throwable) }
showHogeはログイン成功、失敗時の表示をお願いという処理。
twitterAuthObservableはTwitter認証に成功したら発火するストリーム
Presenter実体はこんな感じ。
class LoginPresenter(private val mView: LoginView) : Presenter { override fun startSubscribe() { mView.twitterAuthObservable().subscribe({ t -> FirebaseLoginUsecase(auth).run().subscribe( { t -> TwitterAuthRepositoryImp().saveTwitterAuth(t) mView.showLoginSuccess(t) }, { t -> loginFailure(t) } ) }, { throwable -> loginFailure(throwable) }) } fun loginFailure(throwable : Throwable) { mView.showLoginFailure(throwable) } }
startSubscribeが呼ばれると、ビューのストリームを購読し始める。
Twitter認証が終わったらFirebaseのログインを行うUseCaseに渡して処理する。
成功したら情報をどこかに保存しつつ、ログイン成功画面へ。 失敗だったらログイン失敗画面へ。
ビューの実体はこんな感じ。
class LoginActivity : AppCompatActivity(), LoginView { val mTwitterLoginButton: TwitterLoginButton by bindView(R.id.twitter_login_button) val mPresenter: Presenter = LoginPresenter(this) val mLoginStream: BehaviorSubject<TwitterSession> = BehaviorSubject.create() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) mTwitterLoginButton.callback = object : Callback<TwitterSession>() { override fun success(result: Result<TwitterSession>) = mLoginStream.onNext(result.data) override fun failure(exception: TwitterException) = mLoginStream.onError(exception) } mPresenter.startSubscribe() } override fun twitterAuthObservable(): Observable<TwitterSession> = mLoginStream override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) mTwitterLoginButton.onActivityResult(requestCode, resultCode, data) } override fun showLoginSuccess(session: TwitterAuth) { Toast.makeText(this, "ログインに成功しました", Toast.LENGTH_LONG).show() } override fun showLoginFailure(throwable: Throwable) { Toast.makeText(this, "ログインに失敗しました", Toast.LENGTH_LONG).show() } }
よくあるonCreateでビューにListnerとかcallbackを与えてあげる処理。 コールバック内では、ロジックは書かずに用意したBehaviourSubjectに流してもらう。 全部終わったらPresenterに購読してもらう。
今まではViewが中でPresenterの特定のメソッドを呼ぶという感じだったけど、今回はViewはPresenterのことをほとんど知らなくなった。 クリックしたら何が呼ばれるとか、認証から帰ってきたら何が始まるか、とかは全部Presenter側のコードを読めば済む。
とはいえ、基本ViewとPresenterは1:1の関係で、使いまわしをすることもないと思うのでお互いが依存しあっててもまあいいんじゃないかな・・という気持ちもある。
とりあえずこれでやって辛くなってきたらまたブログにしたためよう
27歳になった
27歳になった。
2016年何してたかは、以前書いてたので割愛
2017年はまだ三ヶ月しか経ってないけど、割と色々あった。
Android開発を始めた
部署移動に伴い、Androidエンジニアに転向した。 今まではRailsやphp、はたまた若干のインフラをやっていたが今回からがっつり変わることになった。 AndroidはUnityでアプリを作ったことはあるが、きちんとJavaを書いたことはないのでこれから勉強。自分は型付言語が好きなのだなあというのを再認識した。
また、移動直前に社内技術イベントで今までやってたことを発表した。 speakerdeck.com
今までの最高はてブ数は2だったのに、300まではてブついたので非常にテンション上がった。
東京に異動が決まった
上記の部署移動に伴い、4月中旬で東京に異動することになった。 福岡と比べて死ぬほど家賃高かったけどめげずにやっていきたい。
東京は勉強会やハッカソンが活発なので、みなさまお世話になります。 しかし、初東京暮らしであり初一人暮らしでもあるので、来年生きているかは怪しい。
家を買った
元々両親と賃貸で三人暮らしだったが、今回の異動に伴い思い切って中古で両親が住む家を買った。
これにより、東京に行ってる間に実家が消滅するという事態は防げたので心の安寧が保たれた。
正直、ここ2ヶ月は家でずっとコード書いてなくて、ひたすら物件選びと不動産屋、リフォーム業者との打ち合わせと書類にサインを繰り返してた。めちゃくちゃ面倒だったのでもうやりたくない。
最後に
ウィッシュリストはこちらになります。ロボットが欲しいです。
https://www.amazon.co.jp/gp/registry/wishlist/1RJ0EM23AIDBT/ref=nav_wishlist_lists_1
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だと Flowable
と Subscriber
がそれにあたる。
基本的には同じだったけど、大きな違いとして消費者側が購読のハンドリングができるかどうかと感じた。 前者だと、subscribeしたときにどのタイミングで止めるかというのは消費者側は操作しにくかった。
Take(n)
などのオペレータで管理はできるけど、それは生産者側の処理なのでちょっと違う。
Disposeを呼び出して止めるはできたけど、それはストリーム外からの操作になるのでちょっと怖いという感じだった。
なので、購読を始めると生産者が流すのを終えるまで購読をやめない!となる。
しかし、ReactiveStreamはSubscriptionインタフェースを提供している。
Subscriptionは生産者と消費者の間に立つインタフェースで、生産者にデータをリクエストする request
と購読を解除する cancel
を持っている。
これらをSubScriberが呼び出すことで、途中でも消費者側の都合で中止できる。これは良いものだと思う。ストリーム内で完結させやすくなりそう。
そういった理由か、2.x以降に実装されたsubscribeはIDisposableを返さなくなってる。 だけど、1.x時代のIDisposableを返すsubScribeは引き続き残っているみたい。
バックプレッシャー
本書で頻繁に出てくる言葉。かなり理解が怪しい。
消費者が通知を受け取れない状況のときにデータをどうするかという設定をバックプレッシャーと呼ぶみたい。 通知ができるまで貯めておくか、通知ができるまで破棄していくかなど。
受け取れない状態というのは例えば非同期で生産者が怒涛の勢いでデータを流してきてさばけないなど。
そういう意味だと非同期の時以外気にしない設定じゃないかなあと思ってる。
この辺、はじめの方からカジュアルに単語が出てきてるけど詳細はChapter3なのでまた後に書くことになりそう。
ちなみにやっとChapter1が終わりました。先は長い。
2016年振り返り
年の瀬なのでやってきたことを振り返る
OSS系
小物でも良いので思いついたら作っとけみたいなノリで、最小限のアセットをいくつか作った。
SimplyPopup
https://github.com/adarapata/SimplyPopup
簡単に使えるポップアップのスクリプト。自分で割と使ってる。
Mikko
https://github.com/adarapata/Mikko
タッチ動作をエディタ、スマホ、スタンドアローンで共通化するやつ。 名前はミッコ。天下のクリスティー式です。
ただ、普通にEventSystems使えばだいたいなんとかなるので不要だなという感じになった。
ArborRx
https://github.com/adarapata/ArborRx
ステートマシーンエディタのArbor2でObservableなスクリプトをGUIで噛ませられる拡張。ちょっとやってみたくて作った。
これ作る際にエディタ拡張でReflection多用するという甘い誘惑に乗ってしまった。
LineTrace
https://github.com/adarapata/LineTrace
だいぶ前に作ったライントレースシステムをゲームから切り離してみたやつ。ペパボアドカレで3Dゲームについて語れる機会があったので公開。
これ結構便利なので使ってみてほしい。
ハッカソンで作った系
リンク切れとか公式のレポートがないのとかあって、データ残ってるの少ない・・
Drive428
GlobalGameJam2016でみんなで作ったやつ。 ハロウィンに悪魔達に支配された渋谷を車で爆走して轢いて回る平和主義ゲーム
この悪魔達、いつもは向かってくるけど車が走ってる時は逃げるという性格をしてます。
地味にRailsでランキングサーバ立ててオンラインでスコア競えるようにしてたけどサーバのお試し期間切れたのでもうできません。
VRHanasaka
Oculus+Myoで花咲か爺さんとなって右手から桜を咲かせる波動を、左手から鬼を滅する波動を繰り出して花を咲かせるゲーム。
人生で初めてVRゲーム作りました。Myoのキャリブレーションがひたすら面倒だった。
お前が!ロボに!なるんだよ!
公式レポートがないんでFBの写真のみになってしまったが、11月のVRゲームジャムで作ったVRゲームです。
人間側とロボット側で協力して敵ロボットを倒す熱いゲームです。ロボット側は視界はOculusで見渡せるものの自分で移動ができないので、見える情報を人間に伝えて操作してもらいます。
また、ロボットはUnlimitedHandを装着しており敵ロボに攻撃されると電流が走ります。割と痛い。
VRゲーム2回目ですが、今回はやりたいことやれて満足です。
ジントリオーク
八耐で作ったやつ。 スプラなんとかが流行ってたので陣取り面白いなと思い作ったやつ。
割とシステムは気に入ってるのでどっかのタイミングで作り直したい。
iOSアプリ
リビングデイブ
会社のお産合宿というイベントで公開したiOSゲーム。仕事を終えて退勤したいデイブが社員の目をかいくぐり退勤するゲームです。
何気に人生初のiOSアプリリリースでした。
ペパラボ
こちらは社内限定で公開したもの。 弊社を退職される先輩にプレゼントとして作ったアプリです。
「ばいきんラボ」というiOSアプリと全く同じシステムを再現してイラストを差し替えました。
クリアするとみんなからの送別メッセージが見れる。 メッセージはゲームからapiサーバを叩いて順次アンロックして見れるような仕組みです。
思い立って二週間くらいで友人と二人で急ピッチで作った記憶ある。
イベント登壇など
UnityFukuoka12
不定期でやってる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シリーズが好きですよね。
僕は大好きです。なのでみなさんも大好きです。
ロックマン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を配置すると楽です。
上記の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を取得する。の繰り返し。
実際にできたアセット
こんなものができました。
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; } } }
キーボードの十字キーで右側、左側にいい感じに動いてくれます。
実装側は DirectionController2d
に向き direction
を与えることと、現在の向きベクトル forward
を取得する以外特に何もしなくていいです。
LineManager
がWayPointからよしなに Line
クラスを生成して、 DirectionController2d
クラスが現在自身が乗っている Line
を逐一チェックして更新してくれます。
ちなみに LineManager
のcycle
にチェックを入れるとWayPointの始点と終点を繋ぐので無限にぐるぐるできます。
DirectionController2d
のautoRotation
チェックを入れると、目的地の方向に自動で回転します。回転速度は 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というアセットを使うことが多いです。
配置がかなりお手軽なのでかなりコスト削減できます。 対象を注視しながら移動ということもできるので横スクロール系だとこれがメインの使い方になりそう。 上記リンクもしくはここからアセットを購入すると僕に報酬が入るシステムなので皆様是非お買い求めください。
ちなみにWayPointシステムも、SimpleWayPointSystem という便利なアセットを使えばサクッと実装できます。こちらはルートをベジエ曲線とかスプライ曲線などで細かく設定できるのがポイントです。
じゃあこの記事はなんだったんでしょうね。
明日は @genkiroid の番です genkiroid.github.io
Unityでシーンをキャッシュする仕組み
2時間遅れの公開になります。遅れてしまった・・。
こちらはUnityアドベントカレンダー、12/5の記事です。
前回は凹さんの「Unity で Windows のデスクトップ画面をテクスチャとして表示するプラグインを作ってみた」でした。
シーンを保持したい問題
Unityでよく悩む問題として、シーン遷移時に前の状態に戻したいということがたびたびあります。
- シーンAからシーンBに遷移
- Bからキャンセル等の動作でAに戻る
- Aのカーソル位置や状態が初期状態に戻る
RPGのフィールド画面 -> 戦闘画面 -> フィールド画面とか パーティゲームのキャラ選択 -> ステージ選択キャンセル -> キャラ選択に戻る
とか。 つまり、前の状態を保持したままシーン遷移を行いたくなることが多いです。 やり方としては、パッと2パターンくらい浮かびました。
- シーン遷移時に必要な情報をシリアライズしてシーンを破棄、再読み込み時に流し込む
- シーンを破棄せずまるごと非アクティブ化。再読み込み時にアクティブにする
前者の方がメモリを食わずに済むので便利そうですが、何をシリアライズするか具体的に決まっていないと作れなさそうな気がしました。 後者は汎用的に使える仕組みではありますが、シーンが残ったままなのでメモリが心配です。特にモバイルだと処理落ちしないかちょっと心配。
今回はこんなやり方どうよというところを紹介したいので、後者を実装します。
シーンのキャッシュ
結論から言うとこんな感じにできました。
シーンのキャッシュ的な機構 pic.twitter.com/mtnC1llFuE
— いも@EFB~相手は死ぬ~ (@adarapata) 2016年12月5日
以下のことができます。
- 任意のシーンにロードして以前のシーンを非アクティブ化。キャッシュがある場合はシーンをアクティブ化。
- 任意の複数のシーンをロードして合成、一つのシーンとしてキャッシュする
追加したクラスは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
LoadScene
は SceneManager.LoadScene
と同じ感覚で、シーン名を指定してロードします。
通常時と違うのは、キャッシュを使うか否かbooleanで渡せるところです。
useCache = trueで渡すと、すでにシーンが非アクティブで存在する場合そちらを有効化します。
見つからなかった場合は通常のシーン読み込みが行われます。
内部的にはロード時は以下のフローです
- キャッシュが無い場合
SceneManager.LoadSceneAsync
でシーンロード - キャッシュがある場合、該当シーンから
SceneProperty
を取得して有効化 - 直前のシーンを無効化
- シーン管理リストに追加
- キャッシュ数上限を超える or キャッシュしないシーンをリストから削除、
SceneManager.UnLoadAsync
でヒエラルキーから削除
呼び出し元には IObservable<SceneProperty>
を返して、ロード完了後にイベントを発行します。
完了後のなにかしらはシーン側にお任せします。
キャッシュ数上限は sceneCacheCapacity
が保持しており、インスペクタ上から設定できます。
LoadMergedScene
は、シーン名を指定してロードするのは同じですが、こちらは複数ロードすることができます。
ベースとなるシーンAと、可変長引数のシーン名配列を渡すことで、該当のシーンをすべてロードした上でシーンAに統合してくれます。
これは、1つのシーンを機能ごとに分割して管理をしている場合に有効です。 例えばデザイナと分担するためにシステムとUIをシーン分けて作業しているけど、実際に使うときは完全に統合したいんだよみたいなときとか。 どういうシチュエーションだろうか、というところは前回のエントリなどを見ていただけるとイメージがつかめるかもしれません。
こちらも最終的な返り値は 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