씬 플로우

로딩 화면, 취소, 애디티브 멀티 씬을 지원하는 비동기 씬 로딩.

씬 전환을 위한 하나의 awaitable API · 자동 로딩 화면, 진행률 이벤트, 협조적 취소 · 애디티브 멀티 씬 레이어링 내장 · no-op 대체물: NullSceneService

CGS는 ISceneService — 모든 메뉴→게임플레이→메뉴 전환을 위한 하나의 awaitable API — 를 통해 게임을 씬 사이로 이동시킵니다. await _scene.LoadAsync("Gameplay")를 호출하면 나머지는 서비스가 조율합니다: 로딩 화면을 자동으로 보여 주고, 한 프레임 번쩍임을 피할 만큼 충분히 화면에 유지하고, 진행률 바를 위한 진행 이벤트를 발행하며, 협조적 취소(플레이어가 Esc를 누르면 로드가 멈춤)를 지원합니다.

같은 서비스가 애디티브 씬 — 현재 씬을 대체하는 대신 그 위에 로드되는 씬 — 도 처리합니다. UI 씬을 겹치거나 이웃 지역을 스트리밍해 들일 때 쓰는 방식입니다. 싱글 모드와 애디티브 로딩은 한 서비스의 두 표면입니다; 추가 설정은 없습니다.

무엇을 하는가

  • 전환당 하나의 비동기 호출. LoadAsyncawait할 수 있는 Task<SceneLoadResult>를 반환합니다. 성공과 실패는 결과 객체로 돌아오고, 취소와 타임아웃은 OperationCanceledException을 던집니다.
  • 자동 로딩 화면. 옵션에 패널을 넘기면 서비스가 로드 전에 push하고 끝난 뒤 pop하며, 최소 표시 시간을 구성할 수 있습니다.
  • 진행률 이벤트. 서비스는 로드 시작/진행/완료/취소 이벤트를 이벤트 버스에 발행합니다 — 음악을 페이드 아웃했다가 다시 페이드 인하기에 자연스러운 지점입니다.
  • 애디티브 레이어링. 베이스 씬 위에 추가 씬을 로드/언로드하고, 로드된 씬의 전체 목록을 조사하고, Unity가 어느 것을 활성으로 취급할지 선택합니다.

서비스 가져오기

한 번 resolve해 참조를 캐시하십시오(Update 안에서는 절대 금지):

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MenuController : MonoBehaviour
{
    private ISceneService _scene;

    private void Awake()
    {
        _scene = ServiceLocator.Resolve<ISceneService>();
    }
}

씬 로드하기 (싱글 모드)

싱글 모드 로딩은 현재 씬을 대체합니다 — 표준적인 메인 메뉴→게임플레이 전환입니다.

Task<SceneLoadResult> LoadAsync(string sceneName,
                                SceneLoadOptions opts = default,
                                CancellationToken ct = default)

이름으로 씬을 로드합니다. 씬은 Build Settings(File > Build Settings > Scenes in Build)에 등록되어 있어야 합니다. 성공 또는 오류 시 SceneLoadResult를 반환하고, 취소나 타임아웃 시 OperationCanceledException을 던집니다.

Task<SceneLoadResult> ReloadCurrentAsync(SceneLoadOptions opts = default,
                                         CancellationToken ct = default)

현재 활성 씬을 다시 로드합니다 — 체크포인트 리스폰과 디버그 리로드를 위한 편의 기능입니다.

옵션 (SceneLoadOptions)

필드설명
LoadingScreenPanel로드가 시작될 때 push되고 끝나면 pop되는 선택적 패널(UI 프레임워크IPanel).
MinDisplayDuration로딩 화면을 계속 보여 줄 최소 초. 로드가 몇 프레임 만에 끝날 때의 거슬리는 번쩍임을 방지합니다.
LoadTimeoutSeconds호출별 타임아웃 재정의. 0(기본값)은 서비스 전역 설정을 사용합니다. 만료되면 로드는 내부에 TimeoutException을 담은 OperationCanceledException을 던집니다.

결과 (SceneLoadResult)

프로퍼티의미
Succeeded씬이 정상적으로 활성화되면 true.
ErrorSucceededfalse일 때 캡처된 예외(예: 씬이 Build Settings에 없음). 성공 시 null.
SceneName로드 대상이었던 씬.
TotalDuration처음부터 끝까지의 로드 시간(TimeSpan).

로드 단계와 상태 조회

로드는 언제든 관찰할 수 있는 단계들을 거칩니다:

string ActiveSceneName { get; } // Active scene name; empty before the first load completes
bool IsLoading { get; }         // true while any scene operation is in flight
LoadPhase CurrentPhase { get; } // Idle, Unloading, Loading, MinDisplayHold, Activating, Active

발행되는 이벤트

이벤트 버스를 통해 구독하십시오:

이벤트시점전형적 용도
SceneLoadStarted(FromSceneName, ToSceneName)로드가 시작됨이전 음악 페이드 아웃
SceneLoadProgress(SceneName, Progress, Phase)진행률 업데이트, Progress는 0..1진행률 바 구동
SceneLoadCompleted(FromSceneName, ToSceneName, TotalDuration)로드가 완료됨새 음악 페이드 인
SceneLoadCanceled(FromSceneName, AttemptedSceneName, PhaseAtCancel, ElapsedBeforeCancel, Cause)로드가 취소되거나 타임아웃됨메뉴로 깔끔하게 복귀

애디티브 씬

애디티브 로드는 이미 로드된 씬들을 대체하는 대신 거기에 씬을 추가합니다. 게임플레이 위의 상시 UI 씬, 스트리밍되는 이웃 지역, 공유 "매니저" 씬에 사용하십시오.

Task<SceneLoadResult> LoadAdditiveAsync(string sceneName,
                                        SceneLoadOptions opts = default,
                                        CancellationToken ct = default)

베이스 씬 위에 씬을 애디티브로 로드합니다. 활성 씬은 바뀌지 않습니다ActiveSceneName은 여전히 베이스 씬을 보고합니다. 이미 애디티브로 로드된 씬을 다시 로드하는 것은 안전한 no-op입니다: 서비스는 경고를 로그하고 완료된 태스크를 반환합니다(참조 카운팅은 없습니다 — 각 씬은 최대 한 번만 로드됩니다).

Task UnloadAdditiveAsync(string sceneName, CancellationToken ct = default)

애디티브로 로드된 씬 하나를 언로드합니다. 베이스 씬의 이름이나 로드되지 않은 이름을 넘기는 것은 경고와 함께 안전한 no-op입니다 — 베이스 씬은 이 방법으로 결코 제거될 수 없습니다(대체하려면 LoadAsync를 사용하십시오).

IReadOnlyList<string> ActiveSceneList { get; }

로드된 모든 씬: 베이스 씬이 먼저, 이어서 로드된 순서대로 애디티브 씬들. 읽을 때마다 자유롭게 보관하거나 수정할 수 있는 새 스냅샷을 반환합니다.

bool SetActiveScene(string sceneName)

로드된 씬 중 어느 것이 "활성"인지 — 새로 인스턴스화되는 오브젝트를 받고 라이팅 설정을 주도하는 씬 — Unity에 알립니다. 그 씬은 이미 ActiveSceneList에 있어야 합니다; 로드되어 있지 않으면 (경고와 함께, 예외 없이) false를 반환합니다.

알아 둘 만한 동작:

  • 애디티브 로드는 로딩 화면을 건너뜁니다. 애디티브 작업에서는 LoadingScreenPanelMinDisplayDuration이 무시됩니다(설정했다면 로그 메시지와 함께) — 애디티브 로드는 가볍고 이음새 없이 이루어지도록 의도된 것입니다.
  • 같은 이벤트가 발행됩니다. 애디티브 로드와 언로드는 싱글 모드 로드와 동일한 SceneLoadStarted / SceneLoadProgress / SceneLoadCompleted 이벤트를 발행합니다. 애디티브 작업 중에는 ActiveSceneName이 바뀌지 않는다는 점으로 구별할 수 있습니다.
  • 싱글 모드 로드는 애디티브 목록을 비웁니다. LoadAsync가 완료되면 Unity가 이전 씬을 전부 언로드한 상태입니다 — 서비스는 ActiveSceneList를 새 베이스 씬 하나로 리셋합니다.
  • 여전히 한 번에 한 작업. 애디티브 로드와 언로드는 싱글 모드 로드와 동일한 in-flight 게이트를 공유합니다: IsLoadingtrue인 동안 다른 작업을 시작하면 거부됩니다.

전체 예제

using System;
using System.Threading;
using System.Threading.Tasks;
using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class GameFlow : MonoBehaviour
{
    private ISceneService _scene;
    private IPanel _loadingScreen;
    private CancellationTokenSource _menuCts;

    private void Awake()
    {
        _scene = ServiceLocator.Resolve<ISceneService>();
        _loadingScreen = GetComponent<IPanel>(); // Your loading screen panel
    }

    // --- Single-mode: menu -> gameplay with a loading screen ---

    public async void OnPlayButtonClicked()
    {
        _menuCts = new CancellationTokenSource();
        try
        {
            var opts = new SceneLoadOptions
            {
                LoadingScreenPanel = _loadingScreen,
                MinDisplayDuration = 1.0f // At least 1 second on screen
            };
            var result = await _scene.LoadAsync("Gameplay", opts, _menuCts.Token);
            if (!result.Succeeded)
                Debug.LogError($"Scene load failed: {result.Error.Message}");
        }
        catch (OperationCanceledException)
        {
            Debug.Log("Scene load was canceled (player hit Esc)");
        }
    }

    public void OnEscapePressed()
    {
        _menuCts?.Cancel(); // Cancel the in-flight load
    }

    // --- Additive: layer a HUD scene over gameplay ---

    public async Task ShowHudAsync()
    {
        await _scene.LoadAdditiveAsync("HudOverlay");

        Debug.Log(_scene.ActiveSceneName);                    // "Gameplay" — unchanged
        Debug.Log(string.Join(", ", _scene.ActiveSceneList)); // "Gameplay, HudOverlay"
    }

    public async Task HideHudAsync()
    {
        await _scene.UnloadAdditiveAsync("HudOverlay");
    }
}

끄는 방법

ServiceLocator.Replace<ISceneService>(new NullSceneService());

null 구현에서는 모든 로드 호출이 이미 완료된 성공 태스크를 반환합니다 — 실제 로딩도, 이벤트도, 로딩 패널도 없습니다. 헤드리스 테스트나 게임플레이 플로우 모킹에 유용합니다. 취소 토큰은 여전히 존중합니다(이미 취소된 토큰은 취소된 태스크를 만듭니다).

흔한 함정

  • 메인 스레드 전용. 로드 메서드 호출과 프로퍼티 읽기는 메인 스레드에서만 하십시오. 디버그 빌드에서 서비스가 이를 검사합니다.

  • 씬 작업은 한 번에 하나. IsLoadingtrue인 동안 다른 로드를 시작할 수 없습니다 — 먼저 것을 await하거나 취소하십시오. 이는 싱글 모드 로드, 애디티브 로드, 애디티브 언로드 전체에 적용됩니다.

  • 취소는 협조적입니다. 새 씬의 Awake 호출이 시작되면(LoadPhase.Activating) 취소는 무시되고 로드가 완료됩니다 — Unity는 활성화 도중의 씬을 안전하게 중단할 수 없습니다. 취소는 LoadingMinDisplayHold 단계에서 존중됩니다.

  • MinDisplayDuration은 UI 클럭으로 흐릅니다. 최소 표시 대기는 Time 서비스의 UI 클럭을 사용하며, 이 클럭은 게임플레이가 일시정지된 동안에도 계속 흐릅니다. 로드 중에 UI 클럭을 일시정지하면 대기도 함께 멈춥니다.

  • 씬은 Build Settings에 있어야 합니다. 등록되지 않은 이름은 Error가 문제를 설명하는 SceneLoadResult로 실패합니다 — 성공을 가정하지 말고 결과를 확인하십시오.

  • IL2CPP + 코드 스트리핑. 프로젝트가 IL2CPP로 빌드되고 로딩 패널이나 이벤트 구독자가 스트리핑된다면, 프로젝트의 link.xml에 다음을 추가하십시오:

    <type fullname="CommonGameSystem.Core.SceneLoadResult" preserve="all"/>
    <type fullname="System.Threading.Tasks.Task`1[[CommonGameSystem.Core.SceneLoadResult]]" preserve="all"/>
    

관련 페이지