오늘은 전에 다형성을 활용해서 만든 아이템들을 맵에 스폰시켜보려고한다. 단순하게 Instantiate()를 사용해서 생성하고 Destroy()를 사용해서 파괴할 수도 있지만, 오브젝트 생성은 메모리를 새로 할당하고 리소스를 로드하는 초기하 과정이고, 오브젝트 파괴는 이후에 발생하는 가비지 컬렉팅으로 인해 프레임 드랍이 발생할 수 있다. 쉽게 말하면 둘다 상당히 비용을 크게 잡아먹는 무거운 작업이라는 뜻이다.
이러한 문제를 보완하기 위해 사용되는 기법이 바로 오브젝트 풀링(Object Pooling)
이다.
이제부터 오브젝트 풀링에 대해 알아보자.
간단하게 설명하면 자주 사용하는 오브젝트를 미리 생성해서 오브젝트 풀에 담아둔 다음, 필요할 때 꺼내 쓰고 필요 없을 때 다시 넣어두는 것이다.
이렇게 하면, 오브젝트 생성 및 파괴 횟수가 현저하게 줄어드므로 성능향상에 많은 도움이 된다. 따라서 자주 생성되었다가 파괴되는 오브젝트를 대상으로 사용할 수록 더 큰 이득을 볼 수 있는 기법이다.
오브젝트 풀링을 사용하면 기본적으로 오브젝트를 미리 생성해 놓기 때문에 계속해서 메모리를 사용하고 있게 된다. 따라서 오브젝트 풀의 크기를 작지도 크지도 않게 적당한 크기로 만드는 것이 오브젝트 풀링의 핵심이다.
먼저 Instantiate()를 이용해서 아이템을 랜덤한 위치에 1초마다 생성하는 스크립트를 만들어 준다. 스크립트를 토대로 오브젝트 풀링을 사용할 것이기 때문이다.
// ItemSpawnTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ItemSpawnTest : MonoBehaviour
{
private float itemTimer;
public GameObject healItem;
private float minX = -5f, maxX = 5f, minY = -4f, maxY = 4f;
void Update()
{
float randomX = Random.Range(minX, maxX);
float randomY = Random.Range(minY, maxY);
Vector2 randomPos = new Vector2(randomX, randomY);
itemTimer += Time.deltaTime;
if (itemTimer > 1f)
{
GameObject Spawnitem1 = Instantiate(healItem, randomPos, Quaternion.identity);
itemTimer = 0;
}
}
아이템들은 생성된지 5초가 지나면 Destroy() 메소드에 의해 5초 뒤에 파괴된다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Item : MonoBehaviour
{
public virtual void Use()
{
Debug.Log("사용");
}
public virtual void GetItem()
{
Use();
Destroy(gameObject);
}
public virtual void DisappearItem()
{
Destroy(gameObject, 5f);
}
}
이제 오브젝트 풀을 구현해보자.
// ObjectPool.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class ObjectPool : MonoBehaviour
{
public int defaultCapacity;
public int maxPoolSize;
public GameObject healItem;
public IObjectPool<GameObject> Pool { get; private set; }
public static ObjectPool instance
{
get
{
if (m_instance == null)
{
m_instance = FindObjectOfType<ObjectPool>();
}
return m_instance;
}
}
private static ObjectPool m_instance;
private void Awake()
{
{
if (instance != this)
{
Destroy(gameObject);
}
}
Init();
}
private void Init()
{
Pool = new ObjectPool<GameObject>(CreatePooledItem, OnTakeFromPool, OnReturnedToPool,
OnDestroyPoolObject, true, defaultCapacity, maxPoolSize);
for (int i = 0; i < defaultCapacity; i++)
{
HealItem healItem = CreatePooledItem().GetComponent<HealItem>();
healItem.Pool.Release(healItem.gameObject);
}
}
private GameObject CreatePooledItem()
{
GameObject poolGo = Instantiate(healItem);
poolGo.GetComponent<HealItem>().Pool = this.Pool;
return poolGo;
}
private void OnTakeFromPool(GameObject poolGo)
{
poolGo.SetActive(true);
}
private void OnReturnedToPool(GameObject poolGo)
{
poolGo.SetActive(false);
}
private void OnDestroyPoolObject(GameObject poolGo)
{
Destroy(poolGo);
}
}
일단 오브젝트 풀은 오브젝트를 생성하는 코드에서 접근해야하기 때문에 싱글톤 패턴으로 구현하는 것이 좋다.
이제 모든 메소드가 무슨 역할을 하는지 알아보자.
- Init() -> 아이템 오브젝트 미리 생성 해놓는 초기 작업
- CreatePooledItem() -> 오브젝트 생성
- OnTakeFromPool(GameObject poolGo) -> 오브젝트 활성화 (사용)
- OnReturnedToPool(GameObject poolGo) -> 오브젝트 비활성화 (반환)
- OnDestroyPoolObject(GameObject poolGo) -> 오브젝트 파괴 (삭제)
이제 오브젝트 풀을 토대로 기존에 아이템을 생성하고 파괴하는 스크립트를 수정해주자.
// ItemSpawnTest.cs
using UnityEngine;
public class ItemSpawnTest : MonoBehaviour
{
private float itemTimer;
private float minX = -5f, maxX = 5f, minY = -4f, maxY = 4f;
void Update()
{
float randomX = Random.Range(minX, maxX);
float randomY = Random.Range(minY, maxY);
Vector2 randomPos = new Vector2(randomX, randomY);
itemTimer += Time.deltaTime;
if (itemTimer > 1f)
{
// 원래는 Instantiate()를 통해 아이템을 생성 했었음
//GameObject Spawnitem1 = Instantiate(healItem, randomPos, Quaternion.identity);
GameObject spawnItem = ObjectPool.instance.Pool.Get();
spawnItem.transform.position = randomPos;
itemTimer = 0;
}
}
}
// Item.cs
using UnityEngine;
using UnityEngine.Pool;
public class Item : MonoBehaviour
{
public IObjectPool<GameObject> Pool { get; set; }
public virtual void Use()
{
Debug.Log("사용");
}
public virtual void GetItem()
{
Use();
// 원래는 Destroy()를 통해 아이템을 파괴 했었음
// Destroy(gameObject);
Pool.Release(this.gameObject);
}
public virtual void DisappearItem()
{
//Destroy(gameObject, 5f);
Pool.Release(this.gameObject);
}
}
// HealItem.cs
using UnityEngine;
public class HealItem : Item
{
public float disappearTimer;
private void Update()
{
disappearTimer += Time.deltaTime;
if(disappearTimer >= 5f)
{
disappearTimer = 0f;
DisappearItem();
}
}
public override void Use()
{
SoundManager.instance.Heal_Play();
PlayerHealth playerhealth = GameObject.Find("Player").GetComponent<PlayerHealth>();
if (playerhealth.Health < 5)
{
playerhealth.Health++;
}
}
void OnTriggerEnter2D(Collider2D other)
{
if (other.tag == "Player")
{
GetItem();
}
}
}
이제 프로젝트를 실행시키면 오브젝트가 미리 생성되고 생성되있는 오브젝트가 활성화, 비활성화 되는 모습을 볼 수 있다.