[Unity] 오픈월드 제작기(4)

WowNo0607·2024년 11월 29일

OpenWorldGame

목록 보기
4/9

👾 몬스터 소환 로직 [심화]

이번 포스팅에서는 저번 오픈월드 제작기(3)에서 구현한 몬스터 소환로직에 좀 더 디테일을 더해봅시다.

설계

  1. 몬스터가 소환되는 위치가 일관적이지 않다.
    1. 다양한 공간으로 몬스터가 소환되도록 설정한다.
  2. 지역마다 몬스터가 소환되는 종류가 다양하다.

이와 같은 설계를 기반으로 오픈월드의 몬스터 소환 로직을 구현해 보겠습니다.

🔧 주요 요소

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;
}
  • 위 코드는 몬스터의 각종 정보들을 담아둔 class 입니다.
    • curCount를 제외한 모든 데이터를 Editor 내 Inspector를 통해 가져오도록 합니다.

SectorMonsterSpawner

  //해당 지역에 있는 몬스터 List
  public List<Monster> monsterObjects = new List<Monster>();
  
  //sector 마다 몬스터 소환 위치를 Inspector를 통해 따로 설정하기!
  public List<Transform> spawnPositions;
  
  //해당 스폰지역의 몬스터 소환 상태.
  private Dictionary<Transform, GameObject> monsterInPosition = new Dictionary<Transform, GameObject>();  
  • 먼저 위 두 List를 접근 지정자를 public으로 세팅한 이유는 일반적으로 게임 맵의 디자인들이 모두 다릅니다. 따라서 개발자가 Editor의 Inspector를 통해 유동적으로 몬스터의 다양한 정보와 위치를 세팅할 수 있도록 하기 위해 public으로 지정했습니다.
  • Dictionary<Transform, GameObject> monsterInPosition 개발자가 세팅해둔 spawnPostitions에 몬스터가 소환 되어있는지 체크하기 위한 Dictionary
  • List monsterObjects
  • List spawnPositions 해당 List는 Editor를 통해 몬스터들이 소환되는 고정 위치를 모아둔 List 입니다. 하지만 앞서 설계에서 언급한 것 처럼 같은 위치에서 소환되지 않도록 하기 위해 몬스터의 소환 정보를 군집의 형태로 두고 이 군집을 돌리거나 위치를 조금씩 수정하여 몬스터가 소환되는 위치를 조금씩 바꿔보도록 하겠습니다. 즉, List상의 위치 참조는 고정 위치가 아닌 소환되는 기준점이 되는 것입니다. 위 List를 아래와 같은 형태로 바꿔 봅시다.
    // public List<Transform> spawnPositions;
    public List<GameObject> spawnPositionGroup;
    • Editor 상의 spawnPositionGroup

      이렇게 참조항목을 단일 Object가 아닌 다양한 위치를 자식으로 가지고 있는 객체로 변경합니다.

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;
            }
        }
    }
}

Total Code

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

ObjectPooling

ObjectPooling에 대한 설명은 아래의 링크에서 정리되어있습니다.

[Link]

Monster Pool Dictionary

	//몬스터의 종류에 따른 Pooling들을 저장해두는 Dictionary
private Dictionary<string, Queue<GameObject>> monsterPoolDictionary = new Dictionary<string, Queue<GameObject>>();

앞서 Sector 별로 다양한 몬스터를 소환하는 방향으로 몬스터 로직을 설계했습니다.

ObjectPooling의 경우 Queue를 통해 몬스터 인스턴스들을 저장해 두는데 필요에 따라 원하는 몬스터를 가져오도록 하기 위해 몬스터의 종류에 따라 각각의 Queue를 가지고 있어야합니다.

Monster Reference Get

	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의 태그가 다르다고 가정]

Monster Return

  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);
  }

Total Code

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);
    }
}

그럼 위와 같은 코드들을 이용하여 Sector 별로 몬스터를 다르게 소환해 봅시다.

Sector(0, 1)

[파란색 바닥]

골렘 2마리, 버섯 1마리 (총 3마리)

Sector(0, 0)

[초록색 바닥]

골렘 1마리, 버섯 1마리 (총 2마리)

이처럼 아래 화면에 Inspector에서 세팅한 값에 맞도록 몬스터가 소환되는 것을 확인할 수 있습니다.

=> 절대 Pool CountIn Count를 헷갈리지 마세요~

Pool Count : Object Pooling으로 초기에 인스턴스화 할 요소의 수.
In Count : 특정 타입의 몬스터가 해당 구역에 존재해야하는 수.

이처럼 Inspector를 통해 몬스터 State를 각각 설정할 수 있습니다.

몬스터 소환 위치를 Group화 했을 때의 장점

  • 이후에 몬스터 소환 로직을 수정할 때, Position을 하나하나 설정해주는 것보다 Group단위로 일괄 처리를 하기 더 용이합니다.
  • 이후에 알게 될 몬스터의 움직임에 따른 충돌을 예방할 수 있습니다.[다음 포스팅에서...]
profile
안녕하세요

0개의 댓글