본문 바로가기

개발/Unity

[Unity] Merge-Is-Mine 맵 생성 시스템

선 요약

8번출구류 게임의 반복되는 맵 시스템을 2d구조에서 구현해주기 위해 

map을 최적화하여 관리할 수 있도록 진행하는 시스템 제작

map이 생성되는 규칙을 쉽게 유지보수하기 위한 시스템 구현

 

* 구현 영상은 해당 글 가장 밑부분에서 확인할 수 있습니다.


구현목표

기본 map flow

구현하려는 기본 map flow는 위와같다.

 

1. 원래의 상태를 보여주는 main map

2. main map에서 사물등의 위치변화를 주어 만든 quiz map

 

quiz map에서 오답을 고르면

다시 main map을 보여주고, quiz map을 다시 입장하게 된다.

 

그리고 이 main map variation이 여러개이면 다음과같은 flow가 나온다

최종 map flow전체

따라서 이를 구현해주기 위해서 필요한 map의 종류는 다음과 같다.

 

1. main map

2. main과 quiz map 사이를 이어주는 복도맵

3. quiz map

4. clear map

 

main map과 quiz map은 하나의 map prefab으로 관리한 뒤,

map내에 사물위치의 variation을 주는 방법으로 해결을 할 수 있다.

따라서 3종류의 map만 구현해주면되는데,

 

이 map들을 어떻게 관리를 해야할지 의문이생기게된다.

 

 


구현사항

위와같은 flow로 map을 구성하기 위해서는 우선 몇개의 map이 생성이 되어야 문제가 없을까?

를 생각해보았다.

 

위와같이 진행을 하게되면 복도 중간에서, 양쪽 main map, quiz map을 볼 때, 최적화를 위해 이전에 지나온 map을 아예 지워버린다면 map이 끊겨보여 부자연스러울 것이다.

 

따라서 map은 진행하며 초기상태를 제외하고, 최대 3개 활성화가 진행되어야한다.

 

 

그렇다면, map을 구성할 때 각 map마다 특정 지점에 다가갈 때, 다음 map을 생성하도록 한다면 어떨까?

 

map을 unity 내 함수인 Instantiate 함수를 통해서 생성을 해주게 되면, 런타임에서 버벅임이 발생하였다.

 

이를 해결해주기위해

오브젝트풀링 기법을 사용하여 게임 시작 시 한번에 맵을 생성함으로서 게임진행중 맵이 생성되며 걸리는 버벅임을 제거하는것이다.

 

단, 풀링 방식을 구현하되, map의 유형별로 각 풀이 관리될 수 있도록 구현을 진행해줄 것이다.


RoomBuffer

public struct RoomBuffer
{
    public List<GameObject> rooms;

    ...

    public void AddBuffer(GameObject room)
    {
        if (bufferAmount > rooms.Count)
        {
            rooms.Add(room);
            room.SetActive(false);
            room.transform.position = savingPos;
        }
        else
        {
            Debug.LogWarning("RoomBuffer 크기 초과로 추가 실패 :" + room);
        }
    }
}

이를 위해 우선 각 유형의 room 마다 관리를 진행해주기 위해 RoomBuffer라는 class를 제작하였다.

 

 

public class RoomSequence : ScriptableObject
{

    public List<RoomBuffer> MainMapBuffers;

    public List<GameObject> MainMapPrefabs;


    public void Init()
    {
        MainMapBuffers = new List<RoomBuffer>();
        

        //map정보 등록
        for (int i = 0; i < MainMapPrefabs.Count; i++)
        {
            for (int j = 0; j < roomBuffer.bufferAmount; j++)
            {
                roomBuffer.AddBuffer(Instantiate(MainMapPrefabs[i], saving_area, Quaternion.identity));
            }
        }
        
    }
}

이후 main map의 variation개수만큼, 버퍼에 해당 map을 미리 생성하여 넣어주는 것을 확인할 수 있다.

 

이때, 각 map별로 버퍼의 개수만큼 생성을 진행해주고 있는데,

위의 사진과 같이

main map - corridor - quiz map

으로 구성될 경우

같은 유형의 map이 2개이상 한번에 존재해야 하기 때문에

같은 유형의 map을 여러개 담고 있는 room buffer라는 class를 만든것이다.

 

 

최종적인 room buffer의 구조를 살펴보면 다음과 같다.

public struct RoomBuffer
{
    public List<GameObject> rooms;
	
    //초기위치
    public Vector3 savingPos;

    public int bufferAmount;

    public string MapId;

    public void Init(int amount)
    {
        bufferAmount = amount;
        rooms = new List<GameObject>();
    }

    public void AddBuffer(GameObject room)
    {
        if (bufferAmount > rooms.Count)
        {
            rooms.Add(room);
            room.SetActive(false);
            room.transform.position = savingPos;
        }
        else
        {
            Debug.LogWarning("RoomBuffer 크기 초과로 추가 실패 :" + room);
        }
    }


    //활성화 되지 않아있는 room을 찾아 반환
    public GameObject GetRoom()
    {
        for (int i = 0; i < rooms.Count; i++)
        {
            if (!rooms[i].activeSelf)
            {
                return rooms[i];
            }
        }

        return null;
    }
}

한 유형의 room을 여러개 생성하여 담고 있으며

 

GetRoom()함수를 통해서 현재 활성화되어있지 않은 room한개를 반환하는 구조이다.

 


RoomSequence

이 버퍼를, RoomSequence라는 class가 담고 있는데,

RoomSequence는 다음 기능을 제공한다.

 

1. FindMap(string mapID) : Map의 id를 입력받아, 해당하는 map을 버퍼에서 반환

 

2. SubClear(bool ison) : Map을 clear하였을 때, 호출 진행, 호출된 것이 quiz map이었을 경우, 다음 생성될 main variation을 교체

 

public class RoomSequence : ScriptableObject
{
	public GameObject FindMap(string mapId)
    {
        if (mapId == _corriderMapId)
        {
            return CorriderBuffers.GetRoom();//CorriderPrefab;
        }
        else if (mapId == _finalMapId)
        {
            return InstantFinalMap;//FinalMapPrefab;
        }
        else
        {
            for (int i = 0; i < MainMapBuffers.Count; i++)
            {
                if (MainMapBuffers[i].MapId == mapId)
                {
                    return MainMapBuffers[i].GetRoom();
                }
            }


            Debug.LogError("Map ID is not found : " + mapId + "\n in RoomSequence.FindMap(string mapId)");
            return null;
        }
    }

    public void SubClear(bool ison)
    {
        if (!ison)
        {
            return;
        }

        _curCount++;

        if (_curCount >= SubCount)
        {
            _curCount = -1;
            _Mapiterate++;

            if (_Mapiterate >= MainMapPrefabs.Count)
            {
                CurrentMain = FinalMapPrefab;
            }
            else
            {
                CurrentMain = MainMapPrefabs[_Mapiterate];
            }
        }
    }
}

 

따라서 정리하자면, RoomSequence는 현재 생성할 map을 id기반으로 찾아 제공해주고, 클리어 시 다음 main map을 생성해줄 수 있도록 하는 역할을 한다.

 

RoomSequence는 ScriptableObject로 구성되어있어,

각 RoomSequence를 생성하고, 생성될 main map, corridor등을 어떤 prefab을 기반으로 생성할지 커스텀해줄 수 있다.

room sequence예시

위와같이 Main Map variation 5가지를 구성하고, 마지막 맵, map과 map사이 중간 Corridor map, 

그리고 처음 생성해줄 Map을 Prefab으로 등록하여

이를 받아들이는 MapController는 호출만 진행하도록하였다.

 

이를 통해서 map의 순서관리와 유지보수를 매우 손쉽게 진행할 수 있다.


MapController

MapController는 2가지 기능을 제공한다.

 

1. RoomSequence에서 받은 map배치

2. 활성화 되어있는 room의 수 조절

 

public class MapController : MonoBehaviour
{


    [SerializeField]
    private List<GameObject> ActiveRoomList;

    [SerializeField]
    private RoomSequence _roomSequence;




    public void LoadMap(bool isCor)
    {
        string id = (isCor) ? _roomSequence.GetCorId() : _roomSequence.CurrentMain.GetComponent<RoomInfo>().MapId;

        GameObject instant = _roomSequence.FindMap(id);
        
        
        instant.transform.position = instantPos;
        instant.SetActive(true);
        
        ...
        
        ActiveRoomList.Add(instant);
        RoomOptimize();
        
    }
    
    private void RoomOptimize()
    {
    	if (ActiveRoomList.Count >= MaxActvieRoom)
        {
    		GameObject oldRoom = ActiveRoomList[0];

            ActiveRoomList.RemoveAt(0);


            if (oldRoom != null && oldRoom.gameObject != null)
            {
                oldRoom.SetActive(false);
                oldRoom.transform.position = _roomSequence.saving_area;
            }
            
        }
    
    }
}

간단히 정리해보면 위와 같다.

 

LoadMap 함수에서는,

RoomSequence에서 현재 배치해줄, Map의 정보를 가져오고, 이를 배치한다.

 

Map을 배치할 위치는, 현재 생성되어 누적된 Map들의 길이를 더하여, 생성될 위치를 계산해준다.

 

생성이후, 활성화되어있는 Room의 개수가 일정수를 초과하면 가장 이전에 생성되었던 Map을 비활성화시켜준다.

 

 

 

그러면 왜 굳이 비활성화를 시켜줄까?

기본 map flow

 

처음 보았던 다음 flow를 살펴보면된다.

 

quiz map에서 계속 오답을 진행할 시, 같은 유형의 map을 계속 오른쪽으로 이어붙여줘야하는 상황이 발생한다.

그런데, 이상황에서 사용이 끝난 map을 active false하여 버퍼에 반환을 해주지 않을경우

//활성화 되지 않아있는 room을 찾아 반환
    public GameObject GetRoom()
    {
        for (int i = 0; i < rooms.Count; i++)
        {
            if (!rooms[i].activeSelf)
            {
                return rooms[i];
            }
        }

        return null;
    }

room sequence의 다음 함수에서

 

결국 모든 room이 활성화되어있어, null을 반환하는 상황이 일어나기 때문에,

항상 전부 사용한 map은 active false를 해두어 buffer가 map을 재사용할 수 있도록 해주어야한다.

 

이 과정에서 map을 새로 생성해주지 않고, 이전에 사용한 map을 재사용함으로서,

map을 새로 생성하는것의 부담을 줄이고, 같은 map을 여러번 지나갈 수 있게한다.

 

 

 

 

 

 

 

//최종 class diagram