유니티 게임 팀 프로젝트를 진행하면서 PR을 날리고 서로 피드백을 주는 시스템을 진행하고 있습니다.
팀 프로젝트를 진행하던 중 팀원분이 GameObject.Find()에 대한 피드백을 해줬습니다.
작성한 코드에 대해서 한번 더 생각해보고 제가 처음에 의도했던 것이 무엇인지 정리하기 위해 이렇게 글을 작성하게 되었습니다.
팀원분이 위와 같이 피드백을 해줬습니다.(PR 원본 링크)
우선 피드백을 받은 개선이 필요한 코드는 아래와 같습니다.
using UnityEngine;
public class Managers : MonoBehaviour
{
private static Managers s_instance;
// Managers 객체를 전역(static)으로 처리하고 한 번 생성 되었던 s_instance 리턴
public static Managers Instance { get { Init(); return s_instance; } }
#region Managers
private UIManager _ui = new UIManager();
public static UIManager UI { get { return Instance._ui; } }
#endregion
void Start()
{
Init();
}
static void Init()
{
// s_instance가 null이면 객체 생성 로직 진행
if (s_instance == null)
{
// 피드백을 받은 GameObject.Find() 코드
GameObject gameObject = GameObject.Find("@Managers");
if (gameObject == null)
{
gameObject = new GameObject { name = "@Managers" };
gameObject.AddComponent<Managers>();
}
// Managers의 GaemObject가 Destroy 되지 않도록 처리
DontDestroyOnLoad(gameObject);
s_instance = gameObject.GetComponent<Managers>();
// Managers에 포함된 매니저들 초기화(Initialization)
// 각 매니저들의 초기 코드를 호출합니다.
s_instance._ui.Init();
}
}
// 씬 변경 시 각 매니저에서 삭제가 필요한 객체 혹은 GameObject를 정의해놓은 함수
public static void Clear()
{
// ex) UIManager에서 씬 이동 시 삭제할 GameObjcet들을 Clear()
s_instance._ui.Clear();
}
}
모든 매니저(Manager) 객체들을 생성해 싱글톤으로 관리하는 Managers라는 클래스입니다.
코드의 최초 의도는 아래와 같이 정리해봤습니다.
Managers.Instance.UI.ShowPopupUI();
전역적으로 처리되기 때문에 각 매니저에 정의된 메서드를 위와 같이 호출 할 수 있습니다.
정리하면 Managers 싱글톤 객체가 생성되면 다른 매니저들의 객체 생성과 초기화 코드가 연속적으로 진행되는 것이 장점입니다.
하지만 당연히 코드가 적고 소규모의 게임에는 괜찮겠지만,
매니저(Manager), GameObject들이 많아지면 문제가 발생할 수 있는 코드입니다.
개선해야 할 점과 우려되는 것들을 하나씩 정리해봤습니다.
GameObject.Find()는 상당히 직관적인 역할을 하는 함수입니다.
하지만 마이크로소프트 공식 문서(MS Docs)에도 해당 함수는 비용이 많이 드는 함수라고 정의되어 있습니다.
C#을 언어를 만든 회사에서도 권유하지 않는 방식이라 문제가 되는 것은 쉽게 인지했습니다.
하지만 실직적으로 어떤 부분이 문제가 되는지 좀 더 찾아봤습니다.
그래서 찾은 문제점이 잘 정리되어 있는 자료의 링크입니다.
(영문으로 된 자료입니다.)
핵심이 되는 부분을 변역기를 사용한 후 캡처한 사진입니다.
정리하면
위 글에는 GameObject.Find()를 Start() 함수에도 사용하지 말라고 당부합니다.
성능에 문제가 되고, 런타임 오류가 발생할 수 있는 코드를 한 번 호출한다고 안심하지 말라는 것 같습니다.
GameObject.Find()를 사용하지 말아야 할 이유를 명확하게 알게 되었습니다.
그러면 어떤 것을 대신 사용해야 할까요?
위 글에 어떤 대안 방법이 좋은 지 예제 코드와 함께 설명이 포함되어 있습니다.
꽤 많은 내용이 있어서 링크에 들어가서 확인해 보시는 것을 추천드립니다.
제일 큰 문제는 프로젝트의 규모가 커지면 Managers에 추가되는 매니저들도 점점 늘어날 것입니다.
그러면 Managers 싱글톤 객체 생성 시 초기로 실행되는 매니저들의 코드도 많아지게 됩니다.
다른 매니저들의 객체 생성과 초기 코드 실행에 대한 책임이 Managers에 너무 종속적인 것이 문제입니다.
Managers는 각 매니저들의 Init 코드를 전부 호출해줘야 합니다.
static void Init()
{
if (s_instance == null)
{
// ... Managers 생성 로직 생략 ...
s_instance._ui.Init();
s_instance._map.Init();
s_instance._sound.Init();
s_instance._resource.Init();
s_instance._data.Init();
// ... 계속 추가되는 Init() 함수 호출 ...
}
}
Managers와 다른 매니저들의 결합도로 인해 씬에서 필요 없는 매니저의 Init() 함수가 실행되는 문제도 발생할 수 있겠다고 생각했습니다.
각 매니저들의 Init() 혹은 Clear()는 각자의 책임으로 실행하게 하고,
매니저 객체의 생성도 필요한 부분에서 호출하는 방식으로 진행하는 방식으로 결정하게 되었습니다.
문제가 되는 코드는 아래와 같습니다.
#region Managers
private UIManager _ui = new UIManager();
public static UIManager UI { get { return Instance._ui; } }
#endregion
최초에 위와 같이 코드를 작성한 이유는 각 매니저들이 MonoBehaviour의 사용에 대한 필요성을 느끼지 못했기 때문입니다.
Managers에서 Start()함수를 통해 각 매니저들의 Init()을 진행하기 때문입니다.
사실 Managers에 매니저 객체를 생성하고 각 Manager에 MonoBeHaviour를 사용해도 작동은 합니다.
하지만 유니티 상에서 경고(Warnning)문구가 뜨게 됩니다.
그리고 MonoBehaviour가 필요한 매니저들은 제한사항이 생기기 때문에 개선해보기로 했습니다.
위 경고 문구와 MonoBehaviour 사용 제한에 대한 답변이 잘 작성된 링크입니다.
(영문으로 작성되어 있습니다.)
C#에서는 문제가 되지 않지만 유니티에서는 제한이 있어서 경고가 뜨는 것을 알 수 있습니다.
MyScript script = obj.AddComponent<MyScript>();
new를 사용해 직접 객체를 생성하는 것이 아닌 AddComponent를 사용하는 것을 권장하고 있습니다.
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
public static GameManager Instance { get { Init(); return _instance; } }
void Start()
{
Init();
}
static void Init()
{
if (_instance == null)
{
_instance = (GameManager)FindObjectOfType(typeof(GameManager));
if (_instance == null)
{
GameObject gameObject = new GameObject { name = "@GameManager" };
_instance = gameObject.AddComponent<GameManager>();
DontDestroyOnLoad(gameObject);
}
}
}
}
제거하고 보니까 Managers가 담당했던 책임이 대부분 사라졌습니다.
현재는 작성된 코드가 거의 없지만 GameManager에서 유저의 게임 시간과 같은 것들을 관리할 예정입니다.
쉽게 생각하고 사용성이 좋다고 적용했던 코드들이 문제의 원인이 된다는 것을 알게되었습니다.
어떤 함수를 사용할 때 다른 대안 방법이 없는지 알아보고 비교해 봐야 한다는 것을 알게되었습니다.
하지만 실제 해당 부분이 발생하는 문제를 직접 경험해보지 못했다는 것이 아쉽습니다.
다음에 기회가 된다면 실제 부하 테스트나 게임 오브젝트를 많이 늘려서 확인을 해보고 싶습니다.