Unity의 매니저 시스템을 활용한 게임 구조 관리

‎김현민·2024년 11월 21일

CapstoneDesign

목록 보기
2/2

최근 학교 졸업 프로젝트로 진행하는 <길거리 판매왕>은 각종 관리자(Managers)를 가지고 있다. 이를 보다 효율적으로 관리하고 씬(Scene) 전환 시 불필요한 자원을 정리하기 위해 필요한 매니저들을 동적으로 추가/제거하는 구조를 구현하였다.

초기 아이디어

보다시피 이렇게 관리자가 많고 이 관리자를 전부 싱글톤으로 관리하는 건 쉬운 일이 아니다. 씬 넘어갈때마다 삭제가 되지 않기 때문에 오류가 나기 십상이다.

이때 Managers 클래스를 만들어 게임 내 여러 시스템(NPC, UI, 플레이어 등)을 부착하고, Managers를 통해서만 다른 시스템에 접근할 수 있게하면 어떨까?

각 매니저의 생성, 접근, 삭제를 Managers에서 제어할 수 있으며 씬 전환 시에 생기는 오류를 잘 제어할 수 있다.

사용 예시

이는 Managers에 의해 호출하게 되는 매니저. 무슨 의미냐면 CityMap이 시작될때 해당하는 관리자들을 불러와서 부착해준다는 소리다.

이런 식으로 CityMap Scene을 시작하면 Managers 스크립트가 부착된 Managers 오브젝트 하위에 이런식으로 Manager가 자동 생성 및 부착된다.

1. Managers 클래스: 싱글톤을 활용한 매니저 관리

우선 각종 시스템들의 중심이 될 Managers 스크립트를 구현해보자.

1.1 싱글톤 구현

  • 싱글톤 패턴을 사용하여 Managers 클래스가 하나만 존재하도록 보장한다.
  • DontDestroyOnLoad를 통해 씬 전환 시에도 파괴되지 않는다.
private static Managers instance; 
public static Managers Instance 
{
    get
    {
        if (instance == null)
        {
            instance = FindObjectOfType<Managers>();

            if (instance == null)
            {
                GameObject singleton = new GameObject();
                instance = singleton.AddComponent<Managers>();
                singleton.name = typeof(Managers).ToString() + " (Singleton)";

                DontDestroyOnLoad(singleton);
            }
        }
        return instance;
    }
}

1.2 매니저 초기화

void Awake()
{
    Init();
    ManagersGO = transform.gameObject;

    if (instance._scene == null)
    {
        instance._scene = ManagersGO.AddComponent<SceneModeManager>();
    }
    if (instance._mission == null)
    {
        instance._mission = ManagersGO.AddComponent<MissionManager>();
    }
    // 필요에 따라 추가 매니저 초기화
}
  • Awake 함수는 초기화 될때마다 수행되는 작업이다. 이 함수에 필요한 매니저들을 동적으로 추가한다.

2. SceneModeManager 클래스: 씬 전환 및 동적 매니저 관리

씬 모드 매니저는 시작할때 현재 씬이 어떤 것인지 확인하고, 하위 매니저(UI, Cash, Concern 등등)를 동적으로 생성하거나 제거하는 역할을 한다.

2.1 씬 로드 이벤트 처리

OnEnable 메서드에서 SceneManager.sceneLoaded 이벤트를 추가하여 동작을 정의한다.

void OnEnable()
{
    SceneManager.sceneLoaded += OnSceneLoaded;
}

2.2 씬에 따른 매니저 초기화 및 동적 생성

이러면 씬 이름에 따라 필요한 매니저들이 초기화 된다.

void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    Managers.Instance.ClearChildManagers();
    switch (scene.name)
    {
        case "CityMap": 
            Awake_CityScene(); 
            curScene = Define.SceneMode.CityMap; 
            break;
        case "OfficeMap": 
            Awake_OfficeScene(); 
            curScene = Define.SceneMode.OfficeMap; 
            break;
        default: 
            break;
    }
}

예를 들어 씬이 시작되었을때 씬으 이름이 CityMap이라면 아래와 같은 함수가 함수가 호출될 것이다.

private void Awake_CityScene()
{
    Managers.Instance.AddPlayerManager();
    Managers.Instance.AddNPCManager();
    Managers.Instance.AddUIManager();
    Managers.Instance.AddTimeManager();
    Managers.Instance.AddCameraManager();
    Managers.Instance.AddChatManager();
    Managers.Instance.AddConvoManager();
    Managers.Instance.AddCashManager();
    Managers.Instance.AddInventoryManager();
    Managers.Instance.AddTurnManager();
}

이 코드에 따르면 CityMap 씬에서는 NPC, UI, Time, Camera, Chat... 등등 다양한 매니저가 Citymap에서 필요하다.

실제로 위에서 호출한 모든 매니저가 호출되는 것을 확인할 수 있다.

이렇게 되면 Script에서 해당 매니저를 편하게 사용할 수 있다. 사용 예시는 아래에 추가하겠다.

3. 매니저 제거

씬 전환 시, 이전 씬에서 사용했던 모든 매니저를 제거하여 메무리 누수를 방지한다.

public void ClearChildManagers()
{
    foreach (Transform child in transform)
    {
        Destroy(child.gameObject);
    }
}

4. 결론

  • 모든 매니저를 중앙에서 통합 관리가 가능.
  • 불필요한 메모리 사용 방지.
  • 리소스 관리에 효과적

위와 같은 장점이 있지만, 결국 매니저를 전부 삭제했다가 다시 부착하기 때문에 모든 씬에서 사용되는 음향 관리자와 같은 관리자는 따로 관리해주어야한다.

5. 사용 예시 : Concern Manager

해당 시스템을 어떻게 사용하는지 위에 두 Manager를 통해 알아보겠다.

5.1 Concern Manager: 플레이어의 고민과 아이템 연결

Concern Manager는 플레이어의 고민(Concern)과 아이템(Item)을 연결하는 시스템이다. 고민 데이터를 기반으로 고민에 해당하는 카테고리에서 적합한 아이템을 추천해준다.

5.1.1 주요 기능

  • 고민과 아이템 매칭: 고민의 카테고리와 같은 카테고리의 아이템을 연결
  • 랜덤 선택 로직: 다양한 고민과 아이템을 무작위로 조합하여 게임에 변화를 제공

5.1.2 기본 데이터 준비

우선 고민과 아이템에 대한 기본 데이터가 있어야 추출하여 사용할 수 있다. 고민과 아이템은 각각 concernData, ItemData 이름의 JSON 파일로 저장되어 있다.

concernData

{
  "concerns": [
    {
      "concern": "최근 스트레스를 받아 기분 전환이 필요하다고 함.",
      "Category": "디저트"
    },
    {
      "ObjID": 2,
      "ObjName": "시험이 끝남. 스스로에게 보상을 주고 싶다고 함.",
      "Category": "디저트"
    },
    {
      "ObjID": 3,
      "ObjName": "친구들과 모임을 가지기로 함. 분위기를 좋게 만들어줄 디저트가 필요함.",
      "Category": "디저트"
    },
    {
      "ObjID": 4,
      "ObjName": "SNS에서 봤는데 너무 맛있어 보임.",
      "Category": "디저트"
    },
    {
      "ObjID": 5,
      "ObjName": "힘든 일상에서 작은 행복을 찾고 싶음. 기분 좋은 디저트를 사고 싶음.",
      "Category": "디저트"
    },
    {
      "ObjID": 6,
      "ObjName": "힘든 일상에서 작은 행복을 찾고 싶음. 기분 좋은 디저트를 사고 싶음.",
      "Category": "디저트"
    },
    {
      "ObjID": 7,
      "ObjName": "힘든 일상에서 작은 행복을 찾고 싶음. 기분 좋은 디저트를 사고 싶음.",
      "Category": "디저트"
    },
    ...
   ]
}

고민 데이터는 위와 같은 형식으로 최근 고민과, 그와 연관된 물건 카테고리가 저장되어 있다.

itemData

{
  "items": [
    {
      "ObjID": 1,
       "ObjName": "탕후루",
      "ObjInfo": "",
      "Category": "디저트"
    },
    {
      "ObjID": 2,
      "ObjName": "케이크",
      "ObjInfo": "",
      "Category": "디저트"
    },
    {
      "ObjID": 3,
      "ObjName": "두바이 초콜릿",
      "ObjInfo": "",
      "Category": "디저트"
    },
    {
      "ObjID": 4,
      "ObjName": "티라미수 밤 케이크",
      "ObjInfo": "",
      "Category": "디저트"
    },
    {
      "ObjID": 5,
      "ObjName": "마카롱",
      "ObjInfo": "",
      "Category":"디저트"
    },
    {
      "ObjID": 6,
      "ObjName": "크렘 브륄레",
      "ObjInfo": "",
      "Category": "디저트"
    },
    ...
  ]
}

마찬가지로 아이템 데이터에는 해당 아이템의 이름과 해당하는 카테고리가 저장되어 있다.
이 데이터를 통해 디저트를 필요로하는 고민은 디저트 아이템과 매칭된다.

5.1.3 데이터 정제

DataManager에서는 JSON 데이터를 불러와 게임 내에서 사용하도록 리스트 형식으로 저장한다.

public void Init()
{
    itemList = LoadJson<ItemData, ItemInfo>("ItemData").GetList();
    concernList = LoadJson<ConcernData, ConcernInfo>("concernData").GetList();
}

Loader LoadJson<Loader, DataFormat>(string path) where Loader : ILoader<DataFormat>
{
    TextAsset textAsset = Resources.Load<TextAsset>($"Data/JsonFile/{path}");
    return JsonUtility.FromJson<Loader>(textAsset.text);
}
  • Init: JSON 데이터를 읽어와 각각의 리스트에 저장.
  • LoadJson: 리스트 함수로 JSON 파일을 읽고 객체로 변환.

5.1.4 고민과 아이템 매칭

concernManager

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class ConcernManager : MonoBehaviour
{
    private List<ConcernInfo> concerns;
    private List<ItemInfo> items;

    private HashSet<int> usedConcernIDs = new HashSet<int>();

    public string GetRandomConcernAndItem()
    {
        concerns = Managers.Data.concernList;
        items = Managers.Data.itemList;

        // Check if concerns list is empty
        if (concerns.Count == 0)
        {
            return "No concerns available.";
        }

        // Randomly select a concern
        var randomConcern = concerns[Random.Range(0, concerns.Count)];

        // Get the corresponding category
        string category = randomConcern.Category;

        // Filter items by the selected concern's category
        var itemsInCategory = items.Where(item => item.Category == category).ToList();

        if (itemsInCategory.Count == 0)
        {
            return "No items available in the selected category.";
        }

        // Randomly select an item
        var randomItem = itemsInCategory[Random.Range(0, itemsInCategory.Count)];

        // Return the formatted result
        return $"고민: {randomConcern.Concern} \n구매자가 사고자 하는 물건: {randomItem.ObjName}";
    }

}

  • GetRandomConcernAndItem: 매서드는 고민과 해당 카테고리 아이템을 매칭하여 출력해준다.

5.1.5 ConcernManager를 Managers에 부착

Managers 스크립트


    ConcernManager _concern;
    public static ConcernManager Concern { get { return Instance._concern; } }
    
    public void AddConcernManager()
    {
        GameObject concernManager = new GameObject("@ConcernManager");
        concernManager.transform.parent = transform;

        if (instance._concern == null)
        {
            instance._concern = concernManager.AddComponent<ConcernManager>();
        }
    }

위와 같이 ConcernManager를 호출해주고 AddConcernManager 함수를 만들어준다.

SceneModeManager 스크립트

   private void Awake_CityScene()
   {
       Managers.Instance.AddPlayerManager();
       Managers.Instance.AddNPCManager();
       Managers.Instance.AddUIManager();
       Managers.Instance.AddTimeManager();
       Managers.Instance.AddCameraManager();
       Managers.Instance.AddChatManager();
       Managers.Instance.AddConvoManager();
       Managers.Instance.AddCashManager();
       Managers.Instance.AddInventoryManager();
       Managers.Instance.AddTurnManager();
	   Managers.Instance.AddConcernManager(); //추가
       
   }

이제 CityMap이 시작될때마다 ConcernManager를 생성해 부착해줄것이다.


위와 같이 CityMap Scene을 로드하면 ConcernManager가 잘 부착된 것을 확인할 수 있다.

5.1.6 ConcernManager 다른 스크립트에서 활용하기

이제 ConcernManager를 싱글톤에 부착해주었기 때문에 다른 스크립트에서도 사용할 수 있게 되었다. 잘 작동되는지 임시로 버튼을 만들어 확인해본다.

임시로 만든 확인 버튼

임시로 만든 확인 버튼에 임시 스크립트를 부착해준다.

tempbuttonClicked script

using UnityEngine;
using UnityEngine.UI;

public class tempbuttonClicked : MonoBehaviour
{
    public Button myButton; // 버튼 연결용

    void Start()
    {
        // 버튼 클릭 이벤트에 메서드 추가
        if (myButton != null)
        {
            myButton.onClick.AddListener(OnButtonClick);
        }
    }

    void OnButtonClick()
    {
        // ConcernManager의 메서드 호출
        string result = Managers.Concern.GetRandomConcernAndItem();
        Debug.Log(result); // 결과를 디버그 로그로 출력
    }
}

이제 클릭할때마다 랜덤 고민과 아이템을 무작위로 맞춰준다.

이런 식으로 잘 작동하는 것을 확인할 수 있다.

5.2 삭제되는 것 확인

위에까지는 CityMap Scene에서의 이야기다. 이제 다른 씬으로 넘어갔을때 해당 매니저가 잘 삭제되는지 확인해보겠다.

이제 OfficeMap Scene으로 왔다.

인스펙터 창에서 CityMap에서의 Manager는 삭제되고 OfficeMap에서 필요한 PlayerManager 와 OfficeManager만 부착되어 있는 것을 확인할 수 있다.

profile
이것저것 하고 싶은 거 합니다

0개의 댓글