최근 학교 졸업 프로젝트로 진행하는 <길거리 판매왕>은 각종 관리자(Managers)를 가지고 있다. 이를 보다 효율적으로 관리하고 씬(Scene) 전환 시 불필요한 자원을 정리하기 위해 필요한 매니저들을 동적으로 추가/제거하는 구조를 구현하였다.
보다시피 이렇게 관리자가 많고 이 관리자를 전부 싱글톤으로 관리하는 건 쉬운 일이 아니다. 씬 넘어갈때마다 삭제가 되지 않기 때문에 오류가 나기 십상이다.
이때 Managers 클래스를 만들어 게임 내 여러 시스템(NPC, UI, 플레이어 등)을 부착하고, Managers를 통해서만 다른 시스템에 접근할 수 있게하면 어떨까?
각 매니저의 생성, 접근, 삭제를 Managers에서 제어할 수 있으며 씬 전환 시에 생기는 오류를 잘 제어할 수 있다.

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

이런 식으로 CityMap Scene을 시작하면 Managers 스크립트가 부착된 Managers 오브젝트 하위에 이런식으로 Manager가 자동 생성 및 부착된다.
우선 각종 시스템들의 중심이 될 Managers 스크립트를 구현해보자.
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;
}
}
void Awake()
{
Init();
ManagersGO = transform.gameObject;
if (instance._scene == null)
{
instance._scene = ManagersGO.AddComponent<SceneModeManager>();
}
if (instance._mission == null)
{
instance._mission = ManagersGO.AddComponent<MissionManager>();
}
// 필요에 따라 추가 매니저 초기화
}
씬 모드 매니저는 시작할때 현재 씬이 어떤 것인지 확인하고, 하위 매니저(UI, Cash, Concern 등등)를 동적으로 생성하거나 제거하는 역할을 한다.
OnEnable 메서드에서 SceneManager.sceneLoaded 이벤트를 추가하여 동작을 정의한다.
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
이러면 씬 이름에 따라 필요한 매니저들이 초기화 된다.
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에서 해당 매니저를 편하게 사용할 수 있다. 사용 예시는 아래에 추가하겠다.
씬 전환 시, 이전 씬에서 사용했던 모든 매니저를 제거하여 메무리 누수를 방지한다.
public void ClearChildManagers()
{
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
}
위와 같은 장점이 있지만, 결국 매니저를 전부 삭제했다가 다시 부착하기 때문에 모든 씬에서 사용되는 음향 관리자와 같은 관리자는 따로 관리해주어야한다.
해당 시스템을 어떻게 사용하는지 위에 두 Manager를 통해 알아보겠다.
Concern Manager는 플레이어의 고민(Concern)과 아이템(Item)을 연결하는 시스템이다. 고민 데이터를 기반으로 고민에 해당하는 카테고리에서 적합한 아이템을 추천해준다.
우선 고민과 아이템에 대한 기본 데이터가 있어야 추출하여 사용할 수 있다. 고민과 아이템은 각각 concernData, ItemData 이름의 JSON 파일로 저장되어 있다.
{
"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": "디저트"
},
...
]
}
고민 데이터는 위와 같은 형식으로 최근 고민과, 그와 연관된 물건 카테고리가 저장되어 있다.
{
"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": "디저트"
},
...
]
}
마찬가지로 아이템 데이터에는 해당 아이템의 이름과 해당하는 카테고리가 저장되어 있다.
이 데이터를 통해 디저트를 필요로하는 고민은 디저트 아이템과 매칭된다.
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);
}
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}";
}
}
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 함수를 만들어준다.
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가 잘 부착된 것을 확인할 수 있다.
이제 ConcernManager를 싱글톤에 부착해주었기 때문에 다른 스크립트에서도 사용할 수 있게 되었다. 잘 작동되는지 임시로 버튼을 만들어 확인해본다.

임시로 만든 확인 버튼

임시로 만든 확인 버튼에 임시 스크립트를 부착해준다.
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); // 결과를 디버그 로그로 출력
}
}
이제 클릭할때마다 랜덤 고민과 아이템을 무작위로 맞춰준다.

이런 식으로 잘 작동하는 것을 확인할 수 있다.
위에까지는 CityMap Scene에서의 이야기다. 이제 다른 씬으로 넘어갔을때 해당 매니저가 잘 삭제되는지 확인해보겠다.
이제 OfficeMap Scene으로 왔다.

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