오브젝트 풀링이 최적화에 도움이 된다는 사실은 대부분 알고 있을 겁니다. 유명한 만큼 뛰어난 효과를 보여주기도 하지만 잘못 사용할 경우 게임을 느려지게 하는 원인이 되기도 합니다. 이번 시간은 오브젝트 풀링을 알아보고 올바른 사용방법을 알아보겠습니다.
[정의]
오브젝트를 생성하는 데에는 메모리 사용 등 리소스를 사용하는 부분이 많은데, 이러한 부분을 최소화 하여 생성을 최대한 적게 하고 재활용 하는 디자인 패턴입니다. 즉, 생성한 오브젝트를 파괴시키지 않고 초기 상태로 되돌려서 재사용 하는 것 입니다.
[총알 발사 구현]
글을 읽고 이해하는 것 보다 코드를 보고 이해하는 것이 쉬울 거라 생각합니다. 그렇기에 오브젝트 풀링을 많이 사용하는 곳 중 하나인 총알 발사를 구현하면서 설명 하겠습니다.
using UnityEngine;
[System.Serializable]
public class BulletData
{
public float damage;
public float speed;
public string additionalData;
public BulletData(float _damage, float _speed, string _additionalData)
{
damage = _damage;
speed = _speed;
additionalData = _additionalData;
}
}
총알의 데이터가 들어 있는 스크립트 입니다. 이 클래스가 필요한 이유는 게임 개발 시 데이터 단위로 개발 해야 코드의 수도 줄어들고 확장성과 유연성도 올라가기 때문입니다. 또한 총알의 데이터인 데미지, 속도 등이 이곳 저곳 퍼져 있다면 관리하기도 어렵습니다.
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public GameObject prefab;
public int poolSize = 10;
private List<GameObject> objects;
private void Awake()
{
objects = new List<GameObject>();
for (int i = 0; i < poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
objects.Add(obj);
}
}
public GameObject GetObject(Vector3 position, Quaternion rotation)
{
foreach (var obj in objects)
{
if (!obj.activeInHierarchy)
{
obj.transform.position = position;
obj.transform.rotation = rotation;
obj.SetActive(true);
return obj;
}
}
return null;
}
public void ReturnObjectToPool(GameObject obj)
{
obj.SetActive(false);
}
}
poolSize를 조절할 수 있는 ObjectPool클래스 입니다. 총알이나, 몬스터 등 카테고리 별로 오브젝트를 관리할 수 있습니다. prefab에 총알이나 몬스터를 적용하고 사용 하시면 됩니다.
using System.Collections.Generic;
using UnityEngine;
public class ObjectPoolManager : MonoBehaviour
{
// Singleton 인스턴스
public static ObjectPoolManager Instance;
// 오브젝트 풀 리스트
public List<ObjectPool> objectPools = new List<ObjectPool>();
private void Awake()
{
// Singleton 인스턴스 설정
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject); // 중복된 오브젝트 파괴
return;
}
}
public GameObject GetObjectFromPool(string poolName, Vector3 position, Quaternion rotation)
{
ObjectPool pool = objectPools.Find(p => p.name == poolName);
if (pool != null)
{
return pool.GetObject(position, rotation);
}
Debug.LogError("Object pool with name '" + poolName + "' not found.");
return null;
}
public void ReturnObjectToPool(string poolName, GameObject obj)
{
ObjectPool pool = objectPools.Find(p => p.name == poolName);
if (pool != null)
{
pool.ReturnObjectToPool(obj);
}
else
{
Debug.LogError("Object pool with name '" + poolName + "' not found.");
}
}
}
개별 ObjectPool을 관리하는 싱글톤 클래스 입니다. 이렇게 ObejctPool클래스와 ObjectPoolManager를 나누면 카테고리 별로 관리가 가능하고, 어디서든 쉽게 사용할 수 있는 장점이 있습니다.
using UnityEngine;
public class Bullet : MonoBehaviour
{
private BulletData bulletData;
private Rigidbody2D rb;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
}
public void Initialize(BulletData data)
{
bulletData = data;
rb.velocity = transform.up * bulletData.speed;
}
private void OnDisable()
{
rb.velocity = Vector2.zero;
}
private void Update()
{
// 여기에 총알이 화면 밖으로 나갈 때 비활성화되도록 처리할 수 있습니다.
// 예를 들어, 화면 밖으로 나가면 비활성화하고 오브젝트 풀로 돌려보냅니다.
}
}
총알 클래스 입니다. 필요한 정보를 BulletData에 담아 실제로 적용 시키는 부분 입니다. 이렇게 데이터를 받아와 적용 시키는 방식을 사용하면 확장성과 유연성이 올라갑니다.
using UnityEngine;
public class PlayerShooting : MonoBehaviour
{
public string bulletPoolName = "BulletPool"; // 오브젝트 풀 이름
public Transform firePoint;
public float fireRate = 0.5f;
public BulletData bulletData;
private float nextFireTime = 0f;
private void Update()
{
if (Input.GetButtonDown("Fire1") && Time.time >= nextFireTime)
{
Shoot();
nextFireTime = Time.time + fireRate;
}
}
private void Shoot()
{
GameObject bullet = ObjectPoolManager.Instance.GetObjectFromPool(
bulletPoolName, firePoint.position, firePoint.rotation
);
if (bullet != null)
{
Bullet bulletScript = bullet.GetComponent<Bullet>();
bulletScript.Initialize(bulletData);
bullet.SetActive(true);
}
}
}
플레이어가 ObjectPoolManager을 사용하여 총알을 생성하고 초기화하는 부분입니다. 초기에 미리 생성하고 비활성화 한 총알을 불러와서 초기화 한 뒤에 활성화 시켜주는 방법을 사용합니다.
핵심은 사용할 오브젝트를 미리 생성 후에 비활성화 시키고 사용할 경우 활성화 시켜서 사용하는 것 입니다. 그리고 다 사용 후 다시 비활성화 시키는 것입니다.
[장점]
- 성능이 향상됩니다. 오브젝트를 생성하고 삭제 하는 것은 많은 자원을 요구합니다.
- 게임 퀄리티가 올라갑니다. 한번에 많은 오브젝트를 생성시킬 경우 프레임 드랍 현상이 생길 수 있습니다. 즉, 게임이 끊길 수 있다는 것 입니다. 하지만 풀링을 사용할 경우 미리 생성시켜 두기 때문에 이런 현상이 사라지게 됩니다.
- 오브젝트의 수를 예측할 수 있으며, 조절하기 쉽습니다. 미리 생성시켜 놓기 때문에 이러한 장점을 취할 수 있는 것 입니다. 만약 게임의 프레임이 만족스럽지 못하다면 오브젝트 수를 손쉽게 조절하여 최적화도 가능합니다.
저 같은 경우 2번인 이유 때문에 사용하는 경우가 많은 것 같습니다. 프레임 드랍 현상이 생기는 순간 유저들은 빠르게 이탈하기 때문에, 테스트 플레이 시 이러한 현상이 생기게 되면 풀링을 사용하는 편입니다.
[단점]
- 쓸데없는 메모리 사용. 필요한 것 보다 더 많은 오브젝트를 생성시켰을 경우 메모리에 계속 남아있게 되고, 다른 게임 요소에 영향을 끼칠 우려가 있습니다.
- 초기화의 번거로움. 재사용을 한다는 특성이 있기 때문에 사용마다 처음 상태로 초기화를 해줘야 합니다. 많은 게임 요소에 오브젝트 풀링을 사용할 경우 초기화 코드를 짜는 것에 시간을 많이 할애하게 되고, 그 결과 생산성이 감소하게 됩니다.
- 오브젝트 수 관리가 어렵습니다. 수를 예측하여 사용할 만큼의 오브젝트를 생성시켜야 하는데, 그 수를 정확히 예측하는 것의 어려움이 있습니다. 또한 예측하였다 하더라도 게임의 밸런스나, 여러 요소로 인해 달라질 경우가 있기에 관리의 어려움이 있습니다.
저 같은 경우 다루기 어렵고 메모리 사용 문제로 인해서 사용하는 것을 신중히 생각하는 편입니다. 잘 못 사용했다 가는 메모리 문제나, 생산성 측면에서 큰 손해를 보게 되기 때문입니다.
이러한 손해를 안보기 위한 방법을 알려드리겠습니다.
[언제 사용해야 하는가?]
먼저 말씀드릴 것은 사용을 최소화하는 것이 좋습니다. 이유는 사용하면 할 수록 관리하기도 까다롭고, 초기화 문제로 인해서 생산성이 감소하기 때문입니다. 하지만 특정 상황의 경우 무조건 사용하는 편입니다.
오브젝트를 한번에 많이 사용할 경우 사용합니다. 예를 들어서 한번에 많은 총알이 발사 되는 총을 구현한다 거나, 탄막 게임의 보스를 구현 한다 거나 할 때 사용합니다.
다른 부분들을 최적화 했는데도, 최적화가 더 필요한 경우 합니다. 풀링은 생산성과 직결되는 디자인 패턴이기 때문에 최적화의 막바지에 사용 해야 합니다. 즉, 사용을 최소화 하기 위해서 가장 마지막에 사용하는 수단이 되는 것 입니다. 예를 들어서 탄막 게임을 만든다고 생각했을 때 탄환에 오브젝트 풀링을 사용했는데도 불구하고 프레임 드랍 현상이나 렉이 걸린다면, 그 외 몬스터나, 다른 오브젝트를 하나씩 풀링 사용으로 교체하는 것 입니다.
![[Unity] 오브젝트 풀링 단점, 총알 발사 구현. 2 오브젝트풀링 언제 사용해야 하는지 도식표](https://programmingdev.com/wp-content/uploads/2023/09/오브젝트풀링_언제_사용해야_하는지_도식표-optimized.jpg)
설명을 간단하게 정리한 도식 표 입니다. 사용하실 때 참고하시면 될 것 같습니다.
[사용하면 왜 생산성이 감소할까?]
단점을 설명할 때 간략히 초기화 상태를 지정해 줘야 하기 때문에 생산성이 감소한다고 말씀드렸습니다.
이유를 좀더 자세하게 설명해 보자면, 만약 몬스터에 오브젝트 풀링을 사용한다고 했을 때 몬스터를 죽인 후 다시 새롭게 사용한 다는 것을 감안하여 초기화 코드를 상세히 작성해 줘야 합니다. 체력을 리셋 해줘야 하고, 만약 몬스터가 스킬을 사용한다면 스킬 쿨타임도 리셋을 해줘야 합니다. 게임의 콘텐츠가 확장 된다면 더욱 더 많은 초기화를 해줘야 하게 됩니다.
또한 하나라도 초기화를 놓쳤을 경우 버그의 생성과 직결되는 문제도 발생하게 됩니다. 이러한 문제들로 자연스럽게 생산성이 감소하게 되는 것 입니다.
[마무리]
오브젝트 풀링은 사용만 잘한다면 최적화의 도움이 많이 되는 패턴입니다. 하지만 사용 상황을 구분하지 못하고 오 남용하게 된다면 많은 수의 버그와 맞닥뜨리게 될 수도 있고 최악의 경우 생산성이 감소하여 프로젝트 마감기한을 못 지키는 경우도 생길 가능성이 있으니 신중하게 사용하시기 바랍니다.