ただの適当な開発記

会社勤めしつつUnityでアプリ作ってる人の雑記

最適化されたスクロールビューをEnhancedScrollerで作る!

この記事は「Unity アセット真夏のアドベントカレンダー 2017」の29日目の記事です!
(アドベントカレンダーは初めて参加します\(^o^)/

今日はEnhanced Scrollerというアセットについて紹介しようと思います!

先日更新した記事でスクロールビューを最適化するにはデータ駆動の実装にするのが良いと書きました

cocokyoro.hateblo.jp

データ駆動のスクロールビューを簡単に実装できるのがEnhancedScrollerです。

こんなことができます

ちなみにEnhancedScrollerはデータ駆動である点以外にも様々な機能があり、書き出すとざっとこんな感じです

1.無限スクロール


2. スナッピング
中途半端なところに止まらないで一番近いセル*1にピタッと合わせるやつ

(なぜか公式のデモはスロットです笑

3. ジャンプ
指定したインデックスのセルに飛ぶやつ

4. 1つのスクロールビュー内に複数の大きさのセルを表示する

5. 下に引っ張って更新するやつ
TwitterとかFacebookとか最近のネイティブには大抵あるやつですね

6. セルの非同期読み込み


とまあこんな感じで最近のアプリ開発で使いそうな機能が簡単に実装できるようになっております。

EnhancedScrollerは個人開発でも会社のプロジェクトでもどちらでも導入しているのですが、特に会社では導入したお陰でパフォーマンスに苦労していた箇所が一気に解決できたので大助かりでした。
そのときはLINEのようなチャット画面を実装しようとしていたんですが、
・履歴の数だけセルを表示したい
・複数の種類のセル(自分の発言・相手の発言・日付の境界)を表示したい
・下に引っ張ったら更新したい
・画像を動的に読み込みたい
・軽くしたい
といったニーズがあったためまさにEnhancedScrollerがドンピシャだったわけです。

データ駆動のスクロール

さて、そもそもデータ駆動のスクロールビューを使うと、こんな感じになります。

このDemoでは1000件のセルを表示しています。
ですが実際にオブジェクトとして描画しているのはこの画像の赤枠の部分だけで、青枠の部分はオブジェクトとして生成していません。
f:id:cocokyoro:20170827221206p:plain

ヒエラルキー上の挙動を見てみるともっと分かりやすいです。


右側のヒエラルキーの部分を見ると、表示に併せてConainerという名前の子階層にあるCell~~というオブジェクトが切り替わっています。
(ちなみにこれは表示非表示の度にInstantiate・Destroyしているわけではなく、描画範囲内に必要な要素を事前に生成して描画時にGameObjectの名前も一緒に変えています)

この手法をであれば、1000個のセルだろうが全く問題なく表示ができてしまうわけです。
実際に1000個をそのまま描画するとFPSが大変なことになってしまいます。。
(そのあたりは前回の記事参照)

MVCライクな作り

EnhancedScrollerのいいところは、MVCを考慮した作りになっているため導入しやすい点です。

Modelとなるデータクラスはプロジェクト独自に定義

Viewとなるクラスは
EnhancedScrollerCellViewを継承

Controllerとなるクラスは
IEnhancedScrollerDelegateのインターフェースを実装

というように役割ごとに分離された実装になっているので、その通りに作れば綺麗な構成になります。

詳しい実装については公式のチュートリアルを見てもらうとわかりやすいです。

この作りはアセットを作る上でも大変参考になり、自分が作成したMultiButtonScrollerもEnhancedScrollerっぽい実装ができるようになっています。

まとめ

・デフォルトのScrollRectが使いにくい!
・たくさんデータを表示させたい!
スマホに最適化されたスクロールビューを使いたい!

という人は是非一度EnhancedScrollerを試してもらえるといいと思います!


そんなわけで私からの更新は以上になります。

明日8/30の担当者は今のところ未定ですが...!
次回8/31はLimes@XRDeveloperさんによる「Shader Forgeのこと」です!

(2017-08-29 23:11追記)
8/30の担当者はjojomonさんです!お楽しみに!

*1:EnhancedScrollerではスクロールビューの一つ一つの要素をセルと表現しています

ボーイスカウト・ルールの重要性

ボーイスカウト・ルール

エンジニアの世界には「ボーイスカウト・ルール」なるものが存在するらしいです。

実際のボーイスカウトの規則には「自分のいた場所は、そこを出て行く時、来た時よりも綺麗にしなければならない」というものがあるらしく、それをエンジニアのコーディングに置き換えて表現したものがここでいうボーイスカウト・ルールですね。
※「ボーイスカウトの規則」と表現することもあるみたいです

myenigma.hatenablog.com
qiita.com

口癖のように使ってる人もいる

前のプロジェクトで一緒に仕事をしていたエンジニアが口癖のように言っていました。
「自分が書いたわけじゃないからって良くないコードを放置しちゃだめだよ。ボーイスカウトルールだよ」的な感じで。

例えば自分が新しい機能を担当したときに既存のクラスを一部使って実装するとする。元の処理を見たら結構汚いコードが書かれている。
そうなったときに、それを見て見ぬふりをするのではなく元の状態より綺麗にして作業に着手しましょう、ということですね。
前述したエンジニアの方が口を酸っぱくして言っていたので、自分は割とその精神が定着していました。

反発も勿論ある

「ただ、人のコードを勝手にいじる」ということに対して反発する人は少なからずいると思います。
(自分も勝手に人にリファクタリングされて「やばかったから直しといたよ」とか言われたら少しイラッとするかもしれません笑)
ただそこはプルリクなんかでお互いが議論して納得する形でよりよいコードに変えていけばいいのかなと思ってます。
実際に前の現場ではコードレビューやプルリクを導入していたので、人のコードに変更を入れた場合はgithubのConversation上で元の実装者とやりとりして納得した上でマージするようにしていました。(元の実装者が居ないパターンももちろん多くありましたが..)
逆にそういった文化がない現場だと、好き放題に人のをいじれるというのは無法地帯になってしまうかもしれません。

明確なアンチパターン

そんな状態で2ヶ月前から参画した直近の現場で驚いたことがありました。
コードにこんなコメントが書かれているんです

//この処理書いた人は割と頭悪い
//なぁにこれwwww

みたいなコメントがあるんです。
そしてこのコミットをした人は本当にただ感想を書いただけでそこを改善する処理は全く書いていない。

既存のコードに対してこういった感想を書くだけ書いて何も手を付けずに立ち去るというのは、結局「この処理は複雑で読みにくいと思うけど僕は何もできませんでした」って宣言しているようなものだと思うんです。

ただその人にも悪気があったわけではなく、プロジェクトの状況が燃えてて疲弊している中でつい見かけた良くない処理に対してキツめのコメントしてしまった、とかもあると思います。
しかし、こういったコメントを見つけてしまうと「ああ、このプロジェクトにはボーイスカウトルールとは真逆をいってるな」と思ってしまうわけです。

メリットを共有することが大事

そもそも「人のコードに手を入れて改善していく」という発想がない可能性があります。
大事なのは技術的負債を皆で返済していくようになると、自分がどれだけ楽になるかというのを共通認識として持つことなのかなと思っています。
今の現場ではプルリクに対するレビューを担当しているので、そういった姿勢をチーム内で共有出来るようになればいいなと思っています。
もしこれを読んでいる方で「こうしたら上手くいったよ」という例があればご教示いただきたいですm(__)m



というわけで珍しく会社で感じたことを書いてみました。明日もがんばるぞい

スクロールビューの負荷を軽減する方法 #Unity


最近はリリース直前のゲームプロジェクトでUnity(80%)やらphp(10%)やらバッチ(5%)やらエクセルマクロ(5%)やらを触っています。
リリース直前ということで申請までになんとかしなきゃいけないタスク一覧みたいなのがあるんですが、その中に
スクロールビューを軽くする
というタスクがありました。

まあ最近のスマホゲームなんかだと友達一覧やらプレゼント一覧やらクエスト一覧やらとにかく縦横にずら~っと表示する情報が多いわけです。
そこがとにかく重いと。

対応方法はいくつかあるんですが、今日はそのあたりについてまとめて書いてみようと思います。
(「いやいや違うよ」とか「こっちの方がオススメだよ」といった方法があればコメントお待ちしてます!

重い原因

まずスクロールビューを重くしている原因を整理してみます。

(a)スクロールしてる要素一つ一つの負荷が重い

f:id:cocokyoro:20170816000021p:plain
こんな感じでいっぱい処理がならんでいてそれぞれに重い処理がくっついてるとトータルの負荷は結構なものになってしまう

(b)表示したい要素を全て描画してしまっている

f:id:cocokyoro:20170816000334p:plain
こんな感じのビューがあるとして
(画像はフリーコンテンツの星宝転生ジュエルセイバー のを使わせてもらってます)
f:id:cocokyoro:20170816000256p:plain
Unityのスクロールビューはデフォルトでマスクのコンポーネントがついているのでこういう見た目ですが、
マスクをオフにすると
f:id:cocokyoro:20170816000617p:plain
こんな見た目になります。
マスクで見た目上は画像が消えていても、裏で描画の処理は走っちゃってます。
(ちなみに表示の負荷だけでいうとマスクをしているときの方がかかります)

と重くなってしまう原因の代表的なものとしてはこの2つが考えられます。

対策

(a-1)スクロールしてるオブジェクトにくっついてるスクリプトの処理を軽減する

- 使わないUpdateとかは消しておく
- Updateの中でFindやGetComponentを呼んでる箇所があったら無くす

(b-1)データ駆動のスクロールに切り替える

- EnhancedScrollerを使う
- 無限スクロールやデータ駆動のスクロールを提供するアセットです。
- 詳しくは次回のアドベントカレンダーのときに更新します

(b-2)描画範囲外の要素を非アクティブにする

- この記事ではこの対応について詳しく説明しようと思います

描画範囲外の要素を非アクティブにする

- ここの部分を描画しているとしたら
f:id:cocokyoro:20170820235821p:plain
- ここの部分のオブジェクトの処理は切ってあげた方がいいわけです
f:id:cocokyoro:20170820235946p:plain
- というわけで

  • 特定のRectに入っているかどうかを確認するscript

  • 指定した範囲内に入ったタイミングと出たタイミングで処理を走るようなComponent


を作りました。
前者についてはUnityのコミュニティに上がっていたやつを参考にしました
(参考 https://forum.unity3d.com/threads/test-if-ui-element-is-visible-on-screen.276549/#post-2978773)

使う側の例としては下記のような感じです。

f:id:cocokyoro:20170821003456g:plain

falseだったら(指定した範囲外に描画しようとしていたら)描画をオフにしたり処理が重いスクリプトをオフってあげればいいと思います。ここでは試しに色を変えています。

実用

public class SampleHeavyComponent : MonoBehaviour {

	// Update is called once per frame
	void Update () {
		for(int i = 0;i < 10; i++){
			Debug.Log("heavy log");
		}
	}
}

たとえばこんなコンポーネントが100個着いていたとして(わかりやすくするためにこんな処理にしてます)、描画範囲外にでたらこのscriptをオフにしてあげるようにします。

さっきの初期化の部分をこんな感じに変えます

private void SpawnScrollObject(){
		var scrollElement = Instantiate(_scrollElementOrigin);
		scrollElement.transform.SetParent(_scrollContentParent);
		var observer = scrollElement.GetComponent<ViewRectObserveComponent>();
		var heavyComponent = scrollElement.GetComponent<SampleHeavyComponent>();
		observer.Initialize(_targetViewRect, (entered) => heavyComponent.enabled = entered);
}


これでProfilerの処理が

  • Before

f:id:cocokyoro:20170822031411p:plainf:id:cocokyoro:20170822031415p:plain

  • After

f:id:cocokyoro:20170822031422p:plainf:id:cocokyoro:20170822031419p:plain
こんな感じになります。fpsが改善されてますね。

描画している部分以外は非アクティブにしているので、当然軽くなります。
「とにかく急遽スクロールを軽くしてくれ」と言われたらこの方法がおすすめです。

注意点

この実装方法はいくつか注意点があります

  • スクロールさせる要素が多すぎると重い

スクロールする項目があまりにも多いと、ViewRectObserveComponentの処理で結局重くなってしまいます。
ViewRectObserveComponentはUpdateで常に監視しているので、スクロールさせる要素が多いと結局重くなってしまいます

  • Hierarchy上の階層が深いと重い

rectTransform.GetWorldCorners
って呼んで字のごとくWorld座標を取得しているんですね。
なので階層が深ければ深いほどWorld座標を計算する負荷が大きくなります。
できれば指定した階層から相対的に位置をチェックするとかができればいいんですが、そこは今のところ未対応です



結局スクロールはデータ駆動が一番

さんざん色々言っといてあれですが、スクロールビューのパフォーマンスを最適化するのに一番良い方法はデータ駆動のスクロールビューに切り替えることです。
おすすめはこちらのEnhancedScrollerです。

こちらについては後日アセットアドベントカレンダーの更新時に詳しくかければと思っています!
atnd.org

ではでは

2017年5月~2017年7月のアセット売上

8月になりました。あつ〜

ということで過去3ヶ月のアセットストアの売上です。

f:id:cocokyoro:20170801010742p:plain

ご購入いただいた皆様ありがとうございますm(__)m
ReferenceFinderはコンスタントに売れているんですが、今思うともうちょっとわかり易い名前にした方がよかったのかな〜と思っています。
というのも同じような名前のアセットが多すぎまして..orz
ReferenceFinderがやってることはInspector上のSerializeFieldで参照してるオブジェクトの参照の一覧を表示しているわけで、InspectorReferenceViewerだとかSerializedComponentFinderとかあったんじゃないかなと。
まあでも今書いてて思いましたけどあまり長ったらしいアセット名だとそれはそれで敬遠されてしまいそうですね。

↑ありがたいことに☆5Ratingが表示されるようになりました!

この3ヶ月で思い出に残ってるのはMultiButtonScrollerに関する質問のメールをいただいたことです。
ちょっとチュートリアルの作りが簡素すぎたせいでご迷惑をおかけしてしまったので日本語のドキュメントをもっと充実させようと思いました。
やっぱ自分で使ってみないことには改善点も見えてこないので一度作ったアセットはどんどん使っていこうと思います(`・ω・´)

残る一つのPuzzleTemplateについても最近メールが来まして、どうやら特定環境で起動時にクラッシュする現象が起きているようです。。
こちらは現在原因を調査中ですのでもう少々お待ち下さい。

MacOS SierraにあげたらKarabinerが効かなくなったやつ

3年ぶりにMacのOSアップデートをしました。
普段開発で使ってると極力安定して動く環境から変えたくないのでずーっと10.10 Yosemiteを使ってました。

pc-karuma.net

2014年の10月からずっと放置してたっぽい..

で今回会社のシステムの都合でアップデートした方がいいかも...ということになりアップデート(結果アップデートしなくてもよかったんだけども)

アップデートしたところ愛用していたKarabinerというキーマップ変更のアプリケーションが機能しなくなってしまった..

調べてみると解決策いっぱいでてきた

qiita.com

使ってる置き換えは右のCommandボタンをescapeに変更するというvim用のやつだけなのでKarabiner-Elementsを使って対応
元々入ってるKarabinerを消さないとKarabiner-Elementsの変更が反映されないかも

github.com

置き換えの処理の部分は中身はc++シェルスクリプトで書かれてるみたい。viewの部分はobjective-cで書かれてる。

c++objective-cの部分の連携ってどうやるんだろうって思ったら普通にできるらしい

ja.stackoverflow.com

Objecive-C++という見慣れないやつが。。

hajimehoshi.hatenablog.com

Objective-C++ は、プログラミング言語 Objective-CC++ とを多重継承したような言語です。
お互いの文法がかち合わないので、混在することができます。

はえー知らなんだ


というわけで話がそれてきたので終了

真偽値で2つのメソッドを呼び分ける

少しでもコードを短く書きたい人は下のようなコードを見た時どう思うのだろうか。

    public void PlayNext()
    {
        if (_isEndless)
        {
            PlayNextEndless();
        }
        else
        {
            PlayNextNormal();
        }
    }

どこかのstaticなクラスに以下のようなメソッドを書くと

 public static void CallByCondition(this bool condition, Action onTrue, Action onFalse)
    {
        if (condition)
        {
            if (onTrue != null) onTrue();
        }
        else
        {
            if(onFalse != null) onFalse();
        }
    }

もとのやつは以下の感じで表現できる

    public void PlayNext()
    {
        _isEndless.CallByCondition(PlayNextEndless,PlayNextNormal);
    }

こういう拡張に慣れると他のプロジェクトにジョインしたとき非常に困る。
かといって導入しようとすると多分嫌がられるという笑

DoTweenの拡張メソッド

今の座標を基準に指定した値を追加で動かす、というありそうでないメソッドを拡張で追加

DoTweenの拡張クラス

今の座標から右に100f動かすのを繰り返したい!とかはこのメソッド使うとシンプルにできますね

DoTweenは無料ですよん ;)