【Unity】お手軽?なオブジェクトプール 【デザインパターン】

f:id:seiroise:20160606030443j:plain

備忘録第二弾ということで「オブジェクトプール」についてまとめておきます。

"お手軽?"とかは、ただの主観なのでもっと使いやすいオブジェクトプールがあったら教えてください。

オブジェクトプール?

まず「オブジェクトプールって何?」ってやつです。
オブジェクトプールは簡単に言えば「プログラムの実行中に作ると重たいから先にいっぱい作っておこう!」っていう仕組みです。

ゲーム以外のプログラムでも使われているものなので割といろんな人になじみ深いものなのかもしれないです。
聞いたところによると有名なHTTPサーバである「Apache」の内部でも使われているみたいです。

オブジェクトプールの効能

気になるオブジェクトプールの効能ですが、とりあえず下の図を見てください。

f:id:seiroise:20160606032602j:plain:w500

それぞれ1000個のGameObjectを生成した結果をProfilerで確認したものです。
1番上がおなじみのInstantiateで普通に生成した場合
2番目がオブジェクトプールで擬似的に生成した場合
3番目はおまけです

注目してほしいのは「Time ms」の項目で
普通にInstantiateした場合はInstantiateとActivate合わせて大体61.21ms
オブジェクトプールの方がActivateのみなので大体8.75ms
なんとオブジェクトプールを使うと速度が約7倍早くなってます!
驚異的ですねぇ~

しかしオブジェクトプールは速度と引き換えにバグを抱えやすくなる要因でもあるので、
きちんと管理していかなければなりません。

デザインパターンなの?

サブタイトルにデザインパターンとか書いてあるのは下の書籍でパターンとして紹介されていたからです。

この本はかなりおすすめです。一押しです。
多摩センターの丸善に一冊しか置かれてないのが不思議なぐらいです。

デザインパターンについて多少知識があった方がいいと思いますが、
作者の語り口やユーモアがいたるところに散りばめてあるので読むだけでもかなり面白いです。
あと裏表紙の著者とわんこが面白いです。

もう手遅れだけど、3年ぐらい前のプログラミング覚えたての時に欲しかったです。(いやほんとに…

前書きが長くなりましたが、次からオブジェクトプールを作っていきます。

作る前に仕様の確認

作ると言っていたのは嘘だったようです。
まずは仕様を確認しましょう。

  1. 管理はGameObjectのActiveのオンオフで
  2. 使わなくなったときはActiveをオフにするだけ
  3. 生成したオブジェクトは子にしてActiveがオフの状態にしてお
  4. 足りなくなったときは追加できるようにする
  5. ジェネリックで作っておいて用途に応じて継承する

とりあえずこんな感じです。
1と2はUnityで既にGameObjectという仕組みができてるのでそれをそのまんま使います。
とくに2は重要で元々の機能を使うことでオブジェクトプール用の基底クラスを作ったりしなくてよくなるのでお手軽さを上げてます。

ではではほんとに作っていきます。

ほんとに作る

まずはクラスの定義から

public class GenericObjectPool<T> : MonoBehaviour where T : Component {
  //クラスの中身
}

仕様の5番で基底はジェネリックにしとくようにしたのでこんな感じ。
一応whereで制限を掛けときます。

次にメンバたち

[Header("Option")]
[SerializeField]
private T obj;             //プールするオブジェクト
[SerializeField, Range(0, 1024)]
private int objNum = 128;    //プールするオブジェクトの数
[Header("Add Option")]
[SerializeField, Range(0, 1024)]
private int addObjNum = 128; //追加するオブジェクトの数

//内部パラメータ
private T[] pool;              //実際のプール
private bool awaked = false; //初期化フラグ

それぞれコメントに書いてある通りですね。

お次は生成(追加)部分

/// <summary>
/// プールにオブジェクトを追加
/// </summary>
private int AddPoolObj(T poolObj, int poolObjNum) {
    //新しいプールを作成
    T[] newPool;
    int count = 0;
    if(pool == null) {
        newPool = new T[poolObjNum];
    } else {
        newPool = new T[pool.Length + poolObjNum];
        //移し替え
        foreach (T elem in pool) {
            newPool[count] = elem;
            count++;
        }
    }
    //新しいのを追加
    int temp = count;  //countの値を一旦保持
    T obj;
    for (int i = 0; i < poolObjNum; i++, count++) {
        obj = Instantiate<T>(poolObj);
        obj.gameObject.SetActive(false);
        obj.name = poolObj.name;
        obj.transform.SetParent(transform);
        newPool[count] = obj;
    }
    //プール入れ替え
    pool = newPool;
    return temp;
}

一応追加と初期の生成も兼ねてるので、
プールがなかったら作って、プールがある場合は移し替えてます。

とあるところで配列の探索はforeachの方が高速やで!という記事(C#におけるforeach文とfor文による添え字アクセスのパフォーマンス検証 | ftvlog)を見かけたので、配列はなるべくforeachを使うようにしてます。

肝心の取り出し部分
単品

/// <summary>
/// オブジェクトの取り出し
/// </summary>
public T Pop() {
    //初期化確認
    if(!awaked) Awake();
    //使用可能オブジェクトの検索
    foreach(T elem in pool) {
        if(!elem.gameObject.activeInHierarchy) {
            elem.gameObject.SetActive(true);
            return elem;
        }
    }
    //足りない場合
    if(addObjNum > 0) {
        //追加して最初の要素を返す
        return pool[AddPoolObj(obj, addObjNum)];
    }
    return null;
}

複数

/// <summary>
/// 複数のオブジェクトの取り出し
/// </summary>
public T[] Pop(int popNum) {
    //初期化確認
    if (!awaked) Awake();
    //使用可能オブジェクトの検索
    T[] objs = new T[popNum];
    int popCount = 0;
    for(int i = 0; i < pool.Length; i++) {
        if(!pool[i].gameObject.activeInHierarchy) {
            pool[i].gameObject.SetActive(true);
            objs[popCount] = pool[i];
            popCount++;
            if(popCount >= popNum) break;
        }
        //足りない場合
        if((addObjNum > 0) && (i == pool.Length - 1)) {
            if(popCount < popNum) {
                //追加する
                AddPoolObj(obj, addObjNum);
            }
        }
    }
    return objs;
}

それぞれ毎回取得O(n)の計算量が掛かってしまっているのが若干痛いところですね。
O(1)にするためには「取り出し」と「しまい」があれば大丈夫なのですが今回オブジェクトプールが管理するのは「取り出し」だけなのでこんな感じになってます。
どちらとも管理しなくてもO(1)にする方法があったような気がするんだけど、忘れてしまったのでそれはまた思い出してからで。

とりあえず実装はこんな感じです。

次は継承先を作ります。恐らく一番使われるであろうTransform用のオブジェクトプールです。

/// <summary>
/// Transform用のオブジェクトプール
/// </summary>
public class TransformObjectPool : GenericObjectPool<Transform>{

    /// <summary>
    /// オブジェクトの取り出し
    /// </summary>
    public Transform Pop(Vector3 position) {
        Transform obj = Pop();
        if(obj == null) return null;
        obj.transform.position = position;
        return obj;
    }
    /// <summary>
    /// オブジェクトの取り出し
    /// </summary>
    public Transform Pop(Vector3 position, Quaternion rotation) {
        Transform obj = Pop();
        if (obj == null) return null;
        obj.transform.position = position;
        obj.transform.rotation = rotation;
        return obj;
    }
    /// <summary>
    /// 複数のオブジェクトの取り出し
    /// </summary>
    public Transform[] Pop(int popNum, Vector3 position) {
        Transform[] objs = Pop(popNum);
        foreach(Transform elem in objs) {
            if(elem == null) break;
            elem.position = position;
        }
        return objs;
    }
}

一旦継承させておくことでそのクラスにあった取り出したときの初期化が出来るようになります。

使うときはこんな感じで

//プールを取ってくる
TransformObjectPool pool = FindObjectOfType<TransformObjectPool>();
//(0, 0, 0)に生成
pool.Pop(Vector3.zero);

というわけでオブジェクトプールについての備忘録でしたー

【Unity】LineRendererの拡張 その2【メッシュ】【自動生成】

※その1の続きです。

seiroise.hatenablog.com

その1は実装部分についての備忘録だったので、
その2ではそれ以外のシェーダとかエディタ拡張とかの備忘録になると思います。
そして最後にプロジェクトファイルを上げておきます。

エディタ拡張

その1で要件の一つに「エディター上で確認できる」という項目を挙げたので、
エディタを拡張してなんとか要件を満たします。
エディタを拡張するといっても何のことはなく普段のインスペクターの描画に更新ボタンを追加するだけです。

シェーダ

Unity標準の頂点カラーシェーダだと透過処理を行ってくれなかったので
頂点カラーで透過処理を行うシェーダを書いていきます。
(正直ここが一番時間かかった!

やってることは頂点シェーダ、フラグメントシェーダを使って頂点カラーをそのまま描画してるだけです。
重要なのは最初のTagsとBlendなのですが、シェーダとかほとんど書いたことなかったので結構苦労しました。

一応サーフェイスシェーダ版も書いたので一応載せときます。

f:id:seiroise:20160530001446g:plain:w300

これはこれでありだけど陰影がついてしまうので、ちょっと変。
(あと描画順がおかしい?

使い方

使い方は割とLineRendererに近い感じです。

f:id:seiroise:20160530002621p:plain:w400

上から

  1. 「Edge Parameter」
    • エッジの大きさと何角形にするかが選べます
  2. 「Node Parameter」
    • ノードに使うメッシュとノードのデフォルトの大きさ、色が選択できます(スクリプトからいじる時用)。
  3. 「Nodes」
    • 表示するノードのリストです。
  4. 「Apply」ボタン
    • 更新用のボタンです。Nodesやパラメータの内容が反映されます。

プロジェクトファイル

最後にプロジェクトファイル
onedrive.live.com

【Unity】LineRendererの拡張 その1【メッシュ】【自動生成】

おっすお疲れ様です!(謎テンション

・・・

・・・・・・

いや、お久しぶりです。

先日とある理由でUnityで線を描画する必要があり
いろいろしてたので、備忘録がてら記録しておきます。

なぜLineRendererだとだめなのか?

そもそもUnityで線を描画させるならLineRendererという機能が存在するのに
なぜ、使わなかったのかというと、いろいろあるんですがとりあえずこれを見てほしい。

f:id:seiroise:20160529220438g:plain:w300

こいつを見てどう思うだろうか?

標準のLineRendererでは頂点の位置によってメッシュが捻られてしまいます。
そのせいで、線の太さが均一ではなくなってしまいます。これは致命的です。

今回はこれが気に入らなかったのでLineRendererExなるものを自分で作ることにしました。
最終的なコードは一番下にあるのでソースが気になる人はスクロールバーを全力で動かしてください。

要件

線を作る前に自分なりの要件を挙げておきます。

  1. 2D/3Dともになるべく線の太さが一定に見える(重要)
  2. 頂点ごとに色が変えられる
  3. あと基本的な機能(頂点の追加、挿入、削除)
  4. エディター上で確認できる
  5. 変更はリアルタイムに適応されなくてもいい

今回はこれらが満たされるようなものを作っていきます。

Unityで線を表現する方法はいくつかあるのですが、要件1を満たすために
メッシュを自動生成する方向で行こうと思います。
要件2は頂点カラーを使っていきます。
要件3は実装で,要件4はExecutableInEditMode属性つけてエディタを少し拡張してあげればおk。
要件5は・・・とくに気にしなくていいよね。

完成物

作る前に完成物をチラ見せ
f:id:seiroise:20160529224343g:plain:w300
こんな感じ
エッジ(辺)は必要に応じて粗さ(何角形か)をいじれるので、
「こんなに細かくなくていいな!」ってきは調節できます。

f:id:seiroise:20160529224515g:plain:w300
頂点(ノード)に表示するメッシュは選択できるのでキューブにするとこんな感じ

作る

それでは実際に作っていきます。 ※あくまで備忘録なので説明は最小限です。気になる人はコメおなしゃす。

必要なデータ構造として頂点(ノード)の色とかを記憶しておく必要があるのでそれらを定義します。

/// <summary>
/// ノード記録用クラス
/// </summary>
[Serializable]
public class Node {
    public Vector3 pos;     //座標
    public Vector3 scale;   //大きさ
    public Color color;     //色
    //public Mesh mesh;     //メッシュ

    //Constructor
    public Node() {
        this.pos = Vector3.zero;
        scale = Vector3.one;
        color = new Color(1f, 0.5f, 0.2f, 0.5f);
    }
    public Node(Vector3 pos, Vector3 scale, Color color) {
        this.pos = pos;
        this.scale = scale;
        this.color = color;
    }
}

このクラスをリストの形で持たせておいて更新関数が走ったら再描画するという感じ。

/// <summary>
/// 更新
/// </summary>
public void Apply() {
    if(nodes.Count <= 0) return;
    //線状のメッシュを作成
    Mesh mesh = CreateLineMesh(nodes, edges);
    mesh.RecalculateNormals();
    mf.mesh = mesh;
}

更新関数の肝になってるCreateLineMeshの流れは

  1. 必要なエッジに相当するメッシュを作成(重要)
  2. ノードとエッジのメッシュをリストに格納
  3. メッシュを結合
  4. 結合したメッシュを返す

という感じになってます。

一番重要なエッジメッシュの作成はこんな感じ

/// <summary>
/// エッジメッシュの作成
/// </summary>
private Mesh CreateEdgeMesh(Vector3 from, Color fromColor, Vector3 to, Color toColor, float size, int square = 4) {
    Vector3 dir = (to - from).normalized;   //方向ベクトル
    //Quaternion.LookRotation(dir)がみそ
    Vector3 dirVertical = Quaternion.AngleAxis(90, dir) * (Quaternion.LookRotation(dir) * Vector3.right) * size;    //方向ベクトルに垂直なベクトル

    Vector3[] vertices = new Vector3[square * 4];
    Vector2[] uvs = new Vector2[square * 4];
    Color[] colors = new Color[square * 4];
    int[] triangles = new int[square * 6];
    for (int i = 0; i < square; ++i) {
        Vector3 angleDir1 = Quaternion.AngleAxis((360f / square) * i, dir) * dirVertical;
        Vector3 angleDir2 = Quaternion.AngleAxis((360f / square) * (i + 1), dir) * dirVertical;
        //vertex
        vertices[i * 4 + 0] = from + angleDir1;
        vertices[i * 4 + 1] = to + angleDir1;
        vertices[i * 4 + 2] = from + angleDir2;
        vertices[i * 4 + 3] = to + angleDir2;
        //uv
        uvs[i * 4 + 0] = new Vector2(0f, 0f);
        uvs[i * 4 + 1] = new Vector2(1f, 0f);
        uvs[i * 4 + 2] = new Vector2(0f, 1f);
        uvs[i * 4 + 3] = new Vector2(1f, 1f);
        //Color
        colors[i * 4 + 0] = fromColor;
        colors[i * 4 + 1] = toColor;
        colors[i * 4 + 2] = fromColor;
        colors[i * 4 + 3] = toColor;
        //triangles
        triangles[i * 6 + 0] = i * 4 + 0;
        triangles[i * 6 + 1] = i * 4 + 2;
        triangles[i * 6 + 2] = i * 4 + 1;

        triangles[i * 6 + 3] = i * 4 + 2;
        triangles[i * 6 + 4] = i * 4 + 3;
        triangles[i * 6 + 5] = i * 4 + 1;
    }
    Mesh m = new Mesh();
    m.vertices = vertices;
    m.uv = uvs;
    m.colors = colors;
    m.triangles = triangles;
    return m;
}

改めてみる汚ねぇ・・・(突貫なのでゆるして
やってることは

  1. 「from」から「to」へのベクトル「dir」を取得
  2. ベクトル「dir」に直行するベクトルの一つ「dirVertical」を取得
  3. メッシュを構成する「頂点」、「uv」、「頂点カラー」、「トライアングルインデックス」の配列を準備
  4. それぞれの値を設定する
  5. メッシュにそれぞれの値を設定して返す

となってます。

ふぅ・・・

使い方とかエディタ拡張のコードとかシェーダとかは次回やると思いまふ。

ではではノ

最終的なコード

ブログ始め

新しくブログを始めてみました。

前のブログはやる気そこそこ出始めたのですが長続きしなかったので、

今度は頑張って書いていけたらいいなぁ。

 

書けたらいいなぁ・・・(届かぬ思い

 

主に書いていく内容は備忘録的な要素が多くなると思います。

 

 

10秒チンしたバターロールみたいな暖かさで見守っていてください。

バターロール美味しいよね。

 

ではでは。