✅ 오늘의 백로그
1. Spawner 구현
1. Spawner 구현
스포너를 구현할 때 고려하여야할 점
1. 천우인은 실외에 스폰
2. 그 외 이상현상은 실내에 스폰
3. 부적을 사용하면 스폰된 이상현상은 제거되는 로직 필요
4. 의뢰를 선택하는 곳에서 난이도에 따라 이상현상 개수를 넘겨주도록 함수 세팅
5. 넘겨진 개수에 따라 스폰되도록 하고, 여러 이상현상 중에 랜덤으로 스폰되도록 구현
얼추 이렇게 5가지인 것 같다.
using UnityEngine;
[CreateAssetMenu(menuName = "Abnormal/AbnormalData")]
public class AbnormalData : ScriptableObject
{
public EAbnormal type;
public GameObject prefab;
[Header("Object Pool Use")]
public bool useObjectPool = false; // 오브젝트풀 사용 여부
public int poolSize; // 풀 크기
}
우선 SO를 만들어주어서
스폰할 대상의 대한 정보를 넣어주도록 하였다.
public enum EAbnormal { Heukgi, Cheonwooin, Eodukshini }
public class AbnormalSpawner : MonoBehaviour
{
[SerializeField] List<AbnormalData> abnormalDatas; // 이상현상 목록
// 스폰 위치
[SerializeField] float navSampleRadius = 25f; // 스폰 반경
[SerializeField] int navSampleMaxTries = 5; // 위치 찾기 재시도 횟수 (무한 루프 방지용)
// 내부 조회용
Dictionary<EAbnormal, AbnormalData> _dataByType;
// 현재 맵에 존재하는 이상현상 추적 (중복 방지용)
Dictionary<EAbnormal, GameObject> _spawned = new Dictionary<EAbnormal, GameObject>();
}
그리고 스포너 스크립트에서
AbnormalData를 빠르게 조회할 수 있도록 룩업용 딕셔너리를 만들어주었고
한 씬에 중복된 이상현상은 나타나지 않도록 하기 위해서
_spawned으로 스폰된 이상현상을 담을 딕셔너리를 만들어주었다.
public void SpawnAnomalies(int count)
{
if (_dataByType == null || _dataByType.Count == 0)
return; // AbnormalData 목록이 비어 있으면 스폰 불가
for (int i = 0; i < count; i++)
{
// 맵에 없는 이상현상만 후보에 남기기
var candidates = GetAvailableTypes();
if (candidates.Count == 0)
{
// 더 이상 스폰 가능한 이상현상이 없음
if (i == 0)
Debug.Log("현재 맵에는 모든 종류의 이상현상이 스폰되어 있습니다.");
break;
}
// 랜덤으로 이상현상 하나 선택
var pick = candidates[Random.Range(0, candidates.Count)];
// 스폰
if (TrySpawn(pick, out var instance))
_spawned[pick] = instance;
else
Debug.LogWarning($"{pick} 스폰 실패(위치 미확보/풀 미할당 등)");
}
}
/// <summary>
/// 현재 맵에 존재하지 않는 이상현상 목록 반환
/// </summary>
List<EAbnormal> GetAvailableTypes()
{
var list = new List<EAbnormal>();
foreach (var kv in _dataByType)
{
if (_spawned.ContainsKey(kv.Key) == false || _spawned[kv.Key] == null)
list.Add(kv.Key);
}
return list;
}
그리고 이제 SpawnAnormalies 메서드 안에서
AbnoramlData로 받아온 것 중에 하나를 랜덤으로 스폰하게 하는 로직을 처리해주었다.
의뢰를 선택하는 곳에서 이 함수를 호출해서 count값을 넣어주면 된다.
public void DeSpawn(EAbnormal type)
{
if (_spawned.TryGetValue(type, out var go) == false || go == null)
return;
if (_dataByType.TryGetValue(type, out var data) && data != null && data.useObjectPool)
{
if (poolManager != null)
poolManager.Return(go);
else
go.SetActive(false);
}
else
Destroy(go);
_spawned[type] = null;
}
그리고 부적으로 인해 소멸되는 로직도 넣어주었다.
부적 스크립트에서 이 함수를 호출하면 된다.
이제 이상현상이 맵에 스폰되는 로직을 만들어주면 되는데
/// <summary>
/// NavMesh 위 임의 좌표 계산
/// </summary>
bool AbnormalSetPosition(out Vector3 pos)
{
for (int t = 0; t < Mathf.Max(1, navSampleMaxTries); t++)
{
var random = Random.insideUnitSphere * navSampleRadius;
if (NavMesh.SamplePosition(random, out var hit, 3f, NavMesh.AllAreas))
{
pos = hit.position;
return true;
}
}
pos = default;
return false;
}
/// <summary>
/// BoxCollider 가져오기 (천우인 전용)
/// </summary>
void EnsureTopVolumesCached()
{
if (_topVolumes != null) return;
_topVolumes = new List<BoxCollider> ();
var tagged = GameObject.FindGameObjectsWithTag(TOP_TAG);
foreach (var go in tagged)
{
var bc = go.GetComponent<BoxCollider>();
if (bc && bc.enabled)
_topVolumes.Add(bc);
}
}
/// <summary>
/// Box Collider 내부에 스폰 (천우인 전용)
/// </summary>
bool AbnormalSetTopPosition(out Vector3 pos, out Bounds bounds)
{
EnsureTopVolumesCached();
bounds = default;
if (_topVolumes.Count == 0)
{
pos = default;
Debug.LogWarning("[AbnormalSpawner] spawnTop 태그의 BoxCollider가 없습니다.");
return false;
}
var vol = _topVolumes[Random.Range(0, _topVolumes.Count)];
var b = vol.bounds;
bounds = b;
float x = Random.Range(b.min.x, b.max.x);
float z = Random.Range(b.min.z, b.max.z);
float y = b.max.y;
pos = new Vector3(x, y + 0.02f, z);
return true;
}
천우인은 하늘에서 스폰될 수 있도록
spawnTop 태그가 붙은 오브젝트를 찾아 거기서 스폰되도록 구현해주었고
나머지 이상현상들은 NavMesh 위에서 스폰될 수 있도록 해주었다.
이때 스폰될때 저번처럼 셰이더를 활용해서 스폰되게 해주면 더 좋을 것 같단 생각이 드는데..
이부분은 차차 고려해보기로 한다. (제대로 구현이 되는지도 모르기에..ㅠㅠ)
/// <summary>
/// 오브젝트 풀 사용 대상은 프레임 드랍 방지 차원으로 미리 PoolManager에 등록
/// </summary>
void RegisterPools()
{
if (poolManager == null)
return;
foreach (var data in abnormalDatas)
{
if (data == null || data.prefab == null) continue;
if (data.useObjectPool)
{
int size = data.poolSize;
if (size > 0)
poolManager.Register(data.prefab, size);
}
}
}
/// <summary>
/// 스폰 시도
/// </summary>
bool TrySpawn(EAbnormal type, out GameObject instance)
{
instance = null;
if (_dataByType.TryGetValue(type, out var data) == false || data == null)
{
Debug.Log($"AbnormalData 누락: {type}");
return false;
}
if (data.prefab == null)
{
Debug.LogWarning($"프리팹 누락: {type}");
return false;
}
Vector3 spawnPos;
Bounds boundsToPass = default; // 천우인 전용
if (data.useTopSpawn)
{
if (!AbnormalSetTopPosition(out spawnPos, out boundsToPass))
return false;
}
else
{
if (!AbnormalSetPosition(out spawnPos))
return false;
}
// 오브젝트풀 사용 여부에 따른 스폰
if (data.useObjectPool)
{
if (poolManager == null)
{
Debug.LogWarning($"ObjectPoolManager 미할당: {type}");
return false;
}
instance = poolManager.Get(data.prefab);
if (instance == null)
return false;
instance.transform.SetPositionAndRotation(spawnPos, Quaternion.identity);
instance.SetActive(true);
// IPoolable 구현했는지 확인하고 SetOwnerPool 호출
if (instance.TryGetComponent<IPoolable>(out var abnormalInstance))
{
// 이 인터페이스를 구현한 이상현상은 모두 풀 소유권 배정
abnormalInstance.SetOwnerPool(poolManager);
if (abnormalInstance is Cheonwooin cw)
cw.Activate(boundsToPass, data);
}
}
else
instance = Instantiate(data.prefab, spawnPos, Quaternion.identity);
return true;
}
그리고 이제
실내/실외 스폰 타입인지 체크하고 그에 맞게 스폰해주고
오브젝트풀을 사용하는 타입인지 체크하고 그에 맞게 스폰해주도록 구현해주었는데
이렇게 구현하니까 큰 문제점이 있었다.
오브젝트 풀을 생성만하고
다시 리턴, 생성 반복하는 로직이 있어야 하는데
스포너는 모든 이상현상을 다루고 있고
이상현상마다 오브젝트 풀 동작 방식이 다르기에
(흑기 = 점차 오브젝트 풀의 개수를 늘려가는 방식 / 천우인 = 배치된 오브젝트풀 개수만큼 생성/리턴 반복하는 방식)
이 안에서 오브젝트 풀 동작 방식을 구현하게되면 스포너를 만든 의미가 없어지게 되는 거였다.
그러니까 단일 책임 원칙을 위반하는 상황..!!
그래서 이 문제점을 해결하려면
1. 스폰한 프리팹의 자식으로 개별 동작을 수행하는 스크립트 제작
2. 스폰은 그냥 스폰만 담당하고 오브젝트 풀 관련된 로직은 지우기
그래서 내가 생각한 결론은
스포너가 오브젝트풀 매니저까지 참조할 필요는 없다고 판단되어서
스포너에 오브젝트풀 관련 로직들을 전부 삭제하는 방향으로 가기로 했다.
그리고 무엇보다
맵에 이상현상이 스폰될 때
이상현상 + 이상현상을 제거하는 아이템이 같이 스폰되어야 하기 때문에
스폰 매니저에서는 이상현상을 제거하는 아이템이 스폰되도록 바꿔주고
그 아이템이 스폰되면 오브젝트풀이나 그 이상현상 자체가 스폰되도록 바꿔주려고 한다.
이렇게 하면 아이템이 제거되었을 때 알아서 자식들도 삭제될테니 좋은 방법이지 않을까..?