ただの適当な開発記

ゲームアプリを作るエンジニアの雑記

スクロールビューの負荷を軽減する方法 #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

ではでは