Technology Blog
技術ブログ
2019.04.09
Unity3D+シューティングゲームのオブジェクトプールのチュートリアル
Blogをいつもご覧いただきありがとうございます。
シューティングゲームを作った時に、武器の弾のシステムは様々な書き方がありますが、一番シンプルな書き方は、恐らく弾のオブジェクトをインスタンスして、敵にヒットしたら(コード側ではコライダーやポジションでどちらでも可能です。)、敵と弾一緒に消滅すると思います。
弾を生成する場合の例:
using UnityEngine; using System.Collections; public class BulletInstance : MonoBehaviour { public GameObject BulletPrefab; // Update is called once per frame void Update () { GameObject bullet = Instantiate(BulletPrefab, Vector3.zero, Quaternion.identity) as GameObject; //弾をインスタンス }
弾と敵を消滅する場合の例:
using UnityEngine; using System.Collections; public class Enemy : MonoBehaviour { void OnTriggerEnter(Collider other) { if (other.tag.CompareTo("Bullet") == 0) { Destroy(other.gameObject);//弾が消される Destroy(this.gameObject);//敵自分が消される } } }
この書き方は普通に動くはずですが、でも、常に弾のPrefabをインスタンスしたり、Destroyしたりしたら、メモリー上にとってあまり優しいとはいえません。正直に言うとPC用としてはまだしも、スマートフォンゲームの場合はかなりきびしいものになると思います。
オブジェクトをずっと増やし続けるとゲームのFPSが落ちたり、重くなったり、スマートフォンが熱くなったりするのもおかしくないです。
それをさける為に、考えを逆にするのはどうでしょうか?
プログラム側では、オブジェクトのInstanceやDestroyの場合にリソースを消費しているとしたら、なら、常にInstanceやDestroyしないシステムを作ればいいじゃんないか?
それでは、オブジェクトプールを勉強しましょう!
·オブジェクトプールの概念
オブジェクトプールとは、この先使えるオブジェクトを溜めておく用の倉庫のような存在で、オブジェクトが必要になったら取り出し、使い終わったら戻すことで、なるべく少数のオブジェクト使い回してオブジェクト数及び生成・消滅のコストを抑えるものです。
それでは、実際にUnity3D側で作っていきましょう。
·実装
Step1 Unityの環境作り
Step2 弾のプールシステム
ここで二つのスクリプトが必要です。
- BulletPool.cs このスクリプトはオブジェクトプールで、オブジェクトにドラッグドロップする必要はありません。
- Bullets.cs このスクリプトは弾のPrefabにドラッグドロップするスクリプトです,弾の方向やスピードをコントロールしています。
実装コード:
BulletPool.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class BulletPool : MonoBehaviour { //オブジェクトのプールとしてのリストを作る List pools = new List(); // start pos Vector3 firstPosition; //シングルトン private static BulletPool instance; private BulletPool() { } public static BulletPool GetInstance() { if (instance == null) { instance = new GameObject("BulletPool").AddComponent(); } return instance; } //オブジェクトプールからオブジェクトを取得する public GameObject MyInstantiate(GameObject name) { //オブジェクトプールは空だから、インスタンスする if (pools.Count == 0) { return Instantiate(name, Vector3.zero, Quaternion.identity) as GameObject; } else { //もしプールでは空じゃない場合は「0」を取る GameObject go = pools[0]; go.SetActive(true); //オブジェクトプールから消される pools.Remove(go); return go; } } //オブジェクトプールを作成 public void DelayInstantiate(GameObject name) { //オブジェクトを隠す name.SetActive(false); //オブジェクトプールに入れる pools.Add(name); } }
Bullets.cs
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Bullets : MonoBehaviour { Transform target; private void OnEnable() { //ポジションを初期化する target = GameObject.Find("Hero").transform; transform.position = target.position; //二秒の後でCoroutineを実行する StartCoroutine(DelayObject(2f)); } private void Update() { //弾の移動 transform.Translate(Vector3.up * Time.deltaTime * 100); } IEnumerator DelayObject(float time) { yield return new WaitForSeconds(time); //弾のプールリストに入れる BulletPool.GetInstance().DelayInstantiate(gameObject); } }
Step3 Playerのコントロール
弾のプールを実装完了したら、次は発砲の機能や移動の機能が入ってる”PlayerControlManager.cs”を「Hero」のオブジェクトにドラッグドロップしてください。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class PlayerControlManager : MonoBehaviour { float _hor_Speed = 1f; //水平移動の速度 float _ver_Speed = 1f; //垂直移動の速度 float _topMax = 300f;//Player移動制御の変数 float _bottomMax = -300f;//Player移動制御の変数 float _sideMax =177f;//Player移動制御の変数 float _turnAngle = 45;//回転の角度 GameObject bulletPrefab; void Awake(){ bulletPrefab = Resources.Load("Prefabs/Bullet") as GameObject; } void Update() { Move(); TurnBody(); Limit(); Fire(); } void Fire()//発砲 { if (Input.GetKeyDown(KeyCode.Space)) { BulletPool.GetInstance().MyInstantiate(bulletPrefab); } } void Limit()//移動制御 { if (transform.localPosition.x > _sideMax) { transform.localPosition = new Vector2(_sideMax, transform.localPosition.y); } if (transform.localPosition.x < -_sideMax) { transform.localPosition = new Vector2(-_sideMax, transform.localPosition.y); } if (transform.localPosition.y > _topMax) { transform.localPosition = new Vector2(transform.localPosition.x, _topMax); } if (transform.localPosition.y < _bottomMax) { transform.localPosition = new Vector2(transform.localPosition.x, _bottomMax); } } void TurnBody()//機体を回転する { if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow)) { transform.Rotate(new Vector3(0, _turnAngle, 0)); } if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow)) { transform.Rotate(new Vector3(0, -_turnAngle, 0)); } if (Input.GetKeyUp(KeyCode.A) || Input.GetKeyUp(KeyCode.LeftArrow)) { transform.Rotate(new Vector3(0, -_turnAngle, 0)); } if (Input.GetKeyUp(KeyCode.D) || Input.GetKeyUp(KeyCode.RightArrow)) { transform.Rotate(new Vector3(0, _turnAngle, 0)); } } void Move()//Playerの移動 { float ver = Input.GetAxis("Vertical"); float hor = Input.GetAxis("Horizontal"); transform.position +=new Vector3(hor * _hor_Speed, ver * _ver_Speed, 0); } }
上記の流れに沿って実装ができたら、プロジェクトをPlayしてください。
こんな感じですね
·注意点:
- オブジェクトプールは非常に便利です。使う場面として繰り返し利用するオブジェクトを生成する場合に有効です。今回の場合は弾や敵などです。
- 使う場合によっては逆効果が出る可能性もあります。実際にゲームのジャンルやシステム、オブジェクトの特徴などによって、どのような形式のオブジェクトプールを使うのか考える必要があると思います。
- オブジェクトを破棄する代わりに隠す(表示しないようにする)ことで、表示的には破棄と同等の効果実現してます。そのおかげでオブジェクトをインスタンス化する際のリソースの無駄な消費を抑えられると思います。
※補足情報としてオブジェクトプールを利用することで、オブジェクトのインスタンス化をできるだけ削減しましたが、既にインスタンスされたオブジェクトはSetActive(false)になっても、まだメモリーの中に存在し、メモリーの容量を占有しています。メモリの占有が完全になくなるわけではありません。
ご覧いただきありがとうございました。