64
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Unity2D】2Dゲームを作るときのオブジェクト管理クラスの設計

Last updated at Posted at 2015-01-29

はじめに

前回は「MonoBehaviourを使いやすくするクラスの設計」を紹介しましたが、今回はそのオブジェクトを管理するクラスを紹介します。

なぜオブジェクト管理クラスが必要なのか?

オブジェクト管理クラスを用意する理由は以下のとおりです。

  1. GameObject.Findが重いので高速にしたい
  2. GameObject.Instantiateが重たいので高速にしたい
  3. 特定のグループをまとめて処理したい

オブジェクトを検索するためのメソッドGameObject.Findは比較的重い処理となります。詳しくはこちらに速度の検証結果があります。

リアルタイムゲームではどうしても速度を求められるので、Find系はできるだけ避ける必要があります。一応Tagを使うことで高速化できるようですが、なるべくスクリプトだけで完結したいのであえて使わないようにしています(ここらへんは好みの問題ですけれども)。
またGameObject.Instantiateも重たいという話も聞いたことがあります。例えば、PS Vita版のUnityを使った際にInstantiateが重すぎてゲームにならない、ということでした。PS Vitaはmemcpyのようなメモリアクセスが遅いという情報もあり、それが影響しているのかもしれません。それはともかく、Windowsだけで動かすならそれほど問題にならないですが、将来的なことも考えて、できるだけ高速化したいものです。
そこで、ゲーム起動時やシーン開始時に、まとめてオブジェクトを確保しておくクラスを用意するようにしました。

ということで独自のインスタンス管理は高速化のために重要ですが、それとは別に、特定のゲームオブジェクトのグループをまとめて操作したいケースがあります。例えば、ヒットストップを実装しようとすると、ゲームキャラクターの動きをすべて止めてエフェクトだけ動かす、ということが必要となります。他にもシューティングゲームでボムを使ったら、敵弾をすべて消す、すべての敵に特定ダメージを与える、ということが必要となるかもしれません。
このようにゲームを作っていると、「敵だけ○○したい」「すべての敵弾を△△したい」「初音ミクと☓☓したい」というようなケースがしばしば発生します。それを楽にするためにも、管理クラスが必要となります。

使い方

先にTokenMgrの使い方の例を紹介します。以下は敵(Enemy)クラスの実装例です。Enemyクラスにparentのstatic変数をもたせ、どこからでも呼び出せるようにstaticなAddメソッドを用意しておきます。

Enemy.cs
/// 敵
public class Enemy : Token
{
  /// 管理オブジェクト
  public static TokenMgr<Enemy> parent = null;

  public static Enemy Add (int id, float x, float y, float direction, float speed)
  {
    // Enemyインスタンスの取得
    Enemy e = parent.Add (x, y, direction, speed);
    // IDを設定したり固有の処理をする
    e.SetParam (id);
    return e;
  }
  ……

EnemyMgrクラスを作らずEnemyのメンバにしてしまう理由は、管理するだけのクラスを作るのは手間となるからです。通常はこれで事足りる場合が多いです。ただ、もし色々な管理をさせたいのであれば、TokenMgrを継承して管理クラスを実装したほうがいいかもしれません。TokenMgrの生成タイミングですが、シーンの開始時にゲーム制御用のクラスで生成します。

GameMgr.cs
  void Start() {
    // 管理オブジェクトを生成
    parent = new TokenMgr<Enemy> ("Enemy", 64);
  }

最初の引数はプレハブ名です。2つ目の引数はアロケーションするサイズとなります。つまり、"Enemy"プレハブからEnemyクラスを"64"だけ確保する、ということになります。

管理するオブジェクトの定義

管理するオブジェクトの詳細は「MonoBehaviourを使いやすくするクラスの設計」にありますが、管理に必要な最低限のプロパティとメソッドは以下のとおりです

  • Exsitsプロパティ(bool): 生存フラグ
  • Vanishメソッド: 生存フラグをfalseにする / 無効化する
  • Reviveメソッド: 生存フラグをtrueにする / 有効化する

管理クラス

クラス定義とメンバ変数

管理クラスの定義とメンバ変数は以下のようにしました

TokenMgr.cs
/// Token管理クラス
public class TokenMgr<Type> where Type : Token {
  /// 保持するオブジェクトの数
  int _size = 0;
  /// プレハブ
  GameObject _prefab = null;
  /// 保持しているオブジェクトのリスト
  List<Type> _pool = null;
  /// Order in Layer
  int _order = 0;
  ……

オブジェクトはListで管理するので_sizeはいらないかもしれませんが、一応持たせることにしました。またジェネリックの型はTokenで制約をかけています。
それと、_orderでOrder in Layerの値を保持しているのは、同じLayerグループに同じSortingOrderの値のオブジェクトがいると、重なった際にちらつきが発生することがあります。それを回避するため、生成するたびにインクリメントをする、という作りになっています。

コンストラクタ

管理クラスのコンストラクタは以下のようにしました。

TokenMgr.cs
  /// コンストラクタ
  /// プレハブは必ず"Resources/Prefabs/"に配置すること
  public TokenMgr(string prefabName, int size=0) {
    _size = size;
    _prefab = Resources.Load("Prefabs/"+prefabName) as GameObject;
    if(_prefab == null) {
      Debug.LogError("Not found prefab. name="+prefabName);
    }
    _pool = new List<Type>();

    if(size > 0) {
      // サイズ指定があれば固定アロケーションとする
      for(int i = 0; i < size; i++) {
        GameObject g = GameObject.Instantiate(_prefab, new Vector3(), Quaternion.identity) as GameObject;
        Type obj = g.GetComponent<Type>();
        obj.Vanish();
        _pool.Add(obj);
      }
    }
  }

コンストラクタでプレハブを取得し、固定アロケーションサイズが「0」より大きければ、インスタンスを必要なぶんだけ先に確保するようにしています。なお生成したインスタンスはToken.Vanishを呼び出して無効化しています。

インスタンスの取り出し

確保しているインスタンスを取り出すために、Addメソッドを用意しました。

TokenMgr.cs
  /// インスタンスを取得する
  public Type Add(float x, float y, float direction=0.0f, float speed=0.0f) {
    foreach(Type obj in _pool) {
      if(obj.Exists == false) {
        // 未使用のオブジェクトを見つけた
        return _Recycle(obj, x, y, direction, speed);
      }
    }

    if(_size == 0) {
      // 自動で拡張
      GameObject g = GameObject.Instantiate(_prefab, new Vector3(), Quaternion.identity) as GameObject;
      Type obj = g.GetComponent<Type>();
      _pool.Add(obj);
      return _Recycle(obj, x, y, direction, speed);
    }
    return null;
  }

保持しているTokenをListの先頭から調べて、Token.Existsがfalse(未使用)であれば、それを再利用するようにしています。ただし、インスタンス数が足りない場合はnullを返すので、インスタンスの取得時にはnullチェックが必要です。それと_sizeが「0」である場合には自動で拡張するようにしています。ですが、Instantiateは重たい処理なので、通常は使わないほうがよいかもしれません。

ラムダ式でインスタンスを操作する

ここまで見てきたように、TokenMgrはインスタンスの「生成」や「取得」を楽に扱えるようにしたクラスです。

これだけでも便利なのですが、もう1つメリットがあります。それがラムダ式によるインスタンスの操作です。

TokenMgr.cs
  /// ForEach関数に渡す関数の型
  public delegate void FuncT(Type t);
  
  /// 生存するインスタンスに対してラムダ式を実行する
  public void ForEachExist(FuncT func) {
    foreach(var obj in _pool) {
      if(obj.Exists) {
        func(obj);
      }
    }
  }

これはToken.Existsがtrueであるインスタンスに特定の操作をする関数です。使用例は以下のコードです。

  if(IsDestroyBoss()) {
    // ボスを倒したので、敵をすべて消滅させる
    Enemy.parent.ForEachExist(t => t.Vanish());
  }

例はボスを倒した際に、敵を全滅させる処理となります。TokenMgr.ForEachExistにはラムダ式を渡すことができます。それにより生存しているインスタンスすべてに対して、特定の処理をさせることが可能になります。

他にも生成数をカウントする処理もできます。

  int ret = 0;
  Enemy.parent.ForEachExist(t => ret++);
  Debug.Log("敵の生存数:" + ret);

課題

この管理クラスには少し問題があります。それはシーンが切り替わった際に保持しているインスタンスが破棄されてしまいます。そのためゲームシーンに入った場合に、毎回アロケーションされてしまいます。これを回避するにはシーンが切り替わっても破棄しないようにDontDestroyOnLoadをTokenに実装する必要があります。

ソースコード

オブジェクト管理クラスのソースコードのすべては、以下のようになります。

TokenMgr.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/// Token管理クラス
public class TokenMgr<Type> where Type : Token {
  int _size = 0;
  GameObject _prefab = null;
  List<Type> _pool = null;
  /// Order in Layer
  int _order = 0;
  /// ForEach関数に渡す関数の型
  public delegate void FuncT(Type t);
  /// コンストラクタ
  /// プレハブは必ず"Resources/Prefabs/"に配置すること
  public TokenMgr(string prefabName, int size=0) {
    _size = size;
    _prefab = Resources.Load("Prefabs/"+prefabName) as GameObject;
    if(_prefab == null) {
      Debug.LogError("Not found prefab. name="+prefabName);
    }
    _pool = new List<Type>();

    if(size > 0) {
      // サイズ指定があれば固定アロケーションとする
      for(int i = 0; i < size; i++) {
        GameObject g = GameObject.Instantiate(_prefab, new Vector3(), Quaternion.identity) as GameObject;
        Type obj = g.GetComponent<Type>();
        obj.Vanish();
        _pool.Add(obj);
      }
    }
  }
  /// オブジェクトを再利用する
  Type _Recycle(Type obj, float x, float y, float direction, float speed) {
    // 復活
    obj.Revive();
    obj.SetPosition(x, y);
    if(obj.RigidBody != null) {
      // Rigidbody2Dがあるときのみ速度を設定する
      obj.SetVelocity(direction, speed);
    }
    obj.Angle = 0;
    // Order in Layerをインクリメントして設定する
    obj.SortingOrder = _order;
    _order++;
    return obj;
  }

  /// インスタンスを取得する
  public Type Add(float x, float y, float direction=0.0f, float speed=0.0f) {
    foreach(Type obj in _pool) {
      if(obj.Exists == false) {
        // 未使用のオブジェクトを見つけた
        return _Recycle(obj, x, y, direction, speed);
      }
    }

    if(_size == 0) {
      // 自動で拡張
      GameObject g = GameObject.Instantiate(_prefab, new Vector3(), Quaternion.identity) as GameObject;
      Type obj = g.GetComponent<Type>();
      _pool.Add(obj);
      return _Recycle(obj, x, y, direction, speed);
    }
    return null;
  }

  /// 生存するインスタンスに対してラムダ式を実行する
  public void ForEachExist(FuncT func) {
    foreach(var obj in _pool) {
      if(obj.Exists) {
        func(obj);
      }
    }
  }

  /// 生存しているインスタンスをすべて破棄する
  public void Vanish() {
    ForEachExist(t => t.Vanish());
  }

  /// インスタンスの生存数を取得する
  public int Count() {
    int ret = 0;
    ForEachExist(t => ret++);

    return ret;
  }
}

64
85
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
64
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?