【Unity】簡単明快なdrawcall削減の例


お疲れ様です。
Unity書いてたら、たまたまいい感じのdrawcall削減例が出来たので、
投稿したいと思います。

どんなUnityプロジェクトなの?

  1. テキストファイルを読み込む
  2. パースし、マップデータに変換
  3. 変換したマップデータからマップを生成する

という簡単なモノです。
8bit RPGのようなマップチップを配置するものではなく、
昨今のソーシャルゲームにあるようなマスを進むタイプのマップになります。

こんな感じ。

現在、draw call : 12となっています。

コードはこんな感じ

LoadText.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class LoadText {
    // Resources/text/から指定のテキストファイルを改行区切りで読む
    public List<string> loadTextFile(string textFileName){
        TextAsset textAsset = Resources.Load<TextAsset>("text/" + textFileName);
        string[] strs = textAsset.text.Split ("\n" [0]);
        foreach(string str in strs){
            txtList.Add(str);
        }
        return txtList;
    }
}
MapCreate.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class MapCreate : MonoBehaviour {
    // Inspectorでprefabを紐付け
    public GameObject mapParent;
    public GameObject map1;
    public GameObject map2;

    // Use this for initialization
    void Start () {
        // テキストファイルを読み出す
        LoadText loadText = new LoadText ();
        List<string> loadMapTextList = loadText.loadTextFile ("map_1");

        // 文字列からMapDataを生成
        List<MapData> mapDataList = new List<MapData> ();
        foreach (string str in loadMapTextList) {
            MapData map = new MapData();
            map.parse(str);
            mapDataList.Add(map);
        }

        createMap (mapDataList);
    }

    // MapDataからマスをポチポチ置いていく 
    private void createMap(List<MapData> mapList){
        GameObject obj = null;
        foreach (MapData map in mapList) {
            switch(map.ImgId){
            case 1:
                obj = Instantiate(map1) as GameObject;
                break;
            case 2:
                obj = Instantiate(map2) as GameObject;
                break;
            }
            if(obj != null){
                obj.transform.position = new Vector3(map.X , map.Y , 0);
                obj.transform.parent = mapParent.transform;
                obj = null;
            }
        }
    }
}

map_data.txt
1,1.0,-2.0
2,1.4,-1.6
2,1.8,-1.2
1,2.3,-0.8
2,1.5,-0.6
2,1.0,-0.5
〜

LoadTextでテキストファイルを外部から読み込み、
MapCreateでパースしてMapDataを作成して、
作成したMapDataからポチポチとマスを置いていく
という処理になります。(MapData.csは省略)

なお、マップに表示する2つの画像データはprefab化しています。
1が大マス、2が小マスになります。

prefab化していても何も考えずに作成するとdrawcallはこのぐらいになります。

draw call削減!

削減したらこうなりました。

なんと、draw call : 3!!
嬉しい限りです。

で、何したの?

種明かしすると描画する順番を画像別にソートしただけです。

map_data.txt
1,1.0,-2.0
1,2.3,-0.8
2,1.4,-1.6
2,1.8,-1.2
2,1.5,-0.6
2,1.0,-0.5
〜

この例ではtxt弄りましたが、
コード上でソートしてからInstantiateしてもOKです。

なんでdrawcall削減出来るの?

UnityはBatchingという機能を持っていて、
同じマテリアルを使用しているオブジェクトを一度に描画してくれます。

--以下、公式より流用--
Unityはランタイムで複数のオブジェクトを合成し、一回のドローコールで同時に描画します。この処理は「バッチング(batching)」と呼ばれます。Unityがより多くのオブジェクトを一緒にバッチングできれば、レンダリングのパフォーマンスはより良くなるということになります。
--ここまで--

あれ、なんか変だぞ?

ソートする前でも、オブジェクト自体は同じマテリアル使用しています。
にも関わらず、バッチングの対象にはならないようです。

--以下、公式より流用--
ダイナミック(動的)バッチング
同じマテリアルを共有していて他の条件を満たせば、Unityは自動的に動いているオブジェクトをバッチングします。ダイナミックバッチングは自動的に処理されるので、何かの手間が必要というわけではありません。
--ここまで--

推測になってしまうのですが、
別のオブジェクトが間に挟まると、同じオブジェクトだとしてもそこで
動的バッチングの対象から外れるようなアルゴリズムになっているようです。
また、Zorderが変更される場合も対象から外れるようです。

以下Batchingイメージ

以上

このようにちょっとした気遣いでdrawcallはガクっと削減させることが出来ます。
今回はUnityのお話でしたが、cocos2d-x v3.x以降も同様のAuto Batchingを持っているようですね。
機会があれば試してみたいな。