5. 핵심 개념

모든 CGS 서비스 뒤에 있는 다섯 가지 아이디어 — 부트스트랩, 서비스 로케이터, 클럭, 이벤트, 널 구현 — 와 용어집.

다섯 가지 아이디어가 프레임워크 전체를 설명합니다. 이것만 알면 23개 서비스 하나하나가 모두 같은 방식으로 동작합니다. 이 장은 각 아이디어를 쉬운 말로 다루며, 프로젝트에 붙여 넣을 수 있는 짧은 코드를 함께 제공합니다. 마지막의 용어집은 이 매뉴얼에 쓰인 모든 기술 용어를 정의합니다.

5.1 부트스트랩: 모든 것이 스스로 시작된다

Common Game System에는 씬에 끌어다 놓을 프리팹도, 매니저 오브젝트도, 초기화 호출도 없습니다. Play를 누르면 Unity가 첫 씬이 로드되기 전에 프레임워크의 시작 코드를 자동으로 실행합니다. 이 시작 단계를 부트스트랩이라고 부릅니다. 부트스트랩은 23개 서비스를 올바른 순서로 생성하고, 코드가 찾을 수 있도록 하나하나 등록합니다.

Play를 누른 뒤 Console을 보면 동작을 확인할 수 있습니다. 마지막 시작 줄은 다음과 같습니다:

bootstrap complete (v2.1.0, 23 services)

부트스트랩을 직접 호출할 일은 없습니다. Play 세션마다 정확히 한 번 실행되며, 두 번째 호출은 의도적으로 오류를 던집니다. 씬의 어떤 Awake()가 실행될 때쯤에는 모든 서비스가 이미 살아서 기다리고 있습니다. 전체 레퍼런스: Bootstrap.

5.2 서비스 로케이터: 한 번 묻고, 답을 간직하라

서비스는 하나의 자체 완결적 기능입니다 — 저장, 오디오, 타이머, 입력 — 그리고 단일 C# 인터페이스를 통해 접근합니다. 서비스 로케이터는 그 모두를 담고 있는 전화번호부입니다. 인터페이스로 서비스를 요청하면 실행 중인 인스턴스를 건네줍니다:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // run after the framework has started
public class GameSetup : MonoBehaviour
{
    private ITimeService _time;
    private ISaveService _save;

    private void Awake()
    {
        // Ask once, keep the answer.
        _time = ServiceLocator.Resolve<ITimeService>();
        _save = ServiceLocator.Resolve<ISaveService>();
    }
}

두 가지 습관이 이것을 빠르고 안전하게 유지합니다:

  • Awake()Start()에서 한 번만 해석하고, 결과를 필드에 저장하세요. 조회 한 번은 딕셔너리 검색입니다. 매 프레임 호출하는 것은 아무 이득 없이 시간만 낭비합니다.
  • Awake()에서 서비스를 해석하는 클래스에는 [DefaultExecutionOrder(100)]을 붙이세요. Unity가 프레임워크 자체 오브젝트들 다음에 여러분의 스크립트를 실행하게 하여, 모든 서비스가 준비된 상태를 보장합니다.

서비스가 없을 수도 있다면 — 예를 들어 선택적 Unity 패키지를 제거한 경우 — 대신 ServiceLocator.TryResolve<T>(out var service)를 쓰세요. 예외를 던지는 대신 false를 반환합니다. 전체 레퍼런스: Service Locator.

등록된 모든 서비스를 나열하는 Service Debugger 창

Service Debugger 창(Tools > Common Game System > Service Debugger)은 플레이 중 등록된 모든 서비스를 나열합니다. 기대하는 서비스가 여기에 없다면 해당 선택적 Unity 패키지가 설치되지 않았을 가능성이 큽니다 — 7.2장을 보세요. 표시되는 개수는 23보다 클 수 있습니다 — 프레임워크가 23개의 공개 서비스와 함께 몇 개의 내부 헬퍼를 등록하기 때문입니다.

5.3 클럭: 세 가지 속도의 시간

클럭은 독립적인 시간의 흐름입니다. 프레임워크는 세 개를 운영합니다: Clock.Gameplay, Clock.UI, Clock.Background. 각각은 나머지 둘을 건드리지 않고 일시정지하거나 느려질 수 있습니다. 이것은 고전적인 버그를 해결합니다: 게임을 일시정지했더니 갑자기 메뉴 애니메이션이 얼어붙고 음악까지 멈추는 문제 말입니다.

  • Gameplay — 여러분의 월드: 캐릭터, 물리 반응, 쿨다운. 일시정지 메뉴는 이 클럭을 멈춥니다.
  • UI — 메뉴와 오버레이. 게임플레이가 일시정지된 동안에도 계속 돌아서 버튼이 계속 움직입니다.
  • Background — 음악, 그리고 결코 멈춰서는 안 되는 모든 것.

일시정지 메뉴는 두 줄이 됩니다:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class PauseMenu : MonoBehaviour
{
    private ITimeService _time;

    private void Awake() => _time = ServiceLocator.Resolve<ITimeService>();

    public void Open()  => _time.Pause(Clock.Gameplay);   // world freezes
    public void Close() => _time.Resume(Clock.Gameplay);  // world resumes
}

Unity의 Time.deltaTime 대신 _time.DeltaTime(Clock.Gameplay)로 클럭별 시간을 읽으세요. 슬로 모션은 호출 하나입니다: _time.SetTimeScale(Clock.Gameplay, 0.3f) — UI와 음악은 정상 속도를 유지합니다. 스케줄러의 타이머와 트윈 서비스의 애니메이션은 각각 클럭에 바인딩되므로 추가 코드 없이 올바르게 일시정지되고 느려집니다. 규칙 하나: Unity의 전역 Time.timeScale을 직접 설정하지 마세요. 클럭별 시스템과 충돌하게 됩니다. 전체 레퍼런스: Time.

5.4 이벤트: 구독하고, 핸들을 간직하고, dispose하라

이벤트 버스는 시스템들이 서로를 모른 채 대화하게 해줍니다. 한 스크립트가 이벤트 객체를 발행하면, 그 이벤트 타입의 모든 구독자가 구독한 순서대로 즉시 호출됩니다. 구독하면 작은 핸들이 반환됩니다. 그 핸들을 dispose하면 구독이 해제됩니다. dispose를 잊으면 핸들러가 영원히 등록된 채 남습니다 — 그것이 메모리 누수입니다.

using System;
using CommonGameSystem.Core;
using UnityEngine;

// Events are plain classes you define yourself.
public class ScoreChanged
{
    public int NewScore;
}

[DefaultExecutionOrder(100)]
public class ScoreLabel : MonoBehaviour
{
    private IEventBus _events;
    private IDisposable _subscription;

    private void Awake()   => _events = ServiceLocator.Resolve<IEventBus>();
    private void OnEnable() => _subscription = _events.Subscribe<ScoreChanged>(OnScore);
    private void OnDisable() => _subscription?.Dispose(); // always unsubscribe

    private void OnScore(ScoreChanged e) => Debug.Log($"Score: {e.NewScore}");
}

발행은 어디서든 한 줄입니다: _events.Publish(new ScoreChanged { NewScore = 100 });. 알아 둘 세부 사항 두 가지: 이벤트 타입은 클래스여야 하며(구조체 불가), 예외를 던지는 구독자가 다른 구독자를 막는 일은 결코 없습니다 — 오류는 로그로 남고 전달은 계속됩니다. 전체 레퍼런스: Event Bus.

5.5 널 구현: 모든 서비스에는 끄기 스위치가 있다

모든 서비스는 짝이 되는 널(Null) 구현과 함께 배포됩니다 — 모든 호출을 받아들이고 아무것도 하지 않는 버전입니다. 이것을 등록하면 호출부를 하나도 바꾸지 않고 해당 서브시스템이 꺼집니다. 모든 곳의 오디오를 전부 음소거하는 것은 한 줄입니다:

ServiceLocator.Replace<IAudioService>(new NullAudio());

프로젝트의 모든 PlayMusicPlaySfx 호출은 여전히 컴파일되고 실행됩니다 — 그저 아무 일도 하지 않을 뿐입니다. 테스트, 헤드리스 서버, 음성 녹음 세션, 또는 서브시스템을 자체 솔루션으로 교체할 때 유용합니다. 7.4장에 23개 서비스 전체의 끄기 스위치 목록과, 특정 서비스를 세션 중에 교체할 때 추가해야 하는 정리용 한 줄이 있습니다.

5.6 용어집

용어의미
서비스단일 C# 인터페이스를 통해 사용하는, 자체 완결적인 프레임워크 기능 하나(저장, 오디오, 타이머).
해석(Resolve)서비스 로케이터에 인터페이스로 서비스를 요청하는 것. 한 번만 하고 참조를 필드에 간직하세요.
부트스트랩첫 씬이 로드되기 전에 23개 서비스를 모두 생성하고 등록하는 자동 시작 단계. 직접 호출할 일은 없습니다.
클럭세 가지 독립적 시간 흐름(Gameplay, UI, Background) 중 하나. 각각 따로 일시정지하거나 느려질 수 있습니다.
트윈정해진 시간 동안 값을 시작점에서 끝점까지 부드럽게 움직이는 짧은 애니메이션. 이징 커브로 모양이 정해집니다.
액션 맵Unity Input System 애셋 안의 이름 있는 입력 액션 그룹("Gameplay", "Menu" 등). 활성화된 맵만 플레이어에게 반응합니다.
Addressables직접 참조 대신 텍스트 주소로 애셋을 필요할 때 로드하는 Unity의 선택적 패키지.
IL2CPPC#을 네이티브 코드로 변환하는 Unity의 빌드 모드. 사용되지 않아 보이는 클래스를 제거하므로, 세이브 클래스에 link.xml 항목이 필요한 이유입니다(6.3장).

다음: 6. 그림으로 보는 설정 단계