소소한 나의 하루들

2d 종스크롤 슈팅(8) - 최적화의 기본, 오브젝트 풀링 본문

개발/유니티

2d 종스크롤 슈팅(8) - 최적화의 기본, 오브젝트 풀링

소소한 나의 하루 2024. 2. 22. 22:15

출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared

 

📚 유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com

여태까지 슈팅게임을 만들면서 많은 프리펩을 사용해왔다. Instantiate(), Destroy() 등의 함수들을 많이 활용해왔다.


#1. 오브젝트 풀링?

https://whiny.tistory.com/17

 

오브젝트 풀링(Object Pooling)이란?

이번 포스팅에서는 다양한 최적화 기법들 중에서 오브젝트 풀링(Object Pooling)에 관해서 알아보겠습니다. 간단하게 오브젝트 풀링을 설명하자면, 오브젝트의 Pool 즉 웅덩이를 만들어두고, 그 웅덩

whiny.tistory.com

Instantiate(), Destroy()같은 경우에는 메모리를 계속 차지하면서, 나중에 Destroy()될 때 쓰레기 메모리를 남기게 된다.

: 프리펩을 생성, 삭제하면서 조각난 메모리가 쌓임

 

결국 그 메모리가 쌓이게 되면서 유니티 자체 내부에서 가비지 컬렉트(Garbage Collection, GC)라는 기술을 실행한다.

가비지 컬렉트(GC) : 쌓인 '조각난 메모리'를 비우는 기술

조각난 메모리가 많이 쌓이게되어 가비지 컬렉트가 실행될 때 렉이 심하게 발생한다. 실제로 퀄리티가 있는 상용 게임들은 가비지 컬렉트를 피하기 위한 오브젝트 풀링 기술을 필수로 사용한다.

 

오브젝트 풀링이라고 하면, '오브젝트가 가득 담긴 수영장'처럼 생각하고, 이미 가득 담겨있는 오브젝트를 SetActive()로 true / false만 조절하는 것이다. 

오브젝트 풀링 : 미리 생성해둔 풀에서 활성화/비활성화로 사용

유저가 플레이하면서 확인하는 게임 화면은 동일한데, 내부에서 구현되는 기술만 다른 것이다.


#1. 풀 생성

오브젝트 풀을 만들어야하니까 풀을 하나 만들어본다.

ObjectManager 오브젝트와 ObjectManager 스크립트를 생성한다. 오브젝트 풀을 만들려고 하는데 여러 기술이 있지만, 조금 간단하게 만들어보도록 한다.

public class ObjectManager : MonoBehaviour
{
    GameObject[] enemyL;
    GameObject[] enemyM;
    GameObject[] enemyS;

    GameObject[] itemCoin;
    GameObject[] itemPower;
    GameObject[] itemBoom;

    GameObject[] bulletPlayerA;
    GameObject[] bulletPlayerB;
    GameObject[] bulletEnemyA;
    GameObject[] bulletEnemyB;
}

여태까지 사용했던 프리펩들을 담을 배열을 만든다. 엄청나게 많은 양을 담을 것이기 때문에 이번에는 public을 사용하지 않는다. 

: 프리펩을 생성하여 저장할 변수 생성

private void Awake()
{
    EnemyL = new GameObject[10];
    EnemyM = new GameObject[10];
    EnemyS = new GameObject[20];

    itemCoin = new GameObject[20];
    itemPower = new GameObject[10];
    itemBoom = new GameObject[10];

    bulletPlayerA = new GameObject[100];
    bulletPlayerB = new GameObject[100];
    bulletEnemyA = new GameObject[100];
    bulletEnemyB = new GameObject[100];
    
    Generate();
}

enemy는 한꺼번에 많이는 넣지 않을 것이니까 한꺼번에 등장할 개수를 고려하여 배열 길이를 할당한다. 이렇게 배열의 길이설정을 완료하면 풀을 만들어준다.

public GameObject enemyLPrefab;
public GameObject enemyMPrefab;
public GameObject enemySPrefab;
public GameObject itemCoinPrefab;
public GameObject itemPowerPrefab;
public GameObject itemBoomPrefab;
public GameObject bulletPlayerAPrefab;
public GameObject bulletPlayerBPrefab;
public GameObject bulletEnemyAPrefab;
public GameObject bulletEnemyBPrefab;

Generate()라는 함수를 만들어주고, 이 안에서 for문을 활용하여 프리펩을 생성한다. 그렇게 하려면 프리펩 변수도 생성하고 할당시켜줘야한다. 이 프리펩들을 미리 생성을 해둔다.

void Generate()
{
    //Enemy
    for (int index = 0; index < enemyL.Length; index++)
    {
        enemyL[index] = Instantiate(enemyLPrefab);
        enemyL[index].SetActive(false);
    }

    for (int index = 0; index < enemyM.Length; index++)
    {
        enemyM[index] = Instantiate(enemyMPrefab);
        enemyM[index].SetActive(false);
    }

    for (int index = 0; index < enemyS.Length; index++)
    {
        enemyS[index] = Instantiate(enemySPrefab);
        enemyS[index].SetActive(false);
    }

    //Item
    for (int index = 0; index < itemCoin.Length; index++)
    {
        itemCoin[index] = Instantiate(itemCoinPrefab);
        itemCoin[index].SetActive(false);
    }

    for (int index = 0; index < itemPower.Length; index++)
    {
        itemPower[index] = Instantiate(itemPowerPrefab);
        itemPower[index].SetActive(false);
    }

    for (int index = 0; index < itemBoom.Length; index++)
    {
        itemBoom[index] = Instantiate(itemBoomPrefab);
        itemBoom[index].SetActive(false);
    }

    //Bullet
    for (int index = 0; index < bulletPlayerA.Length; index++)
    {
        bulletPlayerA[index] = Instantiate(bulletPlayerAPrefab);
        bulletPlayerA[index].SetActive(false);
    }

    for (int index = 0; index < bulletPlayerB.Length; index++)
    {
        bulletPlayerB[index] = Instantiate(bulletPlayerBPrefab);
        bulletPlayerB[index].SetActive(false);
    }

    for (int index = 0; index < bulletEnemyA.Length; index++)
    {
        bulletEnemyA[index] = Instantiate(bulletEnemyAPrefab);
        bulletEnemyA[index].SetActive(false);
    }

    for (int index = 0; index < bulletEnemyB.Length; index++)
    {
        bulletEnemyB[index] = Instantiate(bulletEnemyBPrefab);
        bulletEnemyB[index].SetActive(false);
    }
}

지금까지 instantiate() 함수를 통해 프리펩을 Scene에 생성할 때는 생성위치가 필요했는데, 오브젝트 풀은 생성위치가 필요없다. 그냥 만들고 위에서 만든 배열에 저장하면 된다.

: Instantiate()로 생성한 프리펩 인스턴스를 배열에 저장

 

그렇다면 Scene의 (0, 0, 0) 지점에 다 생길 것이다. 그래서 이 프리펩 오브젝트들이 처음부터 다 보이면 안되기 때문에 활성화시킨다. 

 

여기서 문제가 Awake()에서 배열들을 만들고, Generate()를 통해 배열에 프리펩을 생성해서 채워넣는데 시간이 걸릴 것이다. 실제 실행할 때에도 처음에 살짝 렉이 걸린다. 그래서 보통 게임들이 로딩할 때 이러한 작업들을 수행한다.

: 인스턴스를 생성해서 오브젝트 풀을 생성하기 위함

 로딩 시간 = 장면 배치 + 오브젝트 풀 생성 

 

이렇게 한번 오브젝트 풀을 생성하고나서는 이 발생하지 않는다. : 첫번째 로딩의 이유

 

실행해보면 Hierarchy창에 어마하게 많은 오브젝트들이 생성되어있다.


#2. 풀 활용

public GameObject MakeObj(string type)
{
    switch (type)
    {
        case "EnemyL":
            break;
        case "EnemyM":
            break;
        case "EnemyS":
            break;

        case "ItemCoin":
            break;
        case "ItemPower":
            break;
        case "ItemBoom":
            break;

        case "BulletPlayerA":
            break;
        case "BulletPlayerB":
            break;
        case "BulletEnemyA":
            break;
        case "BulletEnemyB":
            break;
    }
}

오브젝트 풀에 접근할 수 있는 함수를 생성한다. 반환형은 GameObject로 한다. 만약 "EnemyL"를 만들고 싶으면 배열 enemyL에서 꺼내오면 된다.

case "EnemyL":
    for (int index = 0; index < enemyL.Length; index++)
    {
        if (!enemyL[index].activeSelf)
        {
            enemyL[index].SetActive(true);
            return enemyL[index];
        }
    }

비활성화된 오브젝트에 접근하여 활성화한 후 반환한다. 이렇게 다른 오브젝트들도 다 해야하니까 저 로직만 빼서 switch문 밖에 작성해주고, 타겟 변수를 선언해서 타겟으로 처리해준다.

GameObject[] targetPool;

public GameObject MakeObj(string type)
{  
    switch (type)
    {
        case "EnemyL":
            targetPool = enemyL;
                break;
        case "EnemyM":
            targetPool = enemyM;
            break;
        case "EnemyS":
            targetPool = enemyS;
            break;

        case "ItemCoin":
            targetPool = itemCoin;
            break;
        case "ItemPower":
            targetPool = itemPower;
            break;
        case "ItemBoom":
            targetPool = itemBoom;
            break;

        case "BulletPlayerA":
            targetPool = bulletPlayerA;
            break;
        case "BulletPlayerB":
            targetPool = bulletPlayerB;
            break;
        case "BulletEnemyA":
            targetPool = bulletEnemyA;
            break;
        case "BulletEnemyB":
            targetPool = bulletEnemyB;
            break;
    }

    for (int index = 0; index < targetPool.Length; index++)
    {
        if (!targetPool[index].activeSelf)
        {
            targetPool[index].SetActive(true);
            return targetPool[index];
        }
    }

    return null;
}

함수 MakeObj()가 GameObject를 반환하니까 만약 for문을 거쳐서 GameObject를 반환할 수 없다면 return null;까지 작성해줘야 에러가 안난다.

 

이제 Instantiate()는 전부 MakeObj()로 대체해보도록 한다.

//GameManager
public GameObject[] enemyObjs;
public Transform[] spawnPoints;

void SpawnEnemy()
{
    int ranEnemy = Random.Range(0, 3);
    int ranPoint = Random.Range(0, 9);

    GameObject enemy = Instantiate(enemyObjs[ranEnemy], spawnPoints[ranPoint].position, spawnPoints[ranPoint].rotation);

적을 소환하는 것은 GameManager에서 담당하고 있었다. 이제 기존의 Instantiate()를 ObjectManager의 MakeObj()로 교체해보도록 한다. 그런데 문제가 있다. 우리는 random한 Enemy를 할당을 해줘서 사용했는데 ObejctManager의 MakeObj()는 불러오는 방식이 다르다.

//GameManager
public string[] enemyObjs;

    private void Awake()
    {
        playerLogic = player.GetComponent<Player>();
        enemyObjs = new string[] { "EnemyL", "EnemyM", "EnemyS"};
    }
    
    void SpawnEnemy()
{
    int ranEnemy = Random.Range(0, 3);
    int ranPoint = Random.Range(0, 9);

    GameObject enemy = objectManager.MakeObj(enemyObjs[ranEnemy]);
    enemy.transform.position = spawnPoints[ranPoint].position;

그래서 enemyObjs를 string 배열로 바꿔준다. 이제 프리펩 배열이 아니고 문자열 배열이 된다.

여기서 살짝 다르게 주어야할 것은 position과 rotation이다. 위치와 각도는 인스턴스 변수에서 적용시키도록 한다.

*rotation은 작성할 필요가 없다.

//ObjectManager
for (int index = 0; index < targetPool.Length; index++)
{
    if (!targetPool[index].activeSelf)
    {
        targetPool[index].SetActive(true);
        return targetPool[index];
    }
}

실행해보면 생성되는 것은 확인할 수 있지만, 플레이어 또는 적이 총알에 맞아 피격되거나, 총알 또는 적이 경계에 닿아서 파괴(Destroy)된다면 위 코드에서 targetPool[index]을 확인할 수 없어서 MissingReferenceException 에러 메세지가 나타난다.

 

오브젝트 풀링은 프리펩 오브젝트들을 이미 갖고있는 오브젝트 풀에서 프리펩을 필요할 때만 활성화시켰다 비활성화시키기 때문에 이제 Destroy()는 전부 SetActive(false)로 교체한다.

//Player
void Fire()
{
    if (!Input.GetButton("Fire1"))
        return;

    if (curShotDelay < maxShotDelay)
        return;

    switch (power)
    {
        case 1:
            //Power One
            //GameObject bullet = Instantiate(bulletObjA, transform.position, transform.rotation);
            GameObject bullet = objectManager.MakeObj("BulletPlayerA");
            bullet.transform.position = transform.position;
            Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
            rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
            break;
        case 2:
            //GameObject bulletR = Instantiate(bulletObjA, transform.position + Vector3.right * 0.1f, transform.rotation);
            //GameObject bulletL = Instantiate(bulletObjA, transform.position + Vector3.left * 0.1f, transform.rotation);
            GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
            GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
            bulletR.transform.position = transform.position + Vector3.right * 0.1f;
            bulletL.transform.position = transform.position + Vector3.left * 0.1f;
            Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
            Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
            rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
            rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
            break;
        case 3:
            //GameObject bulletRR = Instantiate(bulletObjA, transform.position + Vector3.right * 0.3f, transform.rotation);
            //GameObject bulletCC = Instantiate(bulletObjB, transform.position, transform.rotation);
            //GameObject bulletLL = Instantiate(bulletObjA, transform.position + Vector3.left * 0.3f, transform.rotation);
            GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
            GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
            GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
            bulletRR.transform.position = transform.position + Vector3.right * 0.3f;
            bulletCC.transform.position = transform.position;
            bulletLL.transform.position = transform.position + Vector3.left * 0.3f;
            Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
            Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
            Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
            rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
            rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
            rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
            break;

    }
    

    curShotDelay = 0;
}

 

Enemy 스크립트는 프리펩 내에 있는 스크립트이기 때문에, 바로 ObjectManager를 끌고올 수 없다. 그래서 생성 후, GameManager를 통해서 가져와야한다. Enemy는 총알을 생성하여 발사시키고, 죽었을 때 아이템을 생성한다.

이 부분을 구현하는 instantiate()를 ObjectManager의 MakeObj()로 대체한다.


프리펩 내 스크립트에서 Scene의 스크립트 갖고오기

프리펩은 보통 Scene에 생성해서 활용하고자하는 오브젝트들이다. 프리펩 컴포넌트에는 Scene의 오브젝트들을 가져다 참조시키거나, 오브젝트 내의 스크립트를 가져다 참조할 수 없기 때문에 프리펩은 프리펩(또는 그 내부 스크립트)끼리만 참조가 가능하다.

따라서 프리펩에서 Scene의 오브젝트(또는 내부의 스크립트)를 참조하려면 '프리펩이 Scene에 생성이 된 시점 이후'에 참조하여 활용할 수 있다. 이를 위해서는 GameManager의 간접적인 도움(참조 대입?)이 필요하다.

//GameManager
Player playerLogic;

private void Awake()
{
    playerLogic = player.GetComponent<Player>();
    enemyObjs = new string[] { "EnemyL", "EnemyM", "EnemyS"};
}

void SpawnEnemy()
{
    int ranEnemy = Random.Range(0, 3);
    int ranPoint = Random.Range(0, 9);

    GameObject enemy = objectManager.MakeObj(enemyObjs[ranEnemy]);
    enemy.transform.position = spawnPoints[ranPoint].position;

    Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
    Enemy enemyLogic = enemy.GetComponent<Enemy>();
    enemyLogic.player = player;
    ...
    
//Enemy
void Fire()
{
    if (curShotDelay < maxShotDelay)
        return;

    if (enemyName == "L")
    {
        GameObject bulletL = Instantiate(bulletObjB, transform.position + Vector3.left * 0.25f, transform.rotation);
        GameObject bulletR = Instantiate(bulletObjB, transform.position + Vector3.right * 0.25f, transform.rotation);
        Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();

        Vector3 dirVecL = player.transform.position - transform.position + Vector3.left * 0.25f;
        Vector3 dirVecR = player.transform.position - transform.position + Vector3.right * 0.25f;
        rigidL.AddForce(dirVecL.normalized * 3.5f, ForceMode2D.Impulse);
        rigidR.AddForce(dirVecR.normalized * 3.5f, ForceMode2D.Impulse);
    }

    else if (enemyName == "S")
    {
        GameObject bullet = Instantiate(bulletObjA, transform.position, transform.rotation);
        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();

        Vector3 dirVec = player.transform.position - transform.position;
        rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);
    }

    curShotDelay = 0;
}

이번 프로젝트에서는 Enemy 스크립트에서 (1)Player 오브젝트를 참조하여 총알이 플레이어 방향으로 발사되도록 하거나, (2)오브젝트 풀링을 위해  ObjectManager 스크립트를 참조하고 총알들이 생성되는데 활용했다.

추가적으로 총알이 총알 경계지점을 만나 파괴되는대신 비활성화되기 위해 (3)Bullet 스크립트에서 ObjectManager 스크립트를 참조하고 총알이 경계를 만났을 때 비활성화되는데 활용되었다.


#3. 로직 정리

실행해봤더니 몇가지 문제점이 있다.

1. 왼쪽 오른쪽에서 생성되는 Enemy는 각도가 꺾여서 날라가는데, 그 다음 위쪽에서 생성되는 Enemy는 각도가 꺾여서 생성된다.

//GameManager
void SpawnEnemy()
{
    int ranEnemy = Random.Range(0, 3);
    int ranPoint = Random.Range(0, 9);

    GameObject enemy = objectManager.MakeObj(enemyObjs[ranEnemy]);
    enemy.transform.position = spawnPoints[ranPoint].position;

    Rigidbody2D rigid = enemy.GetComponent<Rigidbody2D>();
    Enemy enemyLogic = enemy.GetComponent<Enemy>();
    enemyLogic.player = player;
    enemyLogic.objectManager = objectManager;

    if (ranPoint == 5 || ranPoint == 6) //왼쪽
    {
        enemy.transform.Rotate(Vector3.forward * 90);
        rigid.velocity = new Vector2(enemyLogic.speed * 1, -1);
    }
    else if (ranPoint == 7 || ranPoint == 8) //오른쪽
    {
        enemy.transform.Rotate(Vector3.back * 90);
        rigid.velocity = new Vector2(enemyLogic.speed * (-1), -1);
    }
    else
    {
        rigid.velocity = new Vector2(0, enemyLogic.speed * -1);
    }
}

: 적 비행기가 왼쪽 혹은 오른쪽에서 생성되어 각도가 90도 회전된 상태로 생성되고 아직 죽지 않았을 때 위쪽에서 적 비행기가 생성됐다면, 직전의 회전값에 변화가 없어서 그대로 꺾인 상태로 내려오게 된다. (죽었다면 회전 값은 0으로 초기화되니까) 따라서 총알에 맞거나/플레이어에 맞거나, 경계지점에 닿아서 비활성화됐을 때 무조건 회전 값을 0으로 초기화해줘야한다.

transform.rotation = Quaternion.identity;

Quaternion.identity : 기본 회전값 = 0으로 설정 (rotation 값)

 

2. 아이템이 내려오지 않는다.

public void OnHit(int damage)
{
    if (health <= 0)
        return;

    health -= damage;
    spriteRenderer.sprite = sprites[1];
    Invoke("ReturnSprite", 0.1f);

    if (health <= 0)
    {
        Player playerLogic = player.GetComponent<Player>();
        playerLogic.score += enemyScore;

        //Random Ratio Item Drop
        int ran = Random.Range(0, 10);
        if(ran < 5) //Nothing 0.5
        {
            Debug.Log("Not Item");
        }
        else if (ran < 8) //Coin 0.3
        {
            GameObject itemCoin = objectManager.MakeObj("ItemCoin");
            itemCoin.transform.position = transform.position;
            Rigidbody2D rigid = GetComponent<Rigidbody2D>();
            rigid.velocity = Vector2.down * 0.5f;
        }

        else if (ran < 9) //Power 0.1
        {
            GameObject itemPower = objectManager.MakeObj("ItemPower");
            itemPower.transform.position = transform.position;
            Rigidbody2D rigid = GetComponent<Rigidbody2D>();
            rigid.velocity = Vector2.down * 0.5f;
        }

        else if (ran < 10) //Boom 0.1
        {
            GameObject itemBoom = objectManager.MakeObj("ItemBoom");
            itemBoom.transform.position = transform.position;
            Rigidbody2D rigid = GetComponent<Rigidbody2D>();
            rigid.velocity = Vector2.down * 0.5f;
        }

        gameObject.SetActive(false);
        transform.rotation = Quaternion.identity;
    }
    
}

아이템 속도는 item 스크립트에서 관리하지 않고, Enemy 스크립트에서 아이템 생성과 동시에 관리하도록 한다.

 

3. 일부 적 비행기가 피격되지 않는다.

실행해보니 가끔씩 일부 적 비행기가 피격되지 않는 문제가 있다.

: Destroy()를 하면 스크립트 상 갖고있던 값들은 (파괴 후 새로 생성되면서) 초기화되어버리지만, .SetActive(false)를 하면 이전에 설정된 값들은 그대로 유지된다. 따라서 Health처럼 소모되는 변수는 활성화될 때 다시 초기화가 필요하다.

*오브젝트 풀링으로 프리펩들을 관리할 때 반드시 고려해야한다.

 

오브젝트 풀링은 프리펩을 생성해두었다가 활성화, 비활성화를 반복하며 계속 재활용하기 때문에 이러한 문제점이 있다. 

그렇다면 매번 이러한 변수들을 관리해주어야할까 생명 주기에서 활성화할 때 실행되는 OnEnable()을 활용하면 쉽게 해결할 수 있다. 참고

OnEnable() : 컴포넌트가 활성화될 때 호출되는 생명주기 함수

//Enemy
private void OnEnable()
{
    switch (enemyName)
    {
        case "L":
            health = 40;
            break;
        case "M":
            health = 10;
            break;
        case "S":
            health = 5;
            break;
    }
}

OnEnable()을 하게되면 Health를 다시 원래대로 초기화시켜주도록 한다. 그래서 활성화될 때마다 피를 최대로 채우고 다시 시작하게 되는 것이다. 

//Item
public class Item : MonoBehaviour
{
    public string type;
    Rigidbody2D rigid;

    private void Awake()
    {
        rigid = GetComponent<Rigidbody2D>();
    }

    private void OnEnable()
    {
        rigid.velocity = Vector3.down * 1.5f;
    }
}

그렇다면 아이템이 아래로 내려오는 것도 아이템이 활성화될 때마다 아래로 내려가도록 OnEnable()로 처리해줄 수 있을 것이다. 따라서 앞에서 Enemy에서 아이템 생성과 동시에 아래로 내려가도록 처리한 로직은 지워준다.


#4. Find 교체

//Player
void Boom()
{
    if (!Input.GetButton("Fire2"))
        return;

    if (isBoomTime)
        return;

    if (curBoom == 0)
        return;

    curBoom--;
    isBoomTime = true;
    gameManager.UpdateBoomUI(curBoom);

    //Effect visible
    boomEffect.SetActive(true);
    Invoke("OffBoomEffect", 3.0f);
    //Remove Enemy
    GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
    for (int index = 0; index < enemies.Length; index++)
    {
        Enemy enemyLogic = enemies[index].GetComponent<Enemy>();
        enemyLogic.OnHit(1000);
    }

    //Remove Enemy Bullet
    GameObject[] bullets = GameObject.FindGameObjectsWithTag("EnemyBullet");
    for (int index = 0; index < bullets.Length; index++)
    {
        bullets[index].SetActive(false);
    }

}

이전에 FindGameObjectsWithTag()를 사용했었다.

그런데 오브젝트 풀링으로 프리펩 오브젝트를 엄청 많이 만들어냈는데, 필살기를 사용하면서 "Enemy" 태그가 달린 것만 모두 찾아내려면, "EnemyBullet" 태그가 달린 것만 모두 찾아내려면 힘들 것이다.

: 오브젝트를 직접 찾는 Find 계열 함수는 성능 부하를 유발시킨다.

 

오브젝트 풀링을 위해 이제는 프리펩들을 ObjectManager에서 관리하고 있기 때문에 더이상 FindGameObjectsWithTag()는 사용할 필요가 없다.

//ObjectManager
public GameObject[] GetPool(string type)
{
    switch (type)
    {
        case "EnemyL":
            targetPool = enemyL;
            break;
        case "EnemyM":
            targetPool = enemyM;
            break;
        case "EnemyS":
            targetPool = enemyS;
            break;

        case "ItemCoin":
            targetPool = itemCoin;
            break;
        case "ItemPower":
            targetPool = itemPower;
            break;
        case "ItemBoom":
            targetPool = itemBoom;
            break;

        case "BulletPlayerA":
            targetPool = bulletPlayerA;
            break;
        case "BulletPlayerB":
            targetPool = bulletPlayerB;
            break;
        case "BulletEnemyA":
            targetPool = bulletEnemyA;
            break;
        case "BulletEnemyB":
            targetPool = bulletEnemyB;
            break;
    }

    return targetPool;
}

//Player
//Remove Enemy
//GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
GameObject[] enemiesL = objectManager.GetPool("EnemyL");
GameObject[] enemiesM = objectManager.GetPool("EnemyM");
GameObject[] enemiesS = objectManager.GetPool("EnemyS");

for (int index = 0; index < enemiesL.Length; index++)
{
    Enemy enemyLogic = enemiesL[index].GetComponent<Enemy>();
    enemyLogic.OnHit(1000);
}

for (int index = 0; index < enemiesM.Length; index++)
{
    Enemy enemyLogic = enemiesM[index].GetComponent<Enemy>();
    enemyLogic.OnHit(1000);
}

for (int index = 0; index < enemiesS.Length; index++)
{
    Enemy enemyLogic = enemiesS[index].GetComponent<Enemy>();
    enemyLogic.OnHit(1000);
}

//Remove Enemy Bullet
//GameObject[] bullets = GameObject.FindGameObjectsWithTag("EnemyBullet");
GameObject[] bulletsA = objectManager.GetPool("BulletEnemyA");
GameObject[] bulletsB = objectManager.GetPool("BulletEnemyB");

for (int index = 0; index < bulletsA.Length; index++)
{
    bulletsA[index].SetActive(false);
}

for (int index = 0; index < bulletsB.Length; index++)
{
    bulletsB[index].SetActive(false);
}

ObjectManager에서 Get 계열의 함수를 사용한다.

GetPool(): 지정한 오브젝트 풀을 가져오는 함수 : GameObject[]를 반환한다. *배열

필살기 사용할 때 적 총알 제거도 마찬가지로 Find 함수를 대체해준다.

*아쉬운 점은 오브젝트 풀 각각마다 for문을 써줘야한다는 것이다.

 

앞에서 수백개의 Scene에 있는 오브젝트를 뒤져가면서 "Enemy" 태그를 가진, "EnemyBullet" 태그를 가진 오브젝트를 검색하는 것보다 우리가 지정한 풀 내에서만 검색하는 것이 훨씬 더 시스템의 부하를 줄여준다. 추가적으로 활성화 오브젝트를 따로 관리하는 배열도 있다면 더 좋을 것이다.

 

코드는 살짝 길어졌지만, 메모리 상으로는  훨씬 더 최적화가 되었다. 저번까지 했던 것과 게임 내 모습은 똑같은데, 메모리 상으로는 훨씬 더 적은 메모리를 소비하게 되었다. 이것이 오브젝트 풀링이다.

Comments