프로젝트 현황

현재 위 사진과 같은 닷지 게임을 유니티로 제작하는 실습 상황에 있다.
이전까지 구현사항은
- Bullet.cs
- BulletSpawner.cs
- Enemy.cs
- EnemySpawner.cs
- Player.cs
- Weapon.cs
- InputComponent.cs
이러한 구조를 베이스로 기본적으로 다음과 같은 흐름을 갖고있다.
- Player → 입력을 받아 **Weapon**을 작동시킴.
- Weapon → 내부에서 BulletSpawner.Fire() 호출.
- BulletSpawner → Instantiate()로 총알 생성.
- Bullet → 충돌 시 자기 자신을 SetActive(false)로 비활성화.
- EnemySpawner → 코루틴으로 일정 간격마다 적 생성.
- Enemy → 체력이 0이 되면 사망 처리 후 스패너에게 콜백을 보냄.
즉, 총알과 적 각각의 생성/파괴 관리 로직이 스크림마다 따로 존재한다.
현재 정상적으로 잘 작동하고 있는 상태지만 다음과 같은 과제를 받았다.
과제
주제
오브젝트 매니저(Object Manager) 설계 및 생성/파괴 관리
과제 소개
이번 과제에서는 총알, 적, 기타 게임 오브젝트의 생성과 파괴를 관리하는 매니저를 설계합니다.
오브젝트 풀링과 매니저 중심의 관리 구조를 통해 게임 성능을 최적화하고,
생성/삭제 로직을 중앙에서 제어하는 것이 목표입니다.
과제 목표
- 총알, 적 등의 오브젝트를 매니저 클래스에서 중앙 관리
- 오브젝트 생성과 파괴 로직을 효율적으로 구현
- 매니저를 통해 게임 내 모든 생성/삭제 이벤트를 일원화
구현 내용
1. 오브젝트 매니저 설계
- 싱글톤 패턴 또는 게임 내 유일한 매니저 오브젝트로 구현
- 총알, 적, 기타 오브젝트를 참조 및 관리하도록 설정
2. 생성/파괴 관리
- 오브젝트 생성 시 매니저를 통해 인스턴스화
- 오브젝트 파괴 시 매니저를 통해 제거 또는 비활성화
구현 시 참고 요소
- 싱글톤 패턴으로 매니저 접근 방식 통일
- UnityEvent 또는 C# 이벤트를 활용한 생성/삭제
- 생성과 파괴 시 게임 로직과의 연계
제출 안내
- 프로젝트 명: 제출일자 Singleton 이름
- 제출 형식: ObjectManger.cs파일 제출
고민
위 과제 내용을 기반으로 기존 코드의 개선해야 할 사항을 추려보았다.
1. 생성과 파괴 로직이 중복됨
- 총알과 적 모두 자기 스크립트 안에서 직접 Instantiate와 Destroy를 반복함.
- 스패너도 자체 풀을 유지하지만, 관리 주체가 분산되어 있음.
2. 파괴 로직이 통일되지 않음
- Bullet은 그냥 SetActive(false)로 꺼버림.
- Enemy는 _spawner.EnemyDead(this) 식으로 스패너를 직접 참조함.
- 즉, “누가 파괴를 담당하냐”가 오브젝트마다 다름.
3. 성능 및 유지보수에 불리
- 관리 대상이 늘어나면 각 스패너를 따로 수정해야 함.
- 게임 전체의 오브젝트 생명주기를 추적하기 어려움.
문제는 이미 Spawner로 Object Pooling 까지 구현돼있는 현재 코드에서, 굳이 싱글톤을 써야하는가?
만약 쓴다면 어떤 부분에 어떻게 적용해야하는가? 를 고민하는 시간이 많이 들었다.
기존의 Spawner 들도 일종의 manager 역할을 하고 있는 상태인데
그렇다면 기존 Spawner 들을 모두 제거하고 ObjectManager 하나로 관리해야하는가?
그렇다면 기존 코드가 꼬이게되니 수정해야할 부분이 늘어나기에 이것이 과연 얼마나 기능적, 구조적인 이득을 주는가에 대한 고민이 쉽게 해결되지 않았다.
무엇보다 설령 합친다 하더라도 중앙화할 경계선을 어디로 잡아야하는지에 대한 아이디어도 애매했다.
구현 아이디어
우선 현재 생성/파괴 (활성/비활성화) 로직이 적용돼있는 것은 Bullet, BulletSpawner, Enemy, EnemySpawner 뿐이다.
그리고 생성 / 파괴 부분과 풀을 관리하는 부분이 중복되며, 이후에도 풀링을 사용하게 되면 동일한 구조가 반복될 것이 예상된다.
그렇다면 생성, 파괴, 풀링 관리 부분을 ObjectManager.cs로 묶어 관리하는 구조로 방향성을 잡아보자.
우선 싱글톤을 만들어준다.
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
// 외부 호출용 프로퍼티, 해당 타입의 싱글톤이 없으면 찾아보고 그래도 없으면 새로 생성 후 설정
public static T Instance
{
get
{
if (_instance == null)
{
// _instance = FindObjectOfType<T>();
_instance = FindAnyObjectByType<T>();
if (_instance == null)
{
GameObject singletonObj = new GameObject();
_instance = singletonObj.AddComponent<T>();
singletonObj.name = typeof(T).ToString();
}
}
return _instance;
}
}
// 중복 체크 및 연결 기능 구현
protected virtual void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
if (_instance != this)
{
Destroy(gameObject);
}
}
}
}
다음으로 ObjectManager를 만들어 주자
using System.Collections.Generic;
using UnityEngine;
public class ObjectManager : Singleton<ObjectManager>
{
private Dictionary<GameObject, Queue<GameObject>> _pool = new();
public GameObject Spawn(GameObject prefab, Vector3 pos, Quaternion rot, Transform parent = null)
{
if (!_pool.TryGetValue(prefab, out var queue))
{
queue = new Queue<GameObject>();
_pool[prefab] = queue;
}
GameObject obj = queue.Count > 0
? queue.Dequeue()
: Instantiate(prefab, parent);
obj.transform.SetPositionAndRotation(pos, rot);
obj.SetActive(true);
return obj;
}
public void Despawn(GameObject prefab, GameObject instance)
{
instance.SetActive(false);
if (!_pool.TryGetValue(prefab, out var queue))
{
queue = new Queue<GameObject>();
_pool[prefab] = queue;
}
queue.Enqueue(instance);
}
}
이제 여기서 생성(활성화), 파괴(비활성화), 풀링까지 관리할 거다.
기존 Spawner 코드에서 pool 부분을 제거하고 ObjectManager를 통해 관리하는 형태로 변경하면 끝난다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[Header("Enemy Settings")]
[SerializeField] private GameObject enemyPrefab;
[SerializeField] private Transform spawnPointBox;
[SerializeField] private float spawnInterval = 1f;
[SerializeField] private int prewamrCount = 20;
private Transform[] _spawnPoints;
private WaitForSeconds _delay;
void Start()
{
if (spawnPointBox == null) return;
_spawnPoints = new Transform[spawnPointBox.childCount];
for (int i = 0; i < spawnPointBox.childCount; i++) _spawnPoints[i] = spawnPointBox.GetChild(i);
_delay = new WaitForSeconds(spawnInterval);
StartCoroutine(SpawnLoop());
}
private void SpawnEnemy()
{
if (!enemyPrefab || _spawnPoints.Length == 0) return;
int rnd = Random.Range(0, _spawnPoints.Length);
Transform spawnPoint = _spawnPoints[rnd];
// OM 스폰
GameObject enemy = ObjectManager.Instance.Spawn(enemyPrefab, spawnPoint.position, spawnPoint.rotation, transform);
// 적 초기화
Enemy enemyScript = enemy.GetComponent<Enemy>();
if (enemyScript != null) enemyScript.Init(this);
}
private IEnumerator SpawnLoop()
{
if (!enemyPrefab || _spawnPoints.Length == 0) yield break;
while (true)
{
SpawnEnemy();
yield return _delay;
}
}
public void EnemyDead(GameObject enemy)
{
// OM 디스폰
ObjectManager.Instance.Despawn(enemyPrefab, enemy);
}
}
결과
정상 작동을 확인했다.
현재는 Spawner가 Bullet, Enemy 두가지 뿐이라 큰 변화가 체감되지 않는다.
기존 완성됐다 생각한 구조를 싱글톤을 적용하라는 과제에 맞춰 생각하려니 다소 억지스러운 부분도 있던 것 같고, 현재 코드에 대한 확신이 많이 부족해지는 순간이 왔다.
싱글톤의 개념 자체는 쉬웠기에 이해했지만 역시나 응용 적용이 많이 힘든 부분인 것 같다.
디자인 패턴 역시 반복 사용을 통핸 숙련 외에는 답이 없는 것인지 더 고민해 볼 필요가 있겠다.
'Unity' 카테고리의 다른 글
| Design Pattern - Observer Pattern (옵저버 패턴) (0) | 2025.10.21 |
|---|---|
| Unity - UGUI : Unity Graphical User Interface (0) | 2025.10.21 |
| Unity - EventSystem (이벤트 시스템) (0) | 2025.10.20 |
| Unity - Coroutine, yield, Invoke, IEnumerator (0) | 2025.10.17 |
| Unity - Rigidbody, Collider - 물리, 충돌 처리 (0) | 2025.10.16 |