싱글톤, 유니티 이벤트를 알아보고
기존에 제작했던 에너미 스포너부터 시작해서
전부 ObjectManager.cs가 관리하도록 개편 예정
프로그램 전체에서 단 하나의 인스턴스만 존재하도록 보장하고
어디서든 접근할 수 있게 하는 디자인 패턴.
이런 경우 new, Instantiate 사용 시, 매번 다른 인스턴스가 생기지만
싱글톤은 전역적으로 단 하나만 유지되므로 불필요한 중복 생성을 방지할 수 있음
GameScore 등의 활용이 가능하다

UnityEngine.Events 네임스페이스에 정의된 시리얼라이즈 가능한 이벤트 클래스.
인스펙터에서 직접 연결 가능하다.
우선 C# 과정에서 배운 delegate, event에 대해 간단히 복습해보고
유니티에서의 이벤트와 어떤 차이가 있나 알아보겠다.
메서드를 변수처럼 저장하고 호출할 수 있는 타입
public delegate void OnPlayerDie();
Invoke() 를 통해 연결된 함수를 호출

델리게이트를 한정해서 외부에서 직접 호출할 수 없도록 보호하는 키워드
배웠던 대로 외부에서 +=, -= 연산자를 통한 포함, 제외만 허용하고
invoke, = 대입을 사용하는 걸 막는 기능
여기까지 알아보았으니 유니티 이벤트와의 차이를 정리해보자.
| 개념 | 핵심 키워드 | 사용 목적 |
|---|---|---|
| delegate | 함수 참조, 콜백 | 메서드 연결 |
| event | 안전한 알림 | 델리게이트 보호 |
| UnityEvent | 시리얼라이즈, 인스펙터 연결 | 시각적 이벤트 관리 |
4~5일차에 제작한 EnemySpawner.cs 및 불릿 생성 메서드는
수많은 객체를 Instantiate, Destroy 하며 생성되기에
해당 과정에서 불필요한 GC 부하가 생기고 있다.
이 부하를 없앨 방식으로 오브젝트 풀링 기법을 사용하며
추가로 모든 오브젝트를 관리할 중앙 집중형 오브젝트 관리 시스템을 구축해보겠다.
일단 오브젝트 풀링에 대해 먼저 알아보도록 하겠다.
오브젝트를 필요할 때마다 생성, 파괴하는 방식이 아닌
미리 일정 개수를 만들어 보관해두고 필요에 따라 꺼내 쓰고
다시 돌려놓는 방식으로, GC 부하를 최소화하기 위해 사용한다.
일단 ObjectManager와 추후 GameManager에게 상속해줄 싱글톤 제네릭 클래스를 제작해보겠다.
//제네릭, T는 MonoBehaviour를 상속받는 클래스 로 제한.
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
//외부 호출용 프로퍼티, 해당 타입의 싱글톤이 없으면 찾아보고, 없을 시 새로 생성 후 설정
public static T Instance
{
get
{
//아직 인스턴스 할당이 되어있지 않을 시
if (_instance == null)
{
//씬에서 해당 타입의 객체를 찾기
_instance = FindObjectOfType<T>();
//여전히 null이면?
if (_instance == null)
{
//T 타입의 이름을 가진 새 게임오브젝트를 생성
GameObject singletoneObj = new GameObject();
singletoneObj.name = typeof(T).ToString();
//컴포넌트 추가, 인스턴스로 설정
_instance = singletoneObj.AddComponent<T>();
}
}
return _instance;
}
}
protected virtual void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
//다른 인스턴스가 존재 시, 현재 인스턴스 파괴(중복방지)
if (_instance != this)
{
Destroy(gameObject);
}
}
}
이제 해당 클래스를 상속받을 ObjectManager.cs를 만들어보겠다.
일단 에너미 프리펫부터 관리하도록 만들고
여기엔 오브젝트 Get, Return하도록 역할을 부여하겠다.
Singleton<T> 클래스의 상속을 받도록 제작
public class ObjectManager : Singleton<ObjectManager>
일단 오브젝트 풀링 기법을 사용해야 하는데
아직 잘 모르겠어서 예시를 검색해보았다.

필요한 메서드가 어떤 건지 알아냈으니
예시 코드를 참고해서 내 코드를 작성해보자.
일단 기본 변수 선언과
에너미를 생성하고 Pool에 담는 메서드부터 제작.
근데 이렇게 되면 다른 종류의 오브젝트를 만들 때마다 계속 추가가 필요하다.
확장성 면에서 좋은 방향성이 아닌 듯 하다.
일단 오브젝트 풀링 자체가 처음이니 이 방식으로 연습해보고
나중에 모든 오브젝트 종류를 총괄하는 범용적인 방식으로 제작해보자.
public class ObjectManager : Singleton<ObjectManager> //싱글톤 상속
{
[SerializeField] private GameObject enemyPrefab;
//풀 사이즈 지정
[SerializeField] private int initialPoolSize = 20;
//에너미 넣을 Queue 생성
private Queue<GameObject> enemyPool = new Queue<GameObject>();
private void InitializeEnemyPool()
{
if (enemyPrefab == null)
{
return;
}
//에너미 풀 사이즈만큼 생성해서 Enqueue
for (int i = 0; i < initialPoolSize; i++)
{
GameObject newEnemy = Instantiate(enemyPrefab, transform);
newEnemy.SetActive(false);
enemyPool.Enqueue(newEnemy);
}
}
}
EnemySpawner.cs 쪽에서 오브젝트를 요청하는 용도의 함수
//에너미스포너가 사용할 함수
public GameObject GetEnemy()
{
GameObject enemyToSpawn;
//저장해둔 Enemy를 Dequeue
if (enemyPool.Count > 0)
{
enemyToSpawn = enemyPool.Dequeue();
}
else
{
// 풀이 비었으면 null 반환
return null;
}
//보내기 전에 액티브 전환
enemyToSpawn.SetActive(true);
return enemyToSpawn;
}
에너미를 풀로 다시 반환받는 함수
public void ReturnEnemy(GameObject enemy)
{
// ObjectManager의 풀로 반환
enemy.SetActive(false);
enemyPool.Enqueue(enemy);
}
이게 맞는 건지 잘 모르겠다.
EnemySpawner.cs 도 수정해야된다




기존 위치에 제작했는데
굳이 코루틴이 오류 조건까지 체크해야할 이유가 있을까 싶어서 밖으로 빼봤다

스타트 시 체크하도록 변경하고 통과 시, 적 생성 코루틴 스타트.

GetEnemy() 를 통해 오브젝트매니저의 풀에서 Dequeue해서 정보를 가져옴
마지막으로 Enemy.cs의 적 사망 로직 변경
위와 마찬가지로 Destroy() 부분을 ReturnEnemy()로 변경

이러면 추후에 Hp값을 설정 시
Hp가 감소한 채로 오브젝트 풀로 Return 될 문제가 생길 거다

까먹지 않게 미리 추가해뒀음
오늘은 감기가 심해서 과제만 제출하고 꺼야겠고
내일은 시간 되면 Hp랑 불릿 데미지도 만들어보자...
에너미는 대충 끝난 듯 하다
구현 테스트 해보겠다

한 마리씩만 생성됨
하나 죽으면 바로 생성

스폰 딜레이를 낮춰도 동일한 문제 발생
로그를 이곳 저곳 달며 확인하다가 찾았다

처음 생성 로그가 떠야 되는데 안 뜨길래 오브젝트매니저를 확인해보니

에너미를 초기 생산하여 풀에 넣는 함수가 사용되지 않고 있었다.
기존 싱글톤.cs에 맞춰 오버라이드 Awake 제작
protected override void Awake()
{
base.Awake();
InitializeEnemyPool();
}

0번부터 19번 개체 생성되어 스폰되는 거 확인했다.
이제 Bullet 오브젝트들도 오브젝트 풀링으로 변경해보자
오브젝트 풀링 쓸 건데
lifeTime 세팅을 이제 없애도 되지 않을까? 했는데
나중에 전방향으로 퍼지는 스킬 등을 구현할 때는
Net를 쓰기보단 이 방법이 나을 듯 해서 유지하기로 결정.
대신 수명이 다하면 다시 오브젝트 풀로 돌아가는 방식으로 구현.
OnEnable()에 넣으면 되겠다
현재 총알 수명도 여기서 초기화 해주면 될 듯.

추가로 수명체크 메서드 만들어서 업데이트마다 작동하도록 구현.
나머지 Destroy() 조건도 전부 ReturnBullet(gameObject)로 변경하고
오브젝트매니저에 에너미와 마찬가지로 함수 제작해보겠다.

그 전에 앞에 오브젝트매니저 인스턴스의 메서드임을 명시하지 않았기에 추가

에너미 오브젝트랑 같은 방식으로 제작함

저번 과제로 총구에 만들어 둔 Weapon.cs에서
불릿 인스턴스를 생성하고 있었다
이걸 변경하고 불릿풀 생성 디버그 로그도 출력해봐야겠다

변경 완료

디버그 로그 출력 추가 완료
다시 테스트

300개 생성 문제없는데
총구에서 발사가 안되고 오브젝트매니저 빈칸 위치에서 발사중
이건 비교적 쉽다
bulletInstance.transform.position = firePoint.position;
bulletInstance.transform.rotation = firePoint.rotation;
FireBullet() 실행 시 포지션을 총구로 변경

구현완료
저번에 과제 때문에 만든 Weapon.cs인데 이런 임시방편 대신 개선이 필요해보인다.
오늘 배운 개념은 복습이 절반이라 어려울 건 없었다.
다만 현재 코드에 문제점이 꽤 있는 듯 하여 추후 개선할 내용을 정리하며 마치겠다.

현재 불릿 정하는 것도 오브젝트 매니저에게 권한이 넘어간 상황이라
Weapon.cs에게 뭔가 특별한 기능을 넣어주면 좋을 듯?

앞으로 오브젝트 종류가 늘어날 때마다 이런 식으로 작성 시
불필요한 코드가 너무 많고 가독성이 떨어진다.
저번 콘솔 프로젝트 처럼
프리팹을 Key, 오브젝트풀Queue를 Value로 딕셔너리를 제작하여
리스트화 해야겠다.
이건 질문해보고 결정해야겠다.