EasyCastleUNITY

SpaceShooter2D 오브젝트 풀링 응용 본문

유니티 심화

SpaceShooter2D 오브젝트 풀링 응용

EasyCastleT 2023. 8. 28. 22:58

모든 것을 GameMain에서 통괄한다. 

BulletPoolManager -> 싱글톤을 통해 어디서든 접근 가능 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletPoolManager : MonoBehaviour
{
    //총알을 미리 생성해 저장할 리스트 
    [SerializeField] private List<GameObject> bulletOnePool = new List<GameObject>(); //총알 1 저장
    [SerializeField] private List<GameObject> bulletTwoPool = new List<GameObject>(); //총알 2 저장
    [SerializeField] private List<GameObject> bulletThreePool = new List<GameObject>(); //총알 3 저장
    //오브젝트 풀에 생성할 총알의 최대 개수
    [SerializeField] private int maxBullets = 10;
    //싱글톤 인스턴스 선언 
    public static BulletPoolManager instance = null;
    //총알 프리팹 
    [SerializeField] private List<GameObject> bulletPrefabs;

    //----------------------속성----------------------------
    public List<GameObject> BulletOnePool
    {
        get { return this.bulletOnePool; }
    }

    public List<GameObject> BulletTwoPool
    {
        get { return this.bulletTwoPool; }
    }

    public List<GameObject> BulletThreePool
    {
        get { return this.bulletThreePool; }
    }
    public List<GameObject> BulletPrefabs
    {
        get { return this.bulletPrefabs; }
    }
    //------------------------------------------------------
    List<GameObject> list = new List<GameObject>();

    public enum eBulletType
    {
        One, Double, Triple
    }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else if (instance != null)
        {
            Destroy(this.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);

    }
    // Start is called before the first frame update
    void Start()
    {
        this.CreateBulletPool();
    }

    private void CreateBulletPool() //10개씩 총알 만들어서 각자 리스트에 저장 
    {
        for (int i = 0; i < maxBullets; i++)
        {
            GameObject bulletOneGo = Instantiate<GameObject>(this.bulletPrefabs[0]);
            bulletOneGo.SetActive(false);
            bulletOneGo.transform.SetParent(this.transform);
            bulletOnePool.Add(bulletOneGo);

            GameObject bulletTwoGo = Instantiate<GameObject>(this.bulletPrefabs[1]);
            bulletTwoGo.SetActive(false);
            bulletTwoGo.transform.SetParent(this.transform);
            bulletTwoPool.Add(bulletTwoGo);

            GameObject bulletThreeGo = Instantiate<GameObject>(this.bulletPrefabs[2]);
            bulletThreeGo.SetActive(false);
            bulletThreeGo.transform.SetParent(this.transform);
            bulletThreePool.Add(bulletThreeGo);
        }
        
    }
    // 플레이어 파워에 따라 받아가는 총알 변경 
    public GameObject GetBulletInPool(int power) 
    {
        switch (power)
        {
            case 1:
                foreach (GameObject bullet in this.bulletOnePool)
                {
                    if (bullet.activeSelf == false)
                    {
                        return bullet;
                    }
                }
                break;
            case 2:
                foreach (GameObject bullet in this.bulletTwoPool)
                {
                    if (bullet.activeSelf == false)
                    {
                        return bullet;
                    }
                }
                break;
            case 3:
                foreach (GameObject bullet in this.bulletThreePool)
                {
                    if (bullet.activeSelf == false)
                    {
                        return bullet;
                    }
                }
                break;
        }
        return null;
    }
    //원래 위치로 돌아감 
    public void ReleaseBullet(GameObject bulletGo)
    {
        bulletGo.SetActive(false);
        bulletGo.transform.SetParent(this.transform);
    }
    //사용하지 않고 있는 메서드 
    private void CreateBullet(eBulletType type)
    {
        GameObject bulletGo = Instantiate<GameObject>(this.bulletPrefabs[(int)type]);
        bulletGo.SetActive(false);
        bulletGo.transform.SetParent(this.transform);
        list.Add(bulletGo);
        if (type == eBulletType.One)
        {
            this.bulletOnePool = list;

        }
        else if (type == eBulletType.Double)
        {
            this.bulletTwoPool = list;

        }
        else if (type == eBulletType.Triple)
        {
            this.bulletThreePool = list;

        }
    }
}

BulletControl -> 총알 각각을 제어하는 스크립트 컴포넌트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletControl : MonoBehaviour
{
    private float moveSpeed = 3.0f;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.Translate(Vector2.up * this.moveSpeed * Time.deltaTime);
        this.SelfComeback();
    }

    private void SelfComeback()
    {
        if(this.transform.position.y > 4.8f)
        {
            Debug.Log("<color=yellow>총알 풀로 되돌아감</color>");
            BulletPoolManager.instance.ReleaseBullet(this.gameObject);
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("<color=yellow>총알 풀로 되돌아감</color>");
        BulletPoolManager.instance.ReleaseBullet(this.gameObject);
        //Destroy(this.gameObject);
    }
}

BulletGenerator ->

오브젝트 풀링을 통해, 총알을 받아오거나,

만들어둔 총알이 부족할 경우 그 순간에 총알을 동적생성하고, 재사용하기 위해 BulletPool에 등록한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletGenerator : MonoBehaviour
{
    [SerializeField]
    private Transform BulletInitPos; //총알 생성 위치

    private float elapsedTime = 0.0f;
    [SerializeField]
    private float intervelTime = 0.3f;


    public void Shoot(int power)
    {
        if (Input.GetKey(KeyCode.Space)) //space 누르고 있는 동안 
        {
            this.elapsedTime += Time.deltaTime;
            if(this.elapsedTime > this.intervelTime) //0.3초 마다 총알을 부름
            {
                this.CallBullet(power);
                this.elapsedTime = 0.0f;
            }
        }
    }

    private void CallBullet(int power)
    {
        GameObject go = BulletPoolManager.instance.GetBulletInPool(power); //만약 없으면, 그 자리에서 복제하고 pool에 넣는다.
        if (go == null) //이미 만들어둔 오브젝트를 다 사용하면 
        {
            go = Instantiate(BulletPoolManager.instance.BulletPrefabs[power - 1]);
            if (power == 1) BulletPoolManager.instance.BulletOnePool.Add(go);
            else if (power == 2) BulletPoolManager.instance.BulletTwoPool.Add(go);
            else if (power == 3) BulletPoolManager.instance.BulletThreePool.Add(go);
            go.transform.position = this.BulletInitPos.position;
            Debug.LogFormat("<color=yellow>{0} 총알 동적 생성</color>", go.name);
        }
        else if (go != null)
        {
            go.transform.SetParent(null);
            go.transform.position = this.BulletInitPos.position;
            go.SetActive(true);
        }
    }
}

ExplosionPoolManager -> 미리 생성해 둔, 폭발 이펙트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ExplosionPoolManager : MonoBehaviour
{
    //폭발을 미리 생성해 저장할 리스트 
    [SerializeField] private List<GameObject> explosionPool = new List<GameObject>();
    //오브젝트 풀에 생성할 폭발의 최대 개수
    [SerializeField] private int maxBullets = 5;
    //폭발 프리팹
    [SerializeField] private GameObject explosionPrefab;
    //싱글톤 인스턴스 선언 
    public static ExplosionPoolManager instance = null;
    private void Awake()
    {
        if(instance == null)
        {
            instance = this;
        }
        else if(instance != null)
        {
            Destroy(this.gameObject);
        }
        DontDestroyOnLoad(this.gameObject);
    }
    // Start is called before the first frame update
    void Start()
    {
        this.CreateExplosionPool();
    }

    private void CreateExplosionPool()
    {
        for(int i = 0; i<maxBullets; i++)
        {
            GameObject explosionGo = Instantiate<GameObject>(explosionPrefab);
            explosionGo.SetActive(false);
            explosionGo.transform.SetParent(this.transform);
            explosionPool.Add(explosionGo);
        }
    }

    public GameObject GetExplosionInPool()
    {
        foreach(GameObject explosionGo in explosionPool)
        {
            if (explosionGo.activeSelf == false)
            {
                return explosionGo;
            }
        }
        return null;
    }

    public void ReleaseExplosion(GameObject explosion)
    {
        explosion.SetActive(false);
        explosion.transform.SetParent(this.transform);
    }
}

EnemyControl -> 적 각각을 제어하는 스크립트 컴포넌트, 대리자를 활용한다 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyControl : MonoBehaviour
{
    private Animator anim;
    private Coroutine routine;
    public System.Action onDie;
    [SerializeField]
    private int hp = 5;
    // Start is called before the first frame update
    void Start()
    {
        this.anim = GetComponent<Animator>();
    }


    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("<color=red>적 피격!</color>");
        this.HitDamage(1);
        this.PlayAnimation();
        if (this.hp <= 0)
        {
            this.Die();
        }
    }

    private void SelfDestroy()
    {
        Destroy(this.gameObject);
    }

    private void Die()
    {
        this.onDie(); //대리자 호출 
        StartCoroutine(CoDie());
    }
    private IEnumerator CoDie()
    {
        yield return new WaitForSeconds(0.167f);
        this.SelfDestroy();
        yield return null;
    }

    private void HitDamage(int power)
    {
        this.hp -= power;
    }
    private void PlayAnimation()
    {
        if(this.routine != null)
        {
            this.StopCoroutine(this.routine);
        }
        this.routine = StartCoroutine(CoPlayAnimation());
    }

    private IEnumerator CoPlayAnimation()
    {
        this.anim.SetBool("State", true);
        yield return new WaitForSeconds(0.167f);
        this.anim.SetBool("State", false);
        yield return null;

    }
}

PlayerControl -> 플레이어 제어 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerControl : MonoBehaviour
{
    enum eState
    {
        Idle, Left , Right
    }
    private Transform trans; //트랜스폼
    private Animator anim; //애니메이터 
    [SerializeField]
    private float moveSpeed = 2.0f; //이동속도
    //파워가 1이면, 한개 짜리 발사
    //파워가 2이면, 두개 짜리 발사
    //파워가 3이면, 세개 짜리 발사 
    [SerializeField] private int power = 1; 

    public int Power
    {
        get { return power; }
        set { power = value; }
    }
    
    // Start is called before the first frame update
    void Start()
    {
        this.trans =GetComponent<Transform>();
        this.anim = GetComponent<Animator>(); 
        // 0: Idle, 1:Left 2:Right
    }

    // Update is called once per frame
    void Update()
    {
        this.trans.position = this.ClampPosition(this.trans.position);
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");

        Vector2 moveDir = (Vector2.up * v) + (Vector2.right * h);
        this.trans.Translate(moveDir.normalized*this.moveSpeed*Time.deltaTime);

        if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))
        {
            this.PlayAnimation(eState.Left);
        } 
        else if(Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D)) {
            this.PlayAnimation(eState.Right);
        }
        else
        {
            this.PlayAnimation(eState.Idle);
        }

    }

    private Vector2 ClampPosition (Vector2 position)
    {
        return new Vector2(Mathf.Clamp(position.x, -2.3f, 2.3f),
            Mathf.Clamp(position.y, -4.5f, 4.5f));
    }

    private void PlayAnimation(eState state)
    {
        int istate = (int)state;
        this.anim.SetInteger("State", istate);
    }
}

GameMain -> 전체 게임 씬을 제어

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameMain : MonoBehaviour
{
    [SerializeField]
    private GameObject explosionPrefab;
    [SerializeField]
    private List<EnemyControl> EnemyList;
    [SerializeField]
    private BulletGenerator bulletGenerator;
    [SerializeField]
    private PlayerControl playerControl;
    [SerializeField]
    private EnemyControl enemyA;
    [SerializeField]
    private EnemyControl enemyB;
    [SerializeField]
    private EnemyControl enemyC;

    // Start is called before the first frame update
    void Start()
    {
        this.enemyA.onDie = () =>
        {
            GameObject go = ExplosionPoolManager.instance.GetExplosionInPool();
            go.transform.SetParent(null);
            //GameObject go = Instantiate(explosionPrefab);
            go.transform.position = enemyA.transform.position;
            go.SetActive(true);
            this.RemoveExplosion(go);
            playerControl.Power++;
            if (playerControl.Power > 3) playerControl.Power = 3;
        };

        this.enemyB.onDie = () => {
            GameObject go = ExplosionPoolManager.instance.GetExplosionInPool();
            go.transform.SetParent(null);
            go.transform.position = enemyB.transform.position;
            go.SetActive(true);
            this.RemoveExplosion(go);
            playerControl.Power++;
            if (playerControl.Power > 3) playerControl.Power = 3;
        };

        this.enemyC.onDie = () => {
            GameObject go = ExplosionPoolManager.instance.GetExplosionInPool();
            go.transform.SetParent(null);
            go.transform.position = enemyC.transform.position;
            go.SetActive(true);
            this.RemoveExplosion(go);
            playerControl.Power++;
            if (playerControl.Power > 3) playerControl.Power = 3;
        };
    }

    private void Update()
    {
        //power가 1이면, 1개짜리
        //power가 2이면, 2개짜리
        //power가 3이면, 3개짜리
        this.bulletGenerator.Shoot(this.playerControl.Power);
    }

    private void RemoveExplosion(GameObject go)
    {
        StartCoroutine(this.CoRemoveExplosion(go));
    }
    private IEnumerator CoRemoveExplosion(GameObject go)
    {
        yield return new WaitForSeconds(0.5f);
        //Destroy(go);
        ExplosionPoolManager.instance.ReleaseExplosion(go);
        yield return null;
    }
}

1. 임의로 적을 죽이면, 플레이어의 power가 1 증가한다. 

2. power에 따라, 발사하는 총알의 종류가 변경된다.

3. space키를 누르고 있으면, 0.3초 마다 총알을 발사한다. 

4. 총알을 발사하다, 이미 만들어둔 총알을 다 사용하는 순간, 동적으로 총알을 생성한다. 

5. 동적으로 생성한 총알은, 사라지는 것이 아닌, 각각의 BulletPool에 넣어 재활용한다. 

6. 결과적으로, 적을 죽이면 발사하는 총알의 종류가 바뀌도록 만들었다. 

시연 영상

말로만 들어왔던 오브젝트 풀링을 처음으로 적용시켜 보았다. 

사용하고 있는 오브젝트를 제대로 보기 위해, 사용하는 오브젝트는 밖으로 나오도록 설정했다. 

 

'유니티 심화' 카테고리의 다른 글

HeroShooter 개발일지1  (0) 2023.08.30
HeroShooter 중간과정 정리  (0) 2023.08.29
오브젝트 풀링 연습  (0) 2023.08.28
[과제]Hero Shooter Stage1 까지  (1) 2023.08.27
Space Shooter 간단한 UI 적용 (플레이어 Hp Bar)  (0) 2023.08.25