오브젝트 풀
최적화 기법을 이야기할 때, 가장 먼저 떠오르는 것 중 하나가 바로 **오브젝트 풀(Object Pool)**이다.
유니티에서도 기본 제공하는 오브젝트 풀이 있지만,
내가 원하는 기능들을 자유롭게 넣을 수 있도록 직접 제작해보고 싶었다.
마침 프로토타입을 진행하던 중, 시간이 조금 여유로워서
그 기회에 오브젝트 풀을 만들어 보기로 했다.
IPoolable
풀에서 관리할 오브젝트들에 연결할 인터페이스.
즉 오브젝트풀에 사용할 객체에 해당 인터페이스를 필수로 상속해야 사용이 가능하다.
using UnityEngine;
namespace dev.jun
{
public interface IPoolable
{
//오브젝트풀 오브젝트가 준비된 상태인지 확인!
bool isGetAble { get; }
// 풀에서 대여될 때 호출한다.
void OnSpawn();
// 풀로 반납될 때 호출한다.
void OnDespawn();
// 내부 상태를 초기화할 때 호출한다.
void ResetState();
}
}
하단은 상속 예시
public class DamageText : MonoBehaviour,dev.jun.IPoolable
오브젝트 풀
// PoolManager.cs
using DevJun.DebugHelper;
using System. Collections;
using System. Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PoolManager : Singleton_generic<PoolManager>
{
[Header("몬스터 풀")]
[SerializeField] private AiMonster monsterPrefab;
[SerializeField] private int monsterInitial = 16;
[SerializeField] private int monsterMax = 50;
[SerializeField] private bool monsterExpandable = true;
private MonsterPool _monsterPool;
private Transform _monsterRoot;
[Header("데미지 텍스트 풀")]
[SerializeField] private Transform DamageTextObj;
[SerializeField] private Transform DamageTextRoot;
[SerializeField] private int _textInitialPerId = 4;
[SerializeField] private int _textMaxPerId = 64;
[SerializeField] private bool _textExpandable = true;
[Header("데미지 텍스트 프리팹")]
[SerializeField] DamageText _txtPrefab;
private DamageTextPool _txtPool;
/// ============================================================
/// 공격 오브젝트도 개별적인 풀을 가지고 있으나 해시테이블을 통해 관리를 진행한다.
/// ============================================================
//DB에서 데이터를 가지고온후 공격 오브젝트 번호 세팅 기본공격은 -1부터 시작 스킬은 1번부터 시작
[Header("공격오브젝트 풀")]
[SerializeField] private Transform skillRoot;
[SerializeField] private int skillInitialPerId = 3;
[SerializeField] private int skillMaxPerId = 5;
[SerializeField] private bool skillExpandable = true;
[Header("공격용 프리팹)")]
[SerializeField] private List<AtkEffectBase> skillPrefabs = new List<AtkEffectBase>();
private Dictionary<int, AtkObjPool> _skillCorePool;
protected override void Awake()
{
base.Awake();
if (_monsterRoot == null)
{
_monsterRoot = new GameObject("[POOL] Monsters").transform;
_monsterRoot.SetParent(transform, false);
}
if (skillRoot == null)
{
skillRoot = new GameObject("[POOL] Skills").transform;
skillRoot.SetParent(transform, false);
}
if (DamageTextObj == null)
{
DamageTextObj = new GameObject("[POOL] txt").transform;
DamageTextObj.SetParent(transform, false);
}
var pool = _monsterRoot.GetComponent<MonsterPool>();
if (pool == null)
{
pool = _monsterRoot.gameObject.AddComponent<MonsterPool>();
LogC.LogColor("에드 컴포넌트 완료", Color.red);
}
_monsterPool = pool;
// 컴포넌트 초기화(필드 세팅 or Init 호출)
_monsterPool.Init(monsterPrefab, monsterInitial, monsterMax, monsterExpandable, _monsterRoot);
var txt_pool = DamageTextObj.GetComponent<DamageTextPool>();
if (txt_pool == null)
{
txt_pool = DamageTextObj.gameObject.AddComponent<DamageTextPool>();
LogC.LogColor("데미지 텍스트 에드 컴포넌트 완료", Color.red);
}
_txtPool = txt_pool;
// 컴포넌트 초기화(필드 세팅 or Init 호출)
_txtPool.Init(_txtPrefab, _textInitialPerId, _textMaxPerId, _textExpandable, DamageTextRoot);
//리스트에 프리팹 세팅후 옵젝에 스킬 등록하기
_skillCorePool = new Dictionary<int, AtkObjPool>();
RegisterSkills();
}
private void RegisterSkills()
{
if (skillPrefabs == null)
{
return;
}
for (int i = 0; i < skillPrefabs.Count; i++)
{
skillPrefabs[i].SkillNumbering(i + 1);
AddPoolAtkObj(skillPrefabs[i]);
}
}
private void AddPoolAtkObj(AtkEffectBase atkEffectBase)
{
if (atkEffectBase == null)
{
return;
}
IAtkObjectBase baseSkill = atkEffectBase as IAtkObjectBase;
if (baseSkill == null)
{
Debug.LogWarning($"[PoolManager] {atkEffectBase.name}는 ISkillObjectBase를 구현하지 않습니다.");
return;
}
int key = baseSkill.SkillId;
if (!_skillCorePool.ContainsKey(key))
{
//포함되어 있지 않다면 새로운 오브젝트풀 할당한다.
AtkObjPool atkObjPool = new AtkObjPool();
atkObjPool.Init(atkEffectBase, skillInitialPerId, skillMaxPerId, skillExpandable, transform);
LogC.LogColor("스킬의 키 : " + key+"스킬 오브젝트 이름:"+baseSkill, Color.red);
_skillCorePool.Add(key, atkObjPool);
}
else
{
#if UNITY_EDITOR
LogC.LogColor("이미 할당된 공격 옵젝풀 key : " + key, Color.yellow);
#endif
}
}
public AtkEffectBase GetAttackEffect(int skillId)
{
AtkObjPool atkObjPool;
AtkEffectBase atkobj;
_skillCorePool.TryGetValue(skillId,out atkObjPool);
atkobj = atkObjPool.Rent();
if(atkobj == null)
{
LogC.LogColor(skillId+"번호 풀에 없음",Color.yellow);
//추후 데이터가 없으면 해당 옵젝풀에 객체 추가하는 로직 만들기.
return null;
}
else
{
//오브젝트 풀 등록이 되어있으면
return atkobj;
}
}
// ===== Monster API =====
public AiMonster RentMonster()
{
return _monsterPool.Rent();
}
public void ReturnMonster(AiMonster m)
{
_monsterPool.Return(m);
}
public void ReturnDamageText(DamageText t)
{
_txtPool.Return(t);
}
}
예전에는 직접 만든 오브젝트 풀에서, 스폰된 오브젝트를 List에 Add하고
그중 비활성화된 오브젝트를 찾아서 꺼내 쓰는 방식으로 구현했다.
문제는 이 방식이 O(n) 시간 복잡도를 가지다 보니,
오브젝트 수가 많아질수록 성능 부담이 커진다는 점이었는데
그래서 로직을 전면 개선했다.
이제는 **O(1)**로 즉시 오브젝트를 가져올 수 있고,
제네릭을 적용해 다양한 타입의 오브젝트를 하나의 풀 시스템에서 관리할 수 있게 되었다.
// PoolManager.cs
using DevJun.DebugHelper;
using System. Collections;
using System. Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PoolManager : Singleton_generic<PoolManager>
{
[Header("몬스터 풀")]
[SerializeField] private AiMonster monsterPrefab;
[SerializeField] private int monsterInitial = 16;
[SerializeField] private int monsterMax = 50;
[SerializeField] private bool monsterExpandable = true;
private MonsterPool _monsterPool;
private Transform _monsterRoot;
[Header("데미지 텍스트 풀")]
[SerializeField] private Transform DamageTextObj;
[SerializeField] private Transform DamageTextRoot;
[SerializeField] private int _textInitialPerId = 4;
[SerializeField] private int _textMaxPerId = 64;
[SerializeField] private bool _textExpandable = true;
[Header("데미지 텍스트 프리팹")]
[SerializeField] DamageText _txtPrefab;
private DamageTextPool _txtPool;
/// ============================================================
/// 공격 오브젝트도 개별적인 풀을 가지고 있으나 해시테이블을 통해 관리를 진행한다.
/// ============================================================
//DB에서 데이터를 가지고온후 공격 오브젝트 번호 세팅 기본공격은 -1부터 시작 스킬은 1번부터 시작
[Header("공격오브젝트 풀")]
[SerializeField] private Transform skillRoot;
[SerializeField] private int skillInitialPerId = 3;
[SerializeField] private int skillMaxPerId = 5;
[SerializeField] private bool skillExpandable = true;
[Header("공격용 프리팹)")]
[SerializeField] private List<AtkEffectBase> skillPrefabs = new List<AtkEffectBase>();
private Dictionary<int, AtkObjPool> _skillCorePool;
protected override void Awake()
{
base.Awake();
if (_monsterRoot == null)
{
_monsterRoot = new GameObject("[POOL] Monsters").transform;
_monsterRoot.SetParent(transform, false);
}
if (skillRoot == null)
{
skillRoot = new GameObject("[POOL] Skills").transform;
skillRoot.SetParent(transform, false);
}
if (DamageTextObj == null)
{
DamageTextObj = new GameObject("[POOL] txt").transform;
DamageTextObj.SetParent(transform, false);
}
var pool = _monsterRoot.GetComponent<MonsterPool>();
if (pool == null)
{
pool = _monsterRoot.gameObject.AddComponent<MonsterPool>();
LogC.LogColor("에드 컴포넌트 완료", Color.red);
}
_monsterPool = pool;
// 컴포넌트 초기화(필드 세팅 or Init 호출)
_monsterPool.Init(monsterPrefab, monsterInitial, monsterMax, monsterExpandable, _monsterRoot);
var txt_pool = DamageTextObj.GetComponent<DamageTextPool>();
if (txt_pool == null)
{
txt_pool = DamageTextObj.gameObject.AddComponent<DamageTextPool>();
LogC.LogColor("데미지 텍스트 에드 컴포넌트 완료", Color.red);
}
_txtPool = txt_pool;
// 컴포넌트 초기화(필드 세팅 or Init 호출)
_txtPool.Init(_txtPrefab, _textInitialPerId, _textMaxPerId, _textExpandable, DamageTextRoot);
//리스트에 프리팹 세팅후 옵젝에 스킬 등록하기
RegisterSkills();
}
메니저를 만들어서 쉽게 관리가 가능하다.
'최적화' 카테고리의 다른 글
튜닝<그래픽> (1) | 2025.06.17 |
---|---|
튜닝 - 물리(Physics) (1) | 2025.06.15 |
에셋 관련 최적화 (1) | 2025.06.01 |
튜닝 기본 상식(2) (0) | 2025.05.17 |
최적화<튜닝>의 기본상식1 (4) | 2025.05.11 |