이번 포스팅에서는 저번 오픈월드 제작기(3)에서 구현한 몬스터 소환로직에 좀 더 디테일을 더해봅시다.
설계
이와 같은 설계를 기반으로 오픈월드의 몬스터 소환 로직을 구현해 보겠습니다.
Monster Class
[System.Serializable]
public class Monster
{
public int HP;
public int damage;
public float speed;
public int size;
public GameObject prefab;
public int inCount;
private int curCount;
public int poolCount;
public Monster(int HP, int size, int damage, float speed, GameObject prefab, int inCount, int poolCount)
{
this.HP = HP;
this.damage = damage;
this.speed = speed;
this.size = size;
this.prefab = prefab;
this.inCount = inCount; //현재 섹터에 해당 몬스터가 존재할 수 있는 최대 수
this.curCount = 0; //현재 섹터에 소환되어있는 몬스터의 수.
this.poolCount = poolCount; //오브젝트 풀링용
}
public void MinusCurCount() => curCount--;
public void PlusCurCount() => curCount++;
public bool CheckInCount() => curCount < inCount;
}
SectorMonsterSpawner //해당 지역에 있는 몬스터 List
public List<Monster> monsterObjects = new List<Monster>();
//sector 마다 몬스터 소환 위치를 Inspector를 통해 따로 설정하기!
public List<Transform> spawnPositions;
//해당 스폰지역의 몬스터 소환 상태.
private Dictionary<Transform, GameObject> monsterInPosition = new Dictionary<Transform, GameObject>();
// public List<Transform> spawnPositions;
public List<GameObject> spawnPositionGroup;
Inspector
위 사진에서 처럼 주변 환경에 걸맞네 몬스터의 정보를 넣어 소환 로직을 구현하면 됩니다.
private void SpawnInitialMonsters()
{
foreach (Monster monsterType in monsterObjects)
{
while (monsterType.CheckInCount()) SpawnMonster(monsterType);
}
}
public void SpawnMonster(Monster monsterType)
{
foreach (GameObject posGroup in spawnPositionGroup)
{
GameObject[] group = posGroup.GetComponentsInChildren<GameObject>();
foreach (GameObject spawnPos in group)
{
if (monsterInPosition[spawnPos.transform] == null)
{
GameObject monster = objectPooling.GetObject(monsterType.prefab.tag, spawnPos.transform.position, Quaternion.identity);
if (monster != null)
{
monsterInPosition[spawnPos.transform] = monster;
monsterType.PlusCurCount();
}
return;
}
}
}
}
public class SectorMonsterSpawner : MonoBehaviour
{
public List<Monster> monsterObjects = new List<Monster>();
public List<GameObject> spawnPositionGroup;
private Dictionary<Transform, GameObject> monsterInPosition= new Dictionary<Transform, GameObject>();
private ObjectPooling objectPooling;
private bool isSpawning;
public Vector2Int sectorVec;
private void Awake()
{
objectPooling = GetComponent<ObjectPooling>();
foreach (GameObject posGroup in spawnPositionGroup)
{
int ran = Random.Range(0, 360);
posGroup.transform.Rotate(0, ran, 0);
GameObject[] group= posGroup.GetComponentsInChildren<GameObject>();
foreach(GameObject spawnPos in group)
{
monsterInPosition[spawnPos.transform] = null;
}
}
objectPooling.InitializeMonsterPool(monsterObjects);
}
void Start()
{
isSpawning = false;
}
private void SpawnInitialMonsters()
{
foreach (Monster monsterType in monsterObjects)
{
while (monsterType.CheckInCount()) SpawnMonster(monsterType);
}
}
public void SpawnMonster(Monster monsterType)
{
foreach (GameObject posGroup in spawnPositionGroup)
{
GameObject[] group = posGroup.GetComponentsInChildren<GameObject>();
foreach (GameObject spawnPos in group)
{
if (monsterInPosition[spawnPos.transform] == null)
{
GameObject monster = objectPooling.GetObject(monsterType.prefab.tag, spawnPos.transform.position, Quaternion.identity);
if (monster != null)
{
monsterInPosition[spawnPos.transform] = monster;
monsterType.PlusCurCount();
}
return;
}
}
}
}
public void DespawnMonsterTotal()
{
foreach(var monster in monsterInPosition)
{
if(monster.Value != null)
{
objectPooling.ReturnObject(monster.Value);
}
}
monsterInPosition.Clear();
}
public void OnPlayerEnter()
{
if (!isSpawning)
{
SpawnInitialMonsters();
isSpawning = true;
}
}
public void OnPlayerExit()
{
if (isSpawning)
{
DespawnMonsterTotal();
isSpawning = false;
}
}
private void OnEnable()
{
isSpawning = false;
MonsterManager.instance.AddMonsterSpawnerSC(sectorVec, this);
}
private void OnDisable()
{
isSpawning = false;
MonsterManager.instance.RemoveMonsterSpawnerSC(sectorVec);
}
}
ObjectPooling
ObjectPoolingObjectPooling에 대한 설명은 아래의 링크에서 정리되어있습니다.
[Link]
//몬스터의 종류에 따른 Pooling들을 저장해두는 Dictionary
private Dictionary<string, Queue<GameObject>> monsterPoolDictionary = new Dictionary<string, Queue<GameObject>>();
앞서 Sector 별로 다양한 몬스터를 소환하는 방향으로 몬스터 로직을 설계했습니다.
ObjectPooling의 경우 Queue를 통해 몬스터 인스턴스들을 저장해 두는데 필요에 따라 원하는 몬스터를 가져오도록 하기 위해 몬스터의 종류에 따라 각각의 Queue를 가지고 있어야합니다.
public void InitializeMonsterPool(List<Monster> monsterList)
{
foreach (Monster monster in monsterList)
{
Queue<GameObject> pool = new Queue<GameObject>();
for (int i = 0; i < monster.poolCount; i++)
{
GameObject instance = Instantiate(monster.prefab);
instance.name = monster.prefab.tag;
instance.transform.SetParent(monsters);
instance.SetActive(false);
pool.Enqueue(instance);
}
monsterPoolDictionary[monster.prefab.tag] = pool;
}
}
SectorMonsterSpawner로 부터 설계된 몬스터의 List를 받아와 해당 몬스터의 Pool을 만듭니다. Dictionary의 Key는 편의상 몬스터의 Tag로 만들었습니다. [모든 몬스터 Prefab의 태그가 다르다고 가정]
public GameObject GetObject(string tag, Vector3 position, Quaternion rotation)
{
if (monsterPoolDictionary.ContainsKey(tag) && monsterPoolDictionary[tag].Count > 0)
{
GameObject instance = monsterPoolDictionary[tag].Dequeue();
instance.transform.position = position;
instance.transform.rotation = rotation;
instance.SetActive(true);
return instance;
}
return null;
}
public void ReturnObject(GameObject monster)
{
monster.SetActive(false);
string tag = monster.tag;
monsterPoolDictionary[tag].Enqueue(monster);
}
public class ObjectPooling : MonoBehaviour
{
private Dictionary<string, Queue<GameObject>> monsterPoolDictionary = new Dictionary<string, Queue<GameObject>>();
public Transform monsters;
public void InitializeMonsterPool(List<Monster> monsterList)
{
foreach (Monster monster in monsterList)
{
Queue<GameObject> pool = new Queue<GameObject>();
for (int i = 0; i < monster.poolCount; i++)
{
GameObject instance = Instantiate(monster.prefab);
instance.name = monster.prefab.tag;
instance.transform.SetParent(monsters);
instance.SetActive(false);
pool.Enqueue(instance);
}
monsterPoolDictionary[monster.prefab.tag] = pool;
}
}
public GameObject GetObject(string tag, Vector3 position, Quaternion rotation)
{
if (monsterPoolDictionary.ContainsKey(tag) && monsterPoolDictionary[tag].Count > 0)
{
GameObject instance = monsterPoolDictionary[tag].Dequeue();
instance.transform.position = position;
instance.transform.rotation = rotation;
instance.SetActive(true);
return instance;
}
return null;
}
public void ReturnObject(GameObject monster)
{
monster.SetActive(false);
string tag = monster.tag;
monsterPoolDictionary[tag].Enqueue(monster);
}
}
[파란색 바닥]

골렘 2마리, 버섯 1마리 (총 3마리)
[초록색 바닥]

골렘 1마리, 버섯 1마리 (총 2마리)
이처럼 아래 화면에 Inspector에서 세팅한 값에 맞도록 몬스터가 소환되는 것을 확인할 수 있습니다.
=> 절대 Pool Count와 In Count를 헷갈리지 마세요~

Pool Count : Object Pooling으로 초기에 인스턴스화 할 요소의 수.
In Count : 특정 타입의 몬스터가 해당 구역에 존재해야하는 수.
이처럼 Inspector를 통해 몬스터 State를 각각 설정할 수 있습니다.
몬스터 소환 위치를 Group화 했을 때의 장점
- 이후에 몬스터 소환 로직을 수정할 때, Position을 하나하나 설정해주는 것보다 Group단위로 일괄 처리를 하기 더 용이합니다.
- 이후에 알게 될 몬스터의 움직임에 따른 충돌을 예방할 수 있습니다.[다음 포스팅에서...]