#はじめに
画面に一度に大量のオブジェクトを出す時、Instantiate()とDestroy()を使ってインスタンスの生成と破棄を行うと重いらしいので、そういう時は事前にある程度のオブジェクトをいくつか生成しておいて、それを使いまわすという方法が良いとのことらしいです。
なので今回はオブジェクトプーリングの概要がわかったので実際に作ってみました。
#コードについて
##Bulletプール搭載ランチャー
ランチャーについてるスクリプトです。
Start関数で配列にBulletインスタンスを格納してます。
ついでにランチャーの子オブジェクトにしてます。
今回は円形弾幕を10フレームに一発出す感じで、弾幕の弾は144方向に放ちます。
//弾インスタンスからスクリプト取得
のコメントアウトの下にあるpoolBullet関数が、
今回のInstantiate()の代わりです。
public GameObject pool_Bullet;
public float _Velocity_0, Degree, Angle_Split;
//とりあえずの配列数144*16
GameObject[] Bullet = new GameObject[2304];
float _theta, WaitFrame = 0;
float PI = Mathf.PI;
private void Start() {
//配列に格納して子オブジェクトに
for (int i = 0; i < 1296; i++) {
Bullet[i] = (GameObject)Instantiate(pool_Bullet);
Bullet[i].transform.parent = gameObject.transform;
Bullet[i].SetActive(false);
}
}
void Update() {
WaitFrame++;
//10フレームに1回円形弾幕
if (WaitFrame > 10) {
for (int i = 0; i <= (Angle_Split - 1); i++) {
//n-way弾流用。360度方向にだす
float AngleRange = PI * (Degree / 180);
//弾インスタンスに渡す角度の計算
_theta = (AngleRange / (Angle_Split - 1)) * i + 0.5f * (PI - AngleRange);
//弾インスタンスからスクリプト取得
GameObject Bullet_obj = poolBullet();
BulletSc2 bullet_cs = Bullet_obj.GetComponent<BulletSc2>();
//アクティブになったフラッグ
bullet_cs.Bulletmove_flag = true;
//角度と初速に加えて、ランチャーの座標も渡す(消滅までの距離測るため)
bullet_cs.Lpos = transform.position;
bullet_cs.theta = _theta;
bullet_cs.Velocity_0 = _Velocity_0;
}
WaitFrame = 0;
}
}
##poolBullet関数
poolBullet関数です。GameObject型?っていっていいのかわかりませんが、戻り値でGameObjectを返します。
for文でプールしてるオブジェクトのアクティブ、非アクティブを確認し、非アクティブなオブジェクトがあれば、そのオブジェクトをアクティブにして返します。
Obj_No変数についてですが、ループを抜け出す前に何番のオブジェクトをアクティブにしたのか記録する役目を持ちます。"-1"が入っているのは下のif文に入るためです。
if文はObj_Noが"-1"、つまり非アクティブなオブジェクトが見つからずループを抜けてきた場合に入ります。こちらはInstantiate()で足りない弾インスタンスを生成して配列に格納しています。
GameObject poolBullet() {
//プールから使うオブジェクトのナンバー。初期化はー1
int Obj_No = -1;
//プールの中のオブジェクトの数
int Obj_Cnt = transform.childCount;
//for文で調べる非アクティブのオブジェクトがあれば抜ける
for (int i = 0; i < Obj_Cnt; i++) {
if (!Bullet[i].activeSelf) {
Obj_No = i;
Bullet[Obj_No].SetActive(true);
break;
}
}
//プール中のオブジェクト全てアクティブなら、増やして新しく配列に入れる
if (Obj_No == -1) {
Obj_No = Obj_Cnt;
Bullet[Obj_No] = (GameObject)Instantiate(pool_Bullet);
Bullet[Obj_No].transform.parent = gameObject.transform;
}
//使うオブジェクトを返す
return Bullet[Obj_No];
}
##弾についてるスクリプト
Instantiate()で生成してたときはStart関数が必ず呼び出されるので、そこで加速度を与えていましたが、今回のやり方だと毎回Start関数を呼び出せないので、これまでの加速度を与えてた処理がUpdate関数の中に移動してます。アクティブになる時に一度だけ入るようになってます。
Object_false関数について
今回の弾インスタンスの削除(非アクティブ)になる条件はランチャーとの距離にしてます。また、非アクティブになる前に再びランチャーの場所から弾が出るよう座標もリセットしてます。
ちなみに、OnEnable()、OnDisable()というアクティブ、非アクティブになった時に入る関数があるのでそちらを使おうと思ったのですが、うまくいかなかったのでこの形になってます。
public float Velocity_0, theta;
public bool Bulletmove_flag;
public Vector2 Lpos;
Rigidbody2D rid2d;
private void Start() {
//Rigidbody取得
rid2d = GetComponent<Rigidbody2D>();
}
private void Update() {
//アクティブになった時の弾の動き
if (Bulletmove_flag) {
//角度を考慮して弾の速度計算
Vector2 bulletV = rid2d.velocity;
bulletV.x = Velocity_0 * Mathf.Cos(theta);
bulletV.y = Velocity_0 * Mathf.Sin(theta);
rid2d.velocity = bulletV;
Bulletmove_flag = false;
}
//弾の座標とランチャーから送られてきた座標で距離を計算
Vector2 Bpos = transform.position;
float Distance_to_Luncher = Vector2.Distance(Bpos, Lpos);
//今回はランチャーからの距離が消滅の条件
if (Distance_to_Luncher >= 50) Object_false();
}
//非アクティブになる前に座標をリセット
void Object_false() {
transform.position = Lpos;
gameObject.SetActive(false);
}
#実行結果
今回のオブジェクトプーリングで円形弾幕
FPSはGameウィンドウのStatus表示で見てみました。
大体高くて63FPS~低くて42FPSぐらいみたいです。
こちらはまったく同じ弾幕に対して、
生成と破棄をInstantiate()とDestroy()を使ってやってみました。
オブジェクトプーリングと比較して60FPSを越えることはなく、50FPSを切ることがやや多いです。
#まとめ
なかなか、こういう弾幕系のものに対してのオブジェクトプーリングがなかったので作成してみました。何かしら役に立ちそうなところがあれば幸いです。
オブジェクトプーリング初めて作ってみましたが、なんともいえないできになってしまいました。
使ってないものと比べてもっと劇的に処理の差が出るかと思ったのですが、比較の仕方が悪い可能性もあります。というか今の仕様だと使える弾インスタンスを探すために上から順に探して言ってるので、あそこをどうにかすればもう少し軽くなるかもしれません。
今回は配列に弾インスタンスを格納してプールしてますが、C#にはListなる機能があるらしく、読んでみた限りそちらを使えばループ処理をせずとも、プールから使えるオブジェクトを探してこれる感じがしてます。改良の余地ありです。