씬 플로우

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

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

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

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

무엇을 하는가

  • 전환당 하나의 비동기 호출. LoadAsync는 await할 수 있는 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.
ErrorSucceeded가 false일 때 캡처된 예외(예: 씬이 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를 반환합니다.

알아 둘 만한 동작:

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

전체 예제

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

흔한 함정

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

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

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

  • 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"/>
    

관련 페이지