앱인토스 개발자센터 로고
Skip to content

첫 씬 시작 최적화 가이드

앱인토스 Unity 게임에서 첫 번째 씬의 로딩 시간을 최소화하여 즉시 플레이 가능한 경험을 제공하는 방법을 다뤄요.

1. 첫 씬 최적화 전략

첫 씬 구조 최적화

🚀 최적화된 첫 씬 구조
├── 핵심 게임 요소 (즉시 로딩)
│   ├── 플레이어 컨트롤러
│   ├── 기본 UI 시스템
│   ├── 입력 관리자
│   └── 게임 매니저
├── 시각적 요소 (단계적 로딩)
│   ├── 배경 이미지 (저해상도 → 고해상도)
│   ├── 캐릭터 모델 (LOD 0 → LOD 상위)
│   ├── 환경 오브젝트 (필수 → 장식)
│   └── 파티클 효과 (기본 → 고급)
├── 오디오 (지연 로딩)
│   ├── 배경 음악
│   ├── 효과음
│   └── 음성
└── 비핵심 시스템 (백그라운드 로딩)
    ├── 분석 시스템
    ├── 소셜 기능
    ├── 광고 시스템
    └── 업데이트 체크

첫 씬 최적화 매니저

c#
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;
using System.Collections.Generic;

public class FirstSceneOptimizer : MonoBehaviour
{
    public static FirstSceneOptimizer Instance { get; private set; }
    
    [System.Serializable]
    public class OptimizationSettings
    {
        [Header("로딩 우선순위")]
        public GameObject[] criticalObjects;
        public GameObject[] secondaryObjects;
        public GameObject[] optionalObjects;
        
        [Header("품질 설정")]
        public int initialQualityLevel = 0;
        public bool enableProgressiveLOD = true;
        public bool enableAsyncLoading = true;
        
        [Header("시간 제한")]
        public float maxInitialLoadTime = 2f;
        public float maxSecondaryLoadTime = 5f;
    }
    
    [Header("최적화 설정")]
    public OptimizationSettings settings;
    
    // 내부 상태
    private bool isOptimizationComplete = false;
    private float optimizationStartTime;
    private List<GameObject> loadedObjects = new List<GameObject>();
    
    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            StartFirstSceneOptimization();
        }
        else
        {
            Destroy(gameObject);
        }
    }
    
    void StartFirstSceneOptimization()
    {
        optimizationStartTime = Time.realtimeSinceStartup;
        
        Debug.Log("첫 씬 최적화 시작");
        
        // 초기 품질 설정
        QualitySettings.SetQualityLevel(settings.initialQualityLevel);
        
        // 단계별 로딩 시작
        StartCoroutine(OptimizedLoadingSequence());
    }
    
    IEnumerator OptimizedLoadingSequence()
    {
        // 1단계: 즉시 필요한 핵심 오브젝트 활성화
        yield return StartCoroutine(LoadCriticalObjects());
        
        // 첫 프레임 렌더링을 위해 대기
        yield return null;
        
        // 2단계: 2차 오브젝트 로딩
        StartCoroutine(LoadSecondaryObjects());
        
        // 3단계: 선택적 오브젝트 백그라운드 로딩
        StartCoroutine(LoadOptionalObjects());
        
        // 최적화 완료
        CompleteOptimization();
    }
    
    IEnumerator LoadCriticalObjects()
    {
        Debug.Log("핵심 오브젝트 로딩 시작");
        
        foreach (var obj in settings.criticalObjects)
        {
            if (obj != null)
            {
                obj.SetActive(true);
                loadedObjects.Add(obj);
                
                // 프레임 분산을 위해 대기
                yield return null;
            }
        }
        
        float loadTime = Time.realtimeSinceStartup - optimizationStartTime;
        Debug.Log($"핵심 오브젝트 로딩 완료: {loadTime:F2}초");
        
        // 게임 시작 가능 신호
        SendGameReadySignal();
    }
    
    IEnumerator LoadSecondaryObjects()
    {
        float startTime = Time.realtimeSinceStartup;
        
        foreach (var obj in settings.secondaryObjects)
        {
            // 시간 제한 체크
            if (Time.realtimeSinceStartup - startTime > settings.maxSecondaryLoadTime)
            {
                break;
            }
            
            if (obj != null)
            {
                obj.SetActive(true);
                loadedObjects.Add(obj);
                
                // 프레임 분산
                yield return null;
            }
        }
        
        Debug.Log("2차 오브젝트 로딩 완료");
    }
    
    IEnumerator LoadOptionalObjects()
    {
        // 플레이어가 활발하지 않을 때만 로딩
        while (true)
        {
            if (IsPlayerIdle())
            {
                foreach (var obj in settings.optionalObjects)
                {
                    if (obj != null && !obj.activeInHierarchy)
                    {
                        obj.SetActive(true);
                        loadedObjects.Add(obj);
                        
                        // 여러 프레임에 걸쳐 분산
                        for (int i = 0; i < 3; i++)
                        {
                            yield return null;
                        }
                        
                        // 플레이어가 다시 활성화되면 중단
                        if (!IsPlayerIdle())
                        {
                            break;
                        }
                    }
                }
            }
            
            yield return new WaitForSeconds(1f);
        }
    }
    
    bool IsPlayerIdle()
    {
        // 간단한 유휴 상태 감지 로직
        return Time.realtimeSinceStartup - Time.time > 3f;
    }
    
    void SendGameReadySignal()
    {
        float readyTime = Time.realtimeSinceStartup - optimizationStartTime;
        
        var readyData = new Dictionary<string, object>
        {
            {"ready_time", readyTime},
            {"loaded_objects", loadedObjects.Count},
            {"quality_level", QualitySettings.GetQualityLevel()}
        };
        
        AppsInToss.SendEvent("first_scene_ready", readyData);
        
        Debug.Log($"첫 씬 준비 완료: {readyTime:F2}초");
    }
    
    void CompleteOptimization()
    {
        isOptimizationComplete = true;
        
        // 점진적 품질 향상 시작
        if (settings.enableProgressiveLOD)
        {
            StartCoroutine(ProgressiveQualityImprovement());
        }
        
        float totalTime = Time.realtimeSinceStartup - optimizationStartTime;
        
        // 최적화 완료 분석 데이터
        var analyticsData = new Dictionary<string, object>
        {
            {"optimization_time", totalTime},
            {"objects_loaded", loadedObjects.Count},
            {"device_model", SystemInfo.deviceModel},
            {"memory_usage", System.GC.GetTotalMemory(false) / (1024f * 1024f)}
        };
        
        AppsInToss.SendAnalytics("first_scene_optimization", analyticsData);
    }
    
    IEnumerator ProgressiveQualityImprovement()
    {
        // 몇 초 대기 후 품질 점진적 향상
        yield return new WaitForSeconds(3f);
        
        int maxQuality = QualitySettings.names.Length - 1;
        int currentQuality = QualitySettings.GetQualityLevel();
        
        while (currentQuality < maxQuality)
        {
            // 성능 여유가 있을 때만 품질 향상
            if (Application.targetFrameRate <= 0 || Time.smoothDeltaTime * Application.targetFrameRate < 1.2f)
            {
                currentQuality++;
                QualitySettings.SetQualityLevel(currentQuality);
                
                Debug.Log($"품질 레벨 향상: {currentQuality}");
                
                // 품질 변경 후 안정화 대기
                yield return new WaitForSeconds(2f);
            }
            else
            {
                // 성능이 부족하면 대기
                yield return new WaitForSeconds(5f);
            }
        }
    }
    
    // 공개 API
    public bool IsOptimizationComplete()
    {
        return isOptimizationComplete;
    }
    
    public float GetOptimizationProgress()
    {
        if (isOptimizationComplete) return 1f;
        
        int totalObjects = settings.criticalObjects.Length + 
                          settings.secondaryObjects.Length + 
                          settings.optionalObjects.Length;
        
        return totalObjects > 0 ? (float)loadedObjects.Count / totalObjects : 0f;
    }
    
    public void ForceCompleteOptimization()
    {
        StopAllCoroutines();
        
        // 모든 오브젝트 즉시 활성화
        foreach (var obj in settings.criticalObjects)
        {
            if (obj != null) obj.SetActive(true);
        }
        foreach (var obj in settings.secondaryObjects)
        {
            if (obj != null) obj.SetActive(true);
        }
        
        CompleteOptimization();
    }
}

2. 성능 모니터링

실시간 성능 추적

c#
public class FirstScenePerformanceMonitor : MonoBehaviour
{
    [Header("성능 임계값")]
    public float targetFPS = 30f;
    public float maxLoadTime = 3f;
    public float maxMemoryUsageMB = 200f;
    
    private float sceneStartTime;
    private List<float> frameTimes = new List<float>();
    
    void Start()
    {
        sceneStartTime = Time.realtimeSinceStartup;
        StartCoroutine(MonitorPerformance());
    }
    
    IEnumerator MonitorPerformance()
    {
        while (true)
        {
            // FPS 모니터링
            float frameTime = Time.smoothDeltaTime;
            frameTimes.Add(frameTime);
            
            if (frameTimes.Count > 60) // 2초간의 프레임 타임 유지
            {
                frameTimes.RemoveAt(0);
            }
            
            // 성능 문제 감지
            CheckPerformanceIssues();
            
            yield return null;
        }
    }
    
    void CheckPerformanceIssues()
    {
        // FPS 체크
        if (frameTimes.Count > 30)
        {
            float avgFrameTime = 0f;
            for (int i = frameTimes.Count - 30; i < frameTimes.Count; i++)
            {
                avgFrameTime += frameTimes[i];
            }
            avgFrameTime /= 30f;
            
            float currentFPS = 1f / avgFrameTime;
            
            if (currentFPS < targetFPS * 0.8f) // 20% 여유
            {
                OnPerformanceIssueDetected("Low FPS", currentFPS);
            }
        }
        
        // 메모리 사용량 체크
        float memoryUsageMB = System.GC.GetTotalMemory(false) / (1024f * 1024f);
        if (memoryUsageMB > maxMemoryUsageMB)
        {
            OnPerformanceIssueDetected("High Memory Usage", memoryUsageMB);
        }
        
        // 로딩 시간 체크
        float loadTime = Time.realtimeSinceStartup - sceneStartTime;
        if (loadTime > maxLoadTime && !FirstSceneOptimizer.Instance.IsOptimizationComplete())
        {
            OnPerformanceIssueDetected("Slow Loading", loadTime);
        }
    }
    
    void OnPerformanceIssueDetected(string issueType, float value)
    {
        Debug.LogWarning($"성능 문제 감지: {issueType} = {value:F2}");
        
        // 자동 최적화 시도
        TryAutoOptimization(issueType);
        
        // 분석 데이터 전송
        var issueData = new Dictionary<string, object>
        {
            {"issue_type", issueType},
            {"value", value},
            {"scene_time", Time.realtimeSinceStartup - sceneStartTime},
            {"device_model", SystemInfo.deviceModel}
        };
        
        AppsInToss.SendAnalytics("first_scene_performance_issue", issueData);
    }
    
    void TryAutoOptimization(string issueType)
    {
        switch (issueType)
        {
            case "Low FPS":
                // 품질 레벨 낮추기
                int currentQuality = QualitySettings.GetQualityLevel();
                if (currentQuality > 0)
                {
                    QualitySettings.SetQualityLevel(currentQuality - 1);
                    Debug.Log($"품질 레벨 자동 조정: {currentQuality - 1}");
                }
                break;
                
            case "High Memory Usage":
                // 강제 가비지 컬렉션
                System.GC.Collect();
                break;
                
            case "Slow Loading":
                // 강제 최적화 완료
                if (FirstSceneOptimizer.Instance != null)
                {
                    FirstSceneOptimizer.Instance.ForceCompleteOptimization();
                }
                break;
        }
    }
}

첫 씬은 사용자의 첫인상을 결정짓는 중요한 순간이에요.
핵심 기능을 우선 로딩하고 나머지는 점진적으로 로딩하여 즉시 플레이 가능한 경험을 제공하세요.