Unity에서 SRDebugger와 VContainer로 디버그 메뉴를 간단하고 안전하게 구현하는 방법을 소개합니다. 트리거 설정, MVP 구조에서의 적용, Scripting Define/asmdef 고려사항과 공개 메서드 관련 과제까지 다룹니다.
Unity로 게임을 개발할 때 디버그용 메뉴를 만드는 일은 흔합니다. 에디터에서는 에디터 확장으로 만들 수도 있고, 실기기에서도 쓰고 싶다면 전용 화면을 만들기도 하죠. 다만 디버그 메뉴는 잘못 설계하면 코드 곳곳에 #if UNITY_EDITOR 등을 흩뿌려야 하는 등, 의외로 설계력이 요구되는 영역입니다.
이번 글에서는 SRDebugger와 VContainer를 사용해 디버그 기능을 간단하고 안전하게 구현한 사례를 소개합니다.
SRDebugger와 VContainer는 이미 도입되어 있다는 전제에서 설명합니다. 자세한 내용은 각 문서를 참고하세요.
SRDebugger
디버그 기능을 간편하고 높은 편의성으로 추가할 수 있는 유료 에셋입니다
VContainer
또한, 본 글에서는 디버그용 Scripting Define Symbol로 !DISABLE_SRDEBUGGER 를 사용합니다.
DISABLE_SRDEBUGGER 는 SRDebugger의 asmdef에서 이 심볼이 활성화되면 SRDebugger가 제외되도록 되어 있습니다. 이번 구현은 SRDebugger에 크게 의존하므로 이를 그대로 활용합니다.

SRDebugger는 기본적으로 화면의 좌상단을 트리플 탭하면 표시됩니다.
다른 모서리도 설정에서 선택할 수 있지만, 제 환경에서는 호출이 간편하고 다른 조작과 충돌하기 어려운 방식으로 4손가락 탭으로 빠르게 열 수 있도록 구현했습니다.
SRDebuggerManager.cs 를 만들어 관리합니다.
#if !DISABLE_SRDEBUGGER
using System;
using SRDebugger;
using UnityEngine;
using VContainer.Unity;
public class SRDebuggerManager : IStartable, ITickable, IDisposable
{
readonly DynamicOptionContainer _optionContainer = new();
bool _prevTriggered;
public void Start()
{
if (!SRDebug.IsInitialized)
{
SRDebug.Init();
}
SRDebug.Instance.AddOptionContainer(_optionContainer);
// (예) 기본 옵션 추가
_optionContainer.AddOption(
OptionDefinition.FromMethod("디버그 메서드1 실행", ExecuteDebugMethod1, "공통 메뉴", int.MaxValue)
);
_optionContainer.AddOption(
OptionDefinition.FromMethod("디버그 메서드2 실행", ExecuteDebugMethod2, "공통 메뉴", int.MaxValue)
);
void ExecuteDebugMethod1()
{
// 디버그 처리1
}
void ExecuteDebugMethod2()
{
// 디버그 처리2
}
}
public void Tick()
{
var currentTouchCount = Input.touchCount;
if (currentTouchCount < 4)
{
_prevTriggered = false;
return;
}
if (_prevTriggered)
{
return;
}
_prevTriggered = true;
if (SRDebug.Instance.IsDebugPanelVisible)
{
SRDebug.Instance.HideDebugPanel();
}
else if (IsActiveSrDebuggerTrigger())
{
SRDebug.Instance.ShowDebugPanel(DefaultTabs.Options);
}
}
public void Dispose()
{
SRDebug.Instance.RemoveOptionContainer(_optionContainer);
}
static bool IsActiveSrDebuggerTrigger()
{
// 특정 화면을 열었을 때 SRDebugger를 표시하지 않는 등의 제어를 여기에 작성
return true;
}
}
#endif
SRDebuggerManager는 뒤에 나올 예시의 LifetimeScope에서 Register합니다.
또한 에디터에서는 SRDebugger 설정에서 ↑→↓← 키로 열리도록 하는 걸 개인적으로 추천하지만, 키 이동을 사용하는 게임이라면 다른 방법이 나을 수 있습니다.

아래와 같이 Presenter가 Model과 View의 다리를 놓고, View와 Model이 느슨하게 결합된 MVP 구성이 VContainer로 구축되어 있다고 가정합니다.
public class MyPresenter
{
private readonly MyView _view;
private readonly MyModel _model;
public MyPresenter(MyView view, MyModel model)
{
_view = view;
_model = model;
}
public void AddHp()
{
_model.Hp += 10;
_view.UpdateHp(_model.Hp);
}
}
public class MyView : MonoBehaviour
{
public void UpdateHp(int hp)
{
Debug.Log(hp);
}
}
public class MyModel
{
public int Hp;
}
여기에 SRDebugger의 디버그 메뉴를 추가해 Presenter의 AddHp 메서드를 실행해 보겠습니다.
디버그 메뉴는 독립된 클래스로, VContainer로 쉽게 떼었다 붙였다 할 수 있도록 구현합니다.
#if !DISABLE_SRDEBUGGER
using System;
using SRDebugger;
using VContainer.Unity;
public class MyDebugOption : IStartable, IDisposable
{
const string CategoryName = "MyDebugOption";
readonly DynamicOptionContainer _dynamicOptionContainer = new();
readonly private MyPresenter _presenter;
public MyDebugOption(MyPresenter presenter)
{
_presenter = presenter;
}
public void Start()
{
SRDebug.Instance.AddOptionContainer(_dynamicOptionContainer);
var optionDefinition = OptionDefinition.FromMethod(
"실행",
_presenter.AddHp,
CategoryName,
0
);
_dynamicOptionContainer.AddOption(optionDefinition);
// 다른 디버그 메뉴를 추가하고 싶다면 _dynamicOptionContainer.AddOption 으로 계속 추가
}
public void Dispose()
{
SRDebug.Instance.RemoveOptionContainer(_dynamicOptionContainer);
}
}
#endif
LifetimeScope는 아래와 같습니다. 이번에는 예시로 SRDebuggerManager도 포함했습니다.
using UnityEngine;
using VContainer;
using VContainer.Unity;
public class MyLifetimeScope : LifetimeScope
{
[SerializeField]
private MyView _view;
protected override void Configure(IContainerBuilder builder)
{
#if !DISABLE_SRDEBUGGER
builder.RegisterEntryPoint<SRDebuggerManager>();
builder.RegisterEntryPoint<MyDebugOption>();
#endif
builder.Register<MyPresenter>(Lifetime.Scoped);
builder.Register<MyModel>(Lifetime.Scoped);
builder.RegisterComponent(_view);
}
}
이제 씬에 MyLifetimeScope 와 MyView 를 붙인 오브젝트를 배치해 재생합니다.
최종적으로 아래와 같은 모습이 됩니다.

VContainer를 사용하면 MyLifetimeScope의 Register를 추가하는 것만으로 Model, View, Presenter 클래스의 변경을 최소화하면서 디버그 메뉴를 붙일 수 있습니다.
이 구현으로, MyLifetimeScope가 빌드되면 MyView용 디버그 메뉴가 옵션에 추가되고, MyLifetimeScope가 파기될 때 디버그 메뉴도 사라지는 동작을 실현할 수 있습니다.
VContainer가 도입되지 않은 평범한 환경이라도, MonoBehaviour의 Start와 OnDestroy 등으로 생명주기를 관리하면 유사하게 옵션을 추가할 수 있을 것입니다.
이러한 설계가 좋아 보이지만, 과제가 몇 가지 있습니다.
디버그 기능을 구현할 때, 지나치게 신경 쓰지 않아도 본番 환경에 들어가지 않는 장치가 되어 있으면 좋습니다.
#if !DISABLE_SRDEBUGGER 를 빠뜨려서 들어가 버린다거나, 프로덕션용 컴파일이 실패한다거나 하는 실수는 흔합니다. 가능하면 안 써도 되는 환경이면 좋겠죠.
좋은 방법이 없을까 고민해 봤지만, 결국 어딘가에는 써야 하므로, 의존 관계를 명확히 하기 위해서도 LifetimeScope에는 쓰는 편이 낫겠다는 소감입니다.
asmdef 단위로 나누면 Editor 폴더처럼 가장 안전하게 분리할 수 있지만, LifetimeScope 클래스가 Debug 클래스와 MVP 클래스를 모두 참조할 필요가 있어 asmdef로 나누려면 LifetimeScope/MVP 클래스/Debug 클래스를 각각 별도의 asmdef로 분리해야 합니다.
레이어 단위로 이렇게 asmdef를 나누고 있다면 asmdef로 분리함으로써 마침내 #if !DISABLE_SRDEBUGGER 에서 벗어날 수 있겠지만, 제 환경에서는 기능 단위로 asmdef를 자르고 싶었기 때문에, Debug 기능 구현을 위해 그만큼 세분화하는 비용이 더 커서 그만두었습니다.
제 환경에서는 프로덕션 빌드 시 SRDebugger를 폴더째 삭제하도록 했기 때문에, SRDebugger를 사용해 구현된 클래스가 혹시 프로덕션 빌드에 남아 있더라도 그 시점에서 컴파일 에러가 나도록 했고, 이것이 제 환경에서의 타협점이었습니다.
(그래서 클래스 자체를 #if !DISABLE_SRDEBUGGER 로 감쌌습니다)
이 부분은 디버그 처리이기도 하고, 리플렉션을 사용하는 등으로 해결하면 될 것 같습니다.
디버그하기 쉽도록 적절히 메서드를 분리하는 등의 설계도 중요하겠죠.
void ExecutePresenterMethod()
{
var method = typeof(MyPresenter).GetMethod("AddHp", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
method?.Invoke(_presenter, null);
}
디버그 기능을 갖추면 개발과 동작 확인의 효율이 크게 오릅니다. 따라서 디버그 기능을 추가하는 심리적 허들과 비용을 낮추기 위해 초기 단계부터 설계해 두는 것이 중요하다고 생각합니다.
이 글이 구현 아이디어에 도움이 되었으면 합니다.