장점: 메모리 단편화 방지, 성능 향상(매번 생성, 삭제하는 비용 절감)
단점: 초기 메모리 점유, 풀 크기 조절 필요 (너무 크면 메모리낭비, 적으면 성능저하)
외부단편화 : 남아있는 메모리의 크기가 실행하려는 프로세스보다 크지만, 연속적이지 않은 공간에 존재
내부단편화 : 100MB만큼 저장하는 메모리공간에 80MB 프로세스 할당되면 20MB는 할당불가능상태가 된다
페이징,세그멘테이션~~ 개념도 이어짐
코드가 상당히 어려운데, 꾸준히 학습이 필요하고, 결국 중요한건 코드가 아니라 오브젝트풀 개념을 알고 내가 사용할 프로젝트에 적용할 수 있어야 함
오브젝트 풀 필수 항목
1. 미리 정해진 갯수만큼 만들고 저장
2. 필요할때 꺼내쓰고 다 썼으면 SetActive(false); 처리. Destroy 하지 않는다.
3. (선택) 꺼낼때 오브젝트 풀 공간을 모두 쓰고있다면 새로운 오브젝트 할당해서 풀 크기를 늘릴 수 있다.
using System.Collections.Generic;
using UnityEngine;
public class PolingScript : MonoBehaviour
{
//폴링을 위해서는 폴링을 만들 배열(리스트, 스택 큐 아무거나 가능) 필요
[SerializeField] GameObject prefab;
[SerializeField] int polingCount = 10;
List<GameObject> polingObjcets;
private void Awake()
{
//미리 polingCount 갯수만큼 SetActive false 상태로 만듬
polingObjcets = new List<GameObject>();
for(int i=0; i<polingCount; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
obj.name = "bullet_" + i;
polingObjcets.Add(obj);
}
}
public GameObject GetPolingObject()
{
//가져와야할때 해당 함수 호출해서 1개씩 꺼낸다
//개선점: 고정 위치가 아닌 Player 위치에서, 바라보는 방향으로 총알을 이동시켜야함.생성되면
for(int i=0; i< polingCount; i++)
{
if (polingObjcets[i].activeInHierarchy == false)
{
polingObjcets[i].SetActive(true);
return polingObjcets[i];
}
}
GameObject obj = Instantiate(prefab);
obj.SetActive(true);
obj.name = "bullet_" + polingCount;
polingObjcets.Add(obj);
polingCount++;
return obj;
}
public void ReturnObject(GameObject obj)
{
//오브젝트 다시 비활성화
obj.SetActive(false);
}
}
리스트에서 매번 검색해서 .isActiveHierachy해야되는데 비활성화체크가 비효율적임
큐로 변경하는데 대신 자기자신이 할당되었던 위치를 찾아서 다시 반환해주는 과정 필요
public class FireLogic : MonoBehaviour
{
[Header("발사관련설정")]
[SerializeField] GameObject bulletPrefab;
[SerializeField] int initialPoolSize = 10;
[SerializeField] float bulletSpeed = 20f;
//private List<GameObject> bulletPool; //리스트에서 매번 검색해서 .isActiveHierachy해야되는데 비활성화체크가 비효율적임 -> 큐로 변경
private Queue<GameObject> bulletPool = new Queue<GameObject>(); //총알을 쏠때마다 Dequeue, 끝나면 다시 Enqueue , 대신 누가쏜건지 몰라서 알려줘야함
private InputAction fireAction;
//총알 새로 만들고 풀에 담음
GameObject CreateNewBullet()
{
GameObject newBullet = Instantiate(bulletPrefab);
newBullet.SetActive(false);
bulletPool.Enqueue(newBullet);
return newBullet;
}
void OnShoot(InputAction.CallbackContext ctx)
{
FireBullet();
}
private void Awake()
{
//오브젝트풀 선언 및 초기 크기 지정
for(int i=0; i< initialPoolSize; i++)
{
CreateNewBullet();
}
fireAction = InputSystem.actions["Attack"];
}
//유니티6라서 Input을 InputAction으로 해야해서 performed 코드작성
private void OnEnable()
{
fireAction.performed += OnShoot;
}
private void OnDisable()
{
fireAction.performed -= OnShoot;
}
//총알 발사
void FireBullet()
{
//풀에서 총알 하나 가져온다
GameObject bullet = GetInactiveBullet();
////풀에 남은공간이 없다면 하나 만들어줌
if (bullet == null)
{
bullet = CreateNewBullet();
}
//총알 위치 및 Active상태 설정
bullet.transform.position = transform.position + transform.forward * 2; //발사자 살짝앞에
bullet.transform.rotation = transform.rotation; //발사자 방향
bullet.SetActive(true);
//의존성주입 (플레이어가 총알쏜건지 적이 총알쏜건지 기억하기위해)
bullet.GetComponent<BulletCtrl>().SetOwner(this);
//총알 발사 진행
bullet.GetComponent<Rigidbody>().linearVelocity = transform.forward * bulletSpeed;
}
GameObject GetInactiveBullet()
{
if(bulletPool.Count > 0) //사용가능 총알이 있다면
{
return bulletPool.Dequeue();
}
return null;
}
public void ReturnBullet(GameObject bullet)
{
bullet.SetActive(false);
bulletPool.Enqueue(bullet);
}
}
SetOwner 이용public class BulletCtrl : MonoBehaviour
{
public float lifeTime = 3f;
Rigidbody rb;
FireLogic fireLogic;
public void SetOwner(FireLogic fire)
{
fireLogic = fire;
}
private void Awake()
{
rb = GetComponent<Rigidbody>();
}
private void OnEnable()
{
Invoke(nameof(DisableBullet),lifeTime);
}
private void OnDisable()
{
rb.linearVelocity = Vector3.zero; //이전 속도가 남아있을수있어서
rb.angularVelocity = Vector3.zero;
CancelInvoke(); //모든 인보크 없애버림
}
private void OnCollisionEnter(Collision collision)
{
DisableBullet();
}
void DisableBullet()
{
fireLogic.ReturnBullet(this.gameObject);
}
}
오브젝트풀+제네릭 시연
인터페이스를 사용하여 구현하는 방법과, 사용하지 않고 하는 방법을 저장해두었다.
인터페이스 안쓰는 방법으로 프로젝트때는 썼었음
마찬가지로 총알에서 풀을 반환할때 ReturnObject()을 호출해주면 된다
public class ObjPool<T> where T : MonoBehaviour
{
//몬스터, 적, 총알, 파티클 등
Queue<T> pool;
private T prefab;
Transform poolParent; //하위객체에 넣어서 관리할때 사용
int aliveCount; //풀에 활성화되어있는 객체 숫자
public ObjPool(T prefab, int initSize, Transform parentPos = null)
{
this.prefab = prefab;
pool = new Queue<T>();
this.poolParent = parentPos;
aliveCount = 0;
for (int i = 0; i < initSize; i++)
{
//ObjPool은 MonoBehavior를 상속받지 않기때문에 Object.을 붙여야함, Where은 제약조건이지 상속을받지는않았다
T instance;
if (poolParent == null)
{
instance = Object.Instantiate(prefab, poolParent);
}
else
{
instance = Object.Instantiate(prefab, poolParent);
}
instance.gameObject.SetActive(false);
pool.Enqueue(instance);
}
}
public T GetObject()
{
aliveCount++;
if (pool.Count > 0)
{
T obj = pool.Dequeue();
obj.gameObject.SetActive(true);
return obj;
}
//풀이 모두 사용중이라면 풀을 늘림
T newObj;
if (poolParent == null)
{
newObj = Object.Instantiate(prefab);
}
else
{
newObj = Object.Instantiate(prefab, poolParent);
}
return newObj;
}
public void ReturnObject(T obj)
{
aliveCount--;
pool.Enqueue(obj);
obj.gameObject.SetActive(false);
}
public int AlivePoolCount()
{
return aliveCount;
}
}
사용예제
public class ExpPointSpawner : Singleton<ExpPointSpawner>
{
//오브젝트풀 프리팹,사이즈 , 선언
[SerializeField] ExpPointScript _expPointPrefab;
[SerializeField] int poolSize = 10;
ObjPool<ExpPointScript> _expPointPool;
public event Action<int> _getExpPoint;
protected override void Awake()
{
isDestroyOnLoad = false;
base.Awake();
_expPointPool = new ObjPool<ExpPointScript>(_expPointPrefab, poolSize, gameObject.transform);
}
//생성 코드
public ExpPointScript GetPrefabByPool(Transform trf)
{
ExpPointScript expBuf = _expPointPool.GetObject();
expBuf.transform.position = trf.position;
expBuf._destroyExpPoint += ReturnExpPointToPool;
return expBuf;
}
//오브젝트 반환 (반환이벤트없으면 public으로 써도됨)
private void ReturnPrefabToPool(ExpPointScript expPoint)
{
expPoint._destroyExpPoint -= ReturnExpPointToPool;
_expPointPool.ReturnObject(expPoint);
//경험치를 주웠을때
_getExpPoint?.Invoke(10);
}
}
//오브젝트풀로 만들 경험치구슬 클래스
public class ExpPointScript : MonoBehaviour
{
//오브젝트풀에 반환하기위한 이벤트
public event Action<ExpPointScript> _destroyExpPoint;
private void OnTriggerEnter2D(Collider2D collision)
{
Debug.Log("발동");
if (collision.CompareTag("Player"))
{
Debug.Log("이벤트까지발동");
_destroyExpPoint?.Invoke(this);
}
}
}
사용은 이런식으로 받아오면 된다
Monster monsterBuf = ExpPointSpawner.Instance.GetMonsterByPool(transform);

제네릭오브젝트풀 코드는 안건드리고,
Prefab을 MonsterParent-Monster구조로 저장되도록 하나만들고, ObjectPool 소스가 있는 'MonsterSpawner'클래스에 MonsterParent가 가지는 스크립트를 하나만들어서 오브젝트풀을 만들고, Get과 Return을 반환할때 Monster를 반환하도록 설정했음
MonsterPreab의 부모에 해당 스크립트도 넣고 끝(MonsterRoot, MonsterSpawner.cs파일 참조)
public class MonsterRoot : MonoBehaviour
{
//몬스터의 부모에 스크립트를 넣어서 스케일을 관리하기 위해서 추가
private Monster _monster;
public Monster Monster => _monster;
private void Awake()
{
_monster = GetComponentInChildren<Monster>();
}
public void SetScale(float value)
{
//스케일 조정 (값에 따라)
transform.localScale = new Vector3(value, value, value);
}
}
public class MonsterSpawner : Singleton<MonsterSpawner>
{
//오브젝트풀 프리팹,사이즈 , 선언
[SerializeField] MonsterRoot _monsterPrefab;
[SerializeField] int poolSize = 10;
ObjPool<MonsterRoot> _monsterRootPool; //기존 오브젝트풀과 다르게 부모를 하나 추가하기위해 MonsterRoot를 넣어서 사용
protected override void Awake()
{
isDestroyOnLoad = false;
base.Awake();
_monsterRootPool = new ObjPool<MonsterRoot>(_monsterPrefab, poolSize, gameObject.transform);
}
//생성 코드
public Monster GetMonsterByPool(Transform trf)
{
MonsterRoot monParent = _monsterRootPool.GetObject();
monParent.transform.position = trf.position;
Monster monsterBuf = monParent.Monster;
//monsterBuf.transform.position = trf.position;
monsterBuf.OnMonsterDead += ReturnMonsterToPool;
return monsterBuf;
}
//오브젝트 반환
private void ReturnMonsterToPool(Monster monster)
{
monster.OnMonsterDead -= ReturnMonsterToPool;
//부모를 가져온다
MonsterRoot monParent = monster.GetComponentInParent<MonsterRoot>();
_monsterRootPool.ReturnObject(monParent);
}
}
2-1.인터페이스
public interface IPoolable
{
public void OnCreate(); //생성 시
public void OnSpawn(); //풀에서 꺼내질때
public void OnDeSapwn(); //풀로 돌아갈때
}
2-2. 오브젝트풀 (인터페이스 사용)
public class ObjPool<T> where T : Component, IPoolable
{
private T _prefab;
private Stack<T> _pool;
public bool IsExpandable { get; set; } //모자라면 확장 가능하게 할건지
public bool IsEmpty => _pool.Count == 0;
public ObjPool(T prefab, int capacity = 10, bool isExpandable = false)
=> Init(prefab, capacity, isExpandable);
//초기 생성자
private void Init(T prefab,int capacity, bool isExpandable)
{
_prefab = prefab;
IsExpandable = isExpandable;
_pool = new Stack<T>(capacity);
for(int i=0; i< capacity; i++)
{
T pooledObject = CreatePooledObject();
Despawn(pooledObject);
}
}
//생성 처리 함수 (하나씩 반환해준다)
public T Spawn()
{
T pooledObject;
if (IsExpandable && IsEmpty) pooledObject = CreatePooledObject();
else pooledObject = _pool.Pop();
pooledObject.OnSpawn();
return pooledObject;
}
//다쓰고 생성 해제 함수 (Pool로 반환)
public void Despawn(T poolableObject)
{
poolableObject.OnDeSapwn();
_pool.Push(poolableObject);
}
//객체 하나 생성할때 사용
private T CreatePooledObject()
{
T pooledObject = MonoBehaviour.Instantiate(_prefab);
pooledObject.OnCreate();
return pooledObject;
}
}
2-3 총알소스 (인터페이스 사용)
public class BulletController : MonoBehaviour, IPoolable
{
Rigidbody _rigidbody;
private float _timeCount;
public event Action<BulletController> OnLifetimeEnd;
//오브젝트풀 사용법 1. 인터페이스를 적용후 구현
public void OnCreate() //생성
{
Init();
}
public void OnSpawn() // 풀에서 꺼냈을때
{
gameObject.SetActive(true);
}
public void OnDeSapwn() //풀로 돌아갈때
{
gameObject.SetActive(false);
}
public void OnDispose() //파괴될때
{
Destroy(gameObject);
}
private void Init()
{
_rigidbody = GetComponent<Rigidbody>();
}
//총알 위치 지정
public void SetUp(Vector3 position, Quaternion rotation, Vector3 force, float lifetime)
{
transform.SetPositionAndRotation(position, rotation);
_rigidbody.linearVelocity = Vector3.zero;
_rigidbody.AddForce(force, ForceMode.Impulse);
_timeCount = lifetime;
}
private void Update() => HandleTimeout();
//총알 수명체크 (코루틴써도됨)
private void HandleTimeout()
{
_timeCount -= Time.deltaTime;
if(_timeCount <= 0)
{
OnLifetimeEnd?.Invoke(this);
}
}
}
2-4.플레이어 무기(총) 코드
public class GunController : MonoBehaviour
{
[SerializeField] private Transform _muzzleTransform;
[SerializeField] private float _force;
[SerializeField] private float _timeToBulletLive;
[SerializeField] private BulletController _bulletPrefab;
private ObjPool<BulletController> _bulletPool;
private void Awake() => Init();
private void Update() => HandleGun();
private void Init()
{
_bulletPool = new ObjPool<BulletController>(_bulletPrefab, 10, true);
}
private void HandleGun()
{
if (Input.GetKeyDown(KeyCode.Alpha1)) Fire();
}
private void Fire()
{
BulletController bullet = _bulletPool.Spawn();
bullet.OnLifetimeEnd += ReturnToPool;
Vector3 force = _muzzleTransform.forward * _force;
bullet.SetUp(_muzzleTransform.position, _muzzleTransform.rotation, force, _timeToBulletLive);
}
private void ReturnToPool(BulletController bullet)
{
bullet.OnLifetimeEnd -= ReturnToPool;
_bulletPool.Despawn(bullet);
}
}
public class RealFinalPool : MonoBehaviour
{
//유니티 내장풀 사용해봄
ObjectPool<GameObject> myPool; //유니티 내장 풀을 만듬
public GameObject toInstantiate;
private void Start()
{
//생성할프리팹
//Get수행메서드
//Return수행메서드
//파괴시메서드
/*
myPool = new ObjectPool<GameObject>
(
CreateNewObject,
ActivateObject,
DeactivateObject,
DestroyObject
);
*/
//한줄이니까 람다식으로 쓸수있다
//아래 toInstantiate 델리게이트 캡쳐 조심하기
myPool = new ObjectPool<GameObject>
(
()=> Instantiate(toInstantiate),
obj => obj.SetActive(true),
obj => obj.SetActive(false),
obj => Destroy(obj.gameObject)
);
}
//객체 생성 메서드
GameObject CreateNewObject()
{
return Instantiate(toInstantiate);
}
//객체 활성화 메서드 및 풀에서 꺼낼때 수행할 메서드
void ActivateObject(GameObject obj)
{
obj.SetActive(true);
//+ 위치를 발사자 위치로 변경
}
void DeactivateObject(GameObject obj)
{
obj.SetActive(false);
//+ 총알의경우 리니어벨로시티 0으로 만드는 코드 작성
}
void DestroyObject(GameObject obj)
{
Destroy(obj.gameObject);
}
//플레이어는 아래처럼 플레이어 로직만 들고있게 하고싶음
//아래코드는 그냥 예시를 위해 막 쓴 코드임
private void Update()
{
if(Input.GetKeyUp(KeyCode.Escape))
{
var obj = myPool.Get();
obj.GetComponent<Rigidbody>().AddForce(transform.position);
//myPool.Release(반환시키고자하는 총알);
}
}
}
이해 단계 차근차근히
1.오브젝트풀 개념
2.배열/List로 SetActive true, false (+Queue로 구현)
3.심화: 제네릭 버전으로 만든다
4.모든걸 다 액션 기반으로 등록할 수 있는 시스템구축 (수업때는안함)
유니티 내장 오브젝트풀이랑 제네릭화 커스텀 둘중 무엇을 쓸지 고민
맵 정보를 오브젝트풀로 저장했다가 플레이어가 더이상 안가는곳은 없애고, 다음에갈곳은 생성해놔서 응용가능