0. 핵심 내용
○ 오브젝트 풀링
- 게임 개발에 널리 사용되는 테크닉으로, 게임의 성능을 개선하기 위해 사용됩니다.
- 오브젝트 풀링은 객체를 미리 생성해 두고 필요할 때 가져다 사용하고, 사용이 끝나면 다시 풀에 반납하는 방식을 말합니다.
- 오브젝트 풀링은 생성(Instantiate)과 소멸(Destroy)이라는 비용이 큰 작업을 최소화함으로써 성능을 향상시키는 데 중요한 역할을 합니다.
- 이는 특히 빈번하게 생성하고 파괴되는 객체(예: 총알, 입자 등)에 대해 중요하며, 이런 객체들을 풀에 저장해 놓고 재사용함으로써 메모리 할당과 가비지 컬렉션에 따른 성능 저하를 방지할 수 있습니다.
- 오브젝트 풀링은 적절히 사용하면 큰 성능 개선을 가져올 수 있지만, 불필요한 메모리 사용을 증가시킬 수 있으므로 사용 시에는 신중해야 합니다. 오브젝트 풀의 크기를 적절히 조절하는 것이 중요합니다.
1. 오브젝트 풀 만들기
○ ObjectPool 만들기
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
// 오브젝트 풀 데이터를 정의할 데이터 모음 정의
[System.Serializable]
public class Pool
{
public string tag;
public GameObject prefab;
public int size;
}
public List<Pool> Pools;
public Dictionary<string, Queue<GameObject>> PoolDictionary;
private void Awake()
{
// 인스펙터창의 Pools를 바탕으로 오브젝트풀을 만들 것.
// 오브젝트풀은 오브젝트마다 따로이며, pool개수를 넘어가면 강제로 끄고 새로운 오브젝트에게 할당.
PoolDictionary = new Dictionary<string, Queue<GameObject>>();
foreach (var pool in Pools)
{
// 큐는 FIFO(First-in First-out) 구조로서, 줄을 서는 것처럼 가장 오래 줄 선(enqueue) 객체가 가장 먼저 빠져 나올(dequeue) 수 있는 구조
Queue<GameObject> objectPool = new Queue<GameObject>();
for (int i = 0; i < pool.size; i++)
{
// Awake하는 순간 오브젝트풀에 들어갈 Instantitate 일어나기 때문에 터무니없는 사이즈 조심
GameObject obj = Instantiate(pool.prefab);
obj.SetActive(false);
// 줄의 가장 마지막에 세움.
objectPool.Enqueue(obj);
}
// 접근이 편한 Dictionary에 등록
PoolDictionary.Add(pool.tag, objectPool);
}
}
public GameObject SpawnFromPool(string tag)
{
// 애초에 Pool이 존재하지 않는 경우
if (!PoolDictionary.ContainsKey(tag))
return null;
// 제일 오래된 객체를 재활용
GameObject obj = PoolDictionary[tag].Dequeue();
PoolDictionary[tag].Enqueue(obj);
obj.SetActive(true);
return obj;
}
}
○ 오브젝트 풀 적용하기
- 임시로 Character에 ObjectPool 컴포넌트 추가.
(현재 상황에서는 플레이어가 오브젝트 풀 가지고 있어도 괜찮으나, 적이 등장하면서 대응할 수 없는 구조임. 적 구현하면서 수정 예정)
- 다음과 같이 Object Pool 수정
🚧 Prefab의 [⊙]를 눌러도 Arrow 뜨지 않습니다.
프리팹은 씬에 직접 배치되어 있는 것이 아니기 때문입니다. 드래그앤드랍해주세요.
○ TopDownShooting 수정
using UnityEngine;
public class TopDownShooting : MonoBehaviour
{
private TopDownController contoller;
// TODO 나중에 GameManager를 만들어서 옮길 예정 (모든 오브젝트는 오브젝트풀을 쓸 권리가 있으니까)
private ObjectPool objectPool;
private Vector2 aimDirection = Vector2.right;
[SerializeField] private Transform projectileSpawnPosition;
public GameObject testPrefab;
private void Awake()
{
contoller = GetComponent<TopDownController>();
objectPool = GetComponent<ObjectPool>();
}
void Start()
{
contoller.OnAttackEvent += OnShoot;
// OnLookEvent에 이제 두개가 등록되는 것(하나는 지난 시간에 등록했었죠? TopDownAimRotation.OnAim(Vec2)
// 한 개의 델리게이트에 여러 개의 함수가 등록되어있는 것을 multicast delegate라고 함.
// Action이나 Func도 델리게이트의 일종인 것 기억하시죠..?
contoller.OnLookEvent += OnAim;
}
private void OnAim(Vector2 newAimDirection)
{
aimDirection = newAimDirection;
}
private void OnShoot(AttackSO attackSO)
{
RangedAttackSO RangedAttackSO = attackSO as RangedAttackSO;
float projectilesAngleSpace = RangedAttackSO.multipleProjectilesAngel;
int numberOfProjectilesPerShot = RangedAttackSO.numberofProjectilesPerShot;
float minAngle = -(numberOfProjectilesPerShot / 2f) * projectilesAngleSpace + 0.5f * RangedAttackSO.multipleProjectilesAngel;
for (int i = 0; i < numberOfProjectilesPerShot; i++)
{
float angle = minAngle + projectilesAngleSpace * i;
float randomSpread = Random.Range(-RangedAttackSO.spread, RangedAttackSO.spread);
angle += randomSpread;
CreateProjectile(RangedAttackSO, angle);
}
}
private void CreateProjectile(RangedAttackSO RangedAttackSO, float angle)
{
// 오브젝트 풀을 활용한 생성으로 변경
GameObject obj = objectPool.SpawnFromPool(RangedAttackSO.bulletNameTag);
// 발사체 기본 세팅
obj.transform.position = projectileSpawnPosition.position;
ProjectileController attackController = obj.GetComponent<ProjectileController>();
attackController.InitializeAttack(RotateVector2(aimDirection, angle), RangedAttackSO);
}
private static Vector2 RotateVector2(Vector2 v, float degree)
{
return Quaternion.Euler(0, 0, degree) * v;
}
}