LoadSceneAsync가 완료되어도 로딩 화면이 남아 있을 수 있습니다.
저장 데이터 복원, 플레이어 생성, 입력 설정이 아직 끝나지 않았을 수 있기 때문입니다.
로드 시간을 줄이려면 먼저 타이머를 멈출 조건을 정해야 합니다.
여기서는 로드 요청부터 조작 가능 시점까지를 전체 시간으로 삼고, 중간 시각을 기록해 원인을 살펴봅니다. Unity 6.4 API와 Framedash Unity SDK 0.1.8의 동작을 바탕으로 한 측정 설계입니다.
진행률 0.9와 조작 가능 상태를 구분하기
allowSceneActivation = false이면 Unity는 진행률을 0.9에서 멈추고 isDone을 false로 유지합니다.
이는 활성화 전 대기 지점이지, 바이트나 CPU 작업의 90%를 끝냈다는 뜻은 아닙니다.
활성화를 보류하면 다른 AsyncOperation 작업도 기다릴 수 있습니다.
Unity 활성화 문서를 참고하고, 계측만을 위해 원래 없던 대기를 넣지는 마세요.
sceneLoaded 역시 별도의 신호입니다.
Unity는 OnEnable 이후, Start 이전에 이 콜백을 호출합니다.
이를 조작 가능 시점으로 삼으면 Start와 이후 비동기 초기화가 측정에서 빠집니다.
엔진 콜백 순서와 게임의 준비 완료 조건을 구분해야 합니다.
하나의 시계로 네 시각 기록하기
- T0: 로드 요청. 로더를 호출하기 직전
- T1: 활성화 허용. 기존 로더가 활성화를 보류할 때만,
allowSceneActivation을 true로 바꾸기 직전 - T2: 씬 작업 완료 관측.
LoadSceneAsync.isDone이 true임을 확인한 시점 - T3: 조작 가능. 필수 초기화를 끝내고 로딩 화면을 제거해 입력을 받는 등, 게임에서 정한 조건이 충족된 시점
T0부터 T1까지는 로드 외에도 연출이나 계속 버튼을 기다리는 시간이 포함될 수 있습니다. T1부터 T2까지도 순수한 활성화 CPU 시간이 아니라 경과 시간입니다. 다른 메인 스레드 작업이나 코루틴의 다음 확인까지 걸린 시간이 들어갈 수 있습니다. 개별 작업에 비용을 귀속하려면 Unity Profiler를 함께 사용하세요.
활성화를 수동 제어하지 않는 로더에는 T1이 필요하지 않습니다.
T0에서 T2, T2에서 T3만 기록해도 씬 작업 이후의 대기를 구분할 수 있습니다.
준비 완료 신호가 T2 관측보다 먼저 올 수 있다면 호출 측에서 그 상태를 기억하고, T2를 관측한 뒤 TryReportPlayable을 다시 호출하세요.
이 보조 클래스는 이른 신호를 저장하지 않습니다. T3는 두 조건이 모두 충족되었음을 관측해 보고하는 시점입니다.
전체 시간 한 건과 별도 상세 이벤트 기록하기
비교할 빌드에서는 map_load의 구간 정의를 같게 유지합니다.
이 예제는 T0부터 T3까지를 한 번의 로드로 기록합니다.
기준 빌드는 T2, 변경 빌드는 T3에서 끝내면 작업이 같아도 측정값이 달라집니다.
Framedash 로드 시간 API는 BeginMapLoad / EndMapLoad와 ReportMapLoad를 제공합니다.
SDK에 타이머를 맡기면 전자를, 로더가 이미 측정한 밀리초를 전달하려면 후자를 사용합니다.
BeginMapLoad를 다시 호출하면 진행 중인 측정이 교체되므로, 동시 로드에는 로드별 시계와 ReportMapLoad를 사용하세요.
아래 보조 클래스는 후자의 예입니다.
초기화된 SDK를 전달하고 T0에서 생성합니다.
T1에서는 선택적으로 MarkActivationRequested, T2에서는 MarkOperationDone, T3에서는 TryReportPlayable을 호출합니다.
씬 전환 때 사라지지 않는 로더가 인스턴스를 유지해야 합니다.
SDK 호출은 0.1.8 소스와 대조했습니다. 모든 호출은 Unity 메인 스레드에서 수행합니다. 백그라운드 로더가 완료되면 메인 스레드로 돌아온 뒤 보고하세요.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Framedash;
// One instance per load. Call every method on Unity's main thread.
public sealed class LoadProbe
{
private readonly TelemetrySDK sdk;
private readonly string mapName;
private readonly Stopwatch clock = Stopwatch.StartNew();
private double? activationRequestedMs;
private double? operationDoneMs;
private bool reported;
public LoadProbe(TelemetrySDK initializedSdk, string mapName)
{
if (initializedSdk == null)
throw new ArgumentNullException(nameof(initializedSdk));
if (string.IsNullOrWhiteSpace(mapName))
throw new ArgumentException("A stable map name is required.", nameof(mapName));
sdk = initializedSdk;
this.mapName = mapName;
}
// Optional: only if the existing loader controls activation.
public void MarkActivationRequested()
{
if (!reported && !activationRequestedMs.HasValue && !operationDoneMs.HasValue)
activationRequestedMs = clock.Elapsed.TotalMilliseconds;
}
public void MarkOperationDone()
{
if (!reported && !operationDoneMs.HasValue)
operationDoneMs = clock.Elapsed.TotalMilliseconds;
}
// Call when both readiness and isDone have been observed.
// An early readiness call is not cached; call again after MarkOperationDone.
public bool TryReportPlayable()
{
if (reported || !operationDoneMs.HasValue) return false;
double totalMs = clock.Elapsed.TotalMilliseconds;
double doneMs = operationDoneMs.Value;
var metrics = new Dictionary<string, float>
{
{ "scene_operation_ms", (float)doneMs },
{ "after_operation_ms", (float)(totalMs - doneMs) }
};
if (activationRequestedMs.HasValue)
{
double activationMs = activationRequestedMs.Value;
metrics["before_activation_ms"] = (float)activationMs;
metrics["activation_window_ms"] = (float)(doneMs - activationMs);
}
reported = true;
sdk.ReportMapLoad(mapName, totalMs);
sdk.Track("load_phases", attributes: new Dictionary<string, string>
{ { "map_name", mapName } }, metrics: metrics);
return true;
}
}
TryReportPlayable의 true는 이 클래스가 보고 처리를 한 번 실행했다는 뜻이며, 서버 수신 확인은 아닙니다.
실패나 취소된 시도에는 호출하지 마세요.
필요하면 별도 실패 이벤트로 기록해 성공 로드 집계에 섞이지 않게 합니다.
load_phases와 메트릭 이름은 이 예제에서 정의한 커스텀 이벤트입니다.
전용 단계 차트나 회귀 게이트가 자동 생성되는 기능은 아닙니다.
표준 map_load의 map_id는 비어 있고, 맵 이름은 attributes["map_name"], 시간은 metrics["load_time_ms"]에 담깁니다.
이 이벤트만으로 공간 히트맵 점이 생성되지는 않습니다.
두 이벤트 모두 샘플링의 영향을 받습니다. 검증 시 샘플링 단계에서 모든 이벤트를 남기려면 초기화 후 다음 오버라이드를 설정합니다. 비율이 1이어도 전송 장애나 버퍼 한도로 인한 누락을 막지는 못합니다.
TelemetrySDK.Instance.SetEventSamplingRate("map_load", 1f);
TelemetrySDK.Instance.SetEventSamplingRate("load_phases", 1f);
비교 전에 경계와 실패 사례 확인하기
자체 로더에 통합할 때는 다음을 확인하세요.
- 같은 씬과 빌드 설정에서 T0, T2, T3의 순서와 단위를 확인합니다
- 기존 활성화 보류 흐름에서 의도적인 대기가 T0부터 T1에 포함되는지 확인합니다
MarkOperationDone이후의 비동기 준비 완료를 의도적으로 지연시켜 T2부터 T3가 늘어나는지 확인합니다.Awake등 앞선 작업의 지연은 T2 자체를 늦출 수 있습니다- 실패, 취소, 중복 완료, 동시 로드가 성공 건수를 늘리거나 다른 시도와 섞이지 않는지 확인합니다
- 콜드 시작과 재로드를 분리하고 기기, 저장장치, 캐시 조건, 빌드 ID를 기록합니다
예제의 검증 범위
이 보조 클래스는 Unity 6000.3.25f1(6.3 LTS)과 6000.6.4f1, Framedash Unity SDK 0.1.8로 검증했습니다.
같은 Windows x64 호스트에서 Mono와 IL2CPP를 각각 비 batchmode D3D12 숨김 창과 -batchmode -nographics로 실행했습니다.
SDK와 LoadProbe를 변경하지 않은 상태에서 기사 예제용 테스트 씬의 검증을 통과했고, SDK 종료 처리 후 Player가 정상 종료했습니다.
이 버전 검증에서는 map_load와 load_phases의 실제 전송 데이터를 로컬에서 디코딩해 수신을 확인했습니다.
실패하거나 취소된 시도는 성공 로드로 전송되지 않았습니다.
별도의 Framedash 운영 환경 연결 테스트에서는 저장 데이터를 다시 조회해 두 이벤트 유형이 모두 저장되었음을 확인했습니다.
이 결과는 검증한 시도의 수신을 확인한 것이며 모든 이벤트의 전달을 보장하지는 않습니다.
검증 대상은 이 글의 예제이며, SDK 전체 테스트 모음이나 고객 게임의 성능까지 검증한 것은 아닙니다. 화면에 표시되는 대화형 창과 Android / iOS 실기기는 아직 검증하지 않았습니다. 자체 게임에 통합할 때는 위 체크 항목과 함께 대상 기기에서의 동작도 확인하세요.
먼저 T0부터 T3의 정의를 고정하고 긴 구간을 조사하세요. 그러면 같은 지표로 씬 로드 작업을 줄였는지, 플레이어가 조작하기 전의 대기를 줄였는지 판단할 수 있습니다.