AutoStreaming 가이드
1. AutoStreaming 개요
AutoStreaming이란?
🚀 AutoStreaming 개념
├── 즉시 시작 (Instant Play) - 0-5초 내 게임 시작
├── 백그라운드 로딩 - 플레이 중 추가 콘텐츠 다운로드
├── 우선순위 기반 로딩 - 중요한 에셋 우선 로딩
└── 적응형 품질 - 네트워크 상태에 따른 품질 조절AutoStreaming 아키텍처
c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
public class AppsInTossAutoStreaming : MonoBehaviour
{
public static AppsInTossAutoStreaming Instance { get; private set; }
[System.Serializable]
public class StreamingConfig
{
public string assetName;
public StreamingPriority priority;
public float sizeMB;
public string[] dependencies;
public bool essential = false; // 게임 진행에 필수 여부
public int loadOrder = 0;
public float timeoutSeconds = 30f;
}
public enum StreamingPriority
{
Immediate = 0, // 즉시 로딩 (게임 시작 전 필수)
High = 1, // 게임 시작 후 바로 로딩
Medium = 2, // 사용자가 해당 영역에 접근하기 전 로딩
Low = 3, // 필요할 때만 로딩
Preload = 4 // 여유가 있을 때 미리 로딩
}
[Header("스트리밍 설정")]
public StreamingConfig[] streamingAssets;
public float initialLoadThresholdMB = 2f; // 즉시 시작을 위한 초기 로딩 임계값
public bool enableAdaptiveQuality = true;
public bool enablePreloadInIdle = true;
[Header("네트워크 설정")]
public string baseUrl = "https://cdn.appintoss.com/streaming/";
public int maxConcurrentDownloads = 3;
public float networkTimeoutSeconds = 15f;
// 내부 상태
private Dictionary<string, StreamingConfig> configMap = new Dictionary<string, StreamingConfig>();
private HashSet<string> loadedAssets = new HashSet<string>();
private HashSet<string> downloadingAssets = new HashSet<string>();
private Queue<StreamingConfig> downloadQueue = new Queue<StreamingConfig>();
private bool isInitialLoadComplete = false;
private int activeDownloads = 0;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeAutoStreaming();
}
else
{
Destroy(gameObject);
}
}
void InitializeAutoStreaming()
{
// 설정 맵 생성
foreach (var config in streamingAssets)
{
configMap[config.assetName] = config;
}
// 네트워크 상태 모니터링 시작
StartCoroutine(MonitorNetworkStatus());
// 즉시 로딩 가능한 최소한의 에셋 로딩
StartCoroutine(LoadImmediateAssets());
}
IEnumerator LoadImmediateAssets()
{
Debug.Log("AutoStreaming 즉시 로딩 시작");
float startTime = Time.realtimeSinceStartup;
// Immediate 우선순위 에셋들을 크기 기준으로 정렬
var immediateAssets = System.Array.FindAll(streamingAssets,
a => a.priority == StreamingPriority.Immediate);
System.Array.Sort(immediateAssets, (a, b) => a.sizeMB.CompareTo(b.sizeMB));
float loadedSizeMB = 0f;
foreach (var asset in immediateAssets)
{
if (loadedSizeMB >= initialLoadThresholdMB && !asset.essential)
{
// 임계값 도달, 필수 에셋이 아니면 나중에 로딩
QueueAssetForDownload(asset);
continue;
}
yield return StartCoroutine(LoadAssetCoroutine(asset));
loadedSizeMB += asset.sizeMB;
}
float loadTime = Time.realtimeSinceStartup - startTime;
Debug.Log($"즉시 로딩 완료: {loadTime:F2}초, {loadedSizeMB:F1}MB 로딩됨");
isInitialLoadComplete = true;
// 게임 시작 가능 신호 전송
SendGameReadySignal(loadTime, loadedSizeMB);
// 백그라운드에서 나머지 에셋 로딩 시작
StartCoroutine(BackgroundStreamingProcess());
}
IEnumerator LoadAssetCoroutine(StreamingConfig config)
{
if (loadedAssets.Contains(config.assetName))
{
yield break;
}
downloadingAssets.Add(config.assetName);
// 의존성 에셋 먼저 로딩
foreach (var dependency in config.dependencies)
{
if (!loadedAssets.Contains(dependency))
{
var dependencyConfig = configMap[dependency];
yield return StartCoroutine(LoadAssetCoroutine(dependencyConfig));
}
}
// 에셋 다운로드
yield return StartCoroutine(DownloadAsset(config));
downloadingAssets.Remove(config.assetName);
loadedAssets.Add(config.assetName);
}
IEnumerator DownloadAsset(StreamingConfig config)
{
string assetUrl = baseUrl + config.assetName;
string cachePath = GetAssetCachePath(config.assetName);
Debug.Log($"에셋 스트리밍 시작: {config.assetName} ({config.sizeMB:F1}MB)");
float startTime = Time.realtimeSinceStartup;
// 로컬 캐시 확인
if (File.Exists(cachePath))
{
yield return StartCoroutine(LoadFromCache(config, cachePath));
yield break;
}
// 네트워크 다운로드
using (var www = new WWW(assetUrl))
{
float timeoutTime = Time.realtimeSinceStartup + config.timeoutSeconds;
while (!www.isDone && Time.realtimeSinceStartup < timeoutTime)
{
// 진행률 업데이트
UpdateDownloadProgress(config.assetName, www.progress);
yield return null;
}
if (www.isDone && string.IsNullOrEmpty(www.error))
{
// 캐시에 저장
Directory.CreateDirectory(Path.GetDirectoryName(cachePath));
File.WriteAllBytes(cachePath, www.bytes);
float downloadTime = Time.realtimeSinceStartup - startTime;
Debug.Log($"스트리밍 완료: {config.assetName} ({downloadTime:F2}초)");
SendStreamingAnalytics(config.assetName, true, downloadTime, config.sizeMB);
}
else
{
Debug.LogError($"스트리밍 실패: {config.assetName} - {www.error}");
SendStreamingAnalytics(config.assetName, false, 0, config.sizeMB);
}
}
}
IEnumerator LoadFromCache(StreamingConfig config, string cachePath)
{
Debug.Log($"캐시에서 로딩: {config.assetName}");
// 캐시 파일 검증
var fileInfo = new FileInfo(cachePath);
float expectedSizeMB = config.sizeMB;
float actualSizeMB = fileInfo.Length / (1024f * 1024f);
if (Mathf.Abs(actualSizeMB - expectedSizeMB) > 0.1f)
{
Debug.LogWarning($"캐시 파일 크기 불일치: {config.assetName} (예상: {expectedSizeMB:F1}MB, 실제: {actualSizeMB:F1}MB)");
File.Delete(cachePath);
// 다시 다운로드
yield return StartCoroutine(DownloadAsset(config));
yield break;
}
yield return null; // 1프레임 대기
Debug.Log($"캐시 로딩 완료: {config.assetName}");
}
IEnumerator BackgroundStreamingProcess()
{
while (true)
{
// 큐에서 다음 에셋 처리
if (downloadQueue.Count > 0 && activeDownloads < maxConcurrentDownloads)
{
var nextAsset = downloadQueue.Dequeue();
StartCoroutine(BackgroundDownload(nextAsset));
}
// Idle 상태에서 preload 에셋 로딩
if (enablePreloadInIdle && IsPlayerIdle() && activeDownloads == 0)
{
var preloadAssets = System.Array.FindAll(streamingAssets,
a => a.priority == StreamingPriority.Preload && !loadedAssets.Contains(a.assetName));
if (preloadAssets.Length > 0)
{
var nextPreload = preloadAssets[0];
QueueAssetForDownload(nextPreload);
}
}
yield return new WaitForSeconds(1f);
}
}
IEnumerator BackgroundDownload(StreamingConfig config)
{
activeDownloads++;
yield return StartCoroutine(LoadAssetCoroutine(config));
activeDownloads--;
}
void QueueAssetForDownload(StreamingConfig config)
{
if (!downloadingAssets.Contains(config.assetName) &&
!loadedAssets.Contains(config.assetName))
{
downloadQueue.Enqueue(config);
}
}
IEnumerator MonitorNetworkStatus()
{
while (true)
{
// 네트워크 상태 확인
var networkReachability = Application.internetReachability;
if (enableAdaptiveQuality)
{
AdaptQualityBasedOnNetwork(networkReachability);
}
yield return new WaitForSeconds(5f);
}
}
void AdaptQualityBasedOnNetwork(NetworkReachability reachability)
{
switch (reachability)
{
case NetworkReachability.ReachableViaCarrierDataNetwork:
// 모바일 데이터: 압축률 높이고 다운로드 제한
maxConcurrentDownloads = 1;
networkTimeoutSeconds = 30f;
break;
case NetworkReachability.ReachableViaLocalAreaNetwork:
// WiFi: 최적 성능
maxConcurrentDownloads = 3;
networkTimeoutSeconds = 15f;
break;
case NetworkReachability.NotReachable:
// 오프라인: 캐시된 에셋만 사용
maxConcurrentDownloads = 0;
break;
}
}
string GetAssetCachePath(string assetName)
{
return Path.Combine(Application.persistentDataPath, "StreamingCache", assetName);
}
bool IsPlayerIdle()
{
// 플레이어가 유휴 상태인지 확인하는 로직
// 예: 일정 시간 동안 입력이 없었는지, 메뉴 화면에 있는지 등
return Time.realtimeSinceStartup - Time.time > 10f;
}
void UpdateDownloadProgress(string assetName, float progress)
{
// UI에 다운로드 진행률 업데이트
var progressData = new Dictionary<string, object>
{
{"asset_name", assetName},
{"progress", progress}
};
// 이벤트 시스템으로 UI에 알림
AppsInToss.SendEvent("streaming_progress", progressData);
}
void SendGameReadySignal(float loadTime, float loadedSizeMB)
{
var readyData = new Dictionary<string, object>
{
{"load_time", loadTime},
{"loaded_size_mb", loadedSizeMB},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendEvent("game_ready", readyData);
Debug.Log($"게임 시작 준비 완료! 로딩 시간: {loadTime:F2}초");
}
void SendStreamingAnalytics(string assetName, bool success, float downloadTime, float sizeMB)
{
var analyticsData = new Dictionary<string, object>
{
{"asset_name", assetName},
{"success", success},
{"download_time", downloadTime},
{"size_mb", sizeMB},
{"network_type", Application.internetReachability.ToString()},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendAnalytics("streaming_performance", analyticsData);
}
// 공개 API
public bool IsAssetLoaded(string assetName)
{
return loadedAssets.Contains(assetName);
}
public bool IsInitialLoadComplete()
{
return isInitialLoadComplete;
}
public void RequestAssetLoad(string assetName, StreamingPriority priority = StreamingPriority.High)
{
if (configMap.ContainsKey(assetName) && !IsAssetLoaded(assetName))
{
var config = configMap[assetName];
config.priority = priority; // 우선순위 업데이트
QueueAssetForDownload(config);
}
}
public float GetLoadingProgress()
{
if (streamingAssets.Length == 0) return 1f;
float totalAssets = streamingAssets.Length;
float loadedCount = loadedAssets.Count;
return loadedCount / totalAssets;
}
public string[] GetLoadedAssets()
{
var result = new string[loadedAssets.Count];
loadedAssets.CopyTo(result);
return result;
}
public long GetCacheSize()
{
string cacheDir = Path.Combine(Application.persistentDataPath, "StreamingCache");
if (!Directory.Exists(cacheDir))
{
return 0;
}
long totalSize = 0;
var files = Directory.GetFiles(cacheDir, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
totalSize += new FileInfo(file).Length;
}
return totalSize;
}
public void ClearCache()
{
string cacheDir = Path.Combine(Application.persistentDataPath, "StreamingCache");
if (Directory.Exists(cacheDir))
{
Directory.Delete(cacheDir, true);
Debug.Log("스트리밍 캐시 정리 완료");
}
loadedAssets.Clear();
}
}2. 최적화 전략
에셋 우선순위 설정
c#
[System.Serializable]
public class AssetPriorityManager
{
[Header("게임 시작 필수 에셋")]
public string[] immediateAssets = {
"core_ui", // 핵심 UI 시스템
"player_controller", // 플레이어 조작
"first_level_mesh", // 첫 번째 레벨 메시
"essential_sounds" // 핵심 사운드
};
[Header("조기 로딩 권장 에셋")]
public string[] highPriorityAssets = {
"level_backgrounds", // 레벨 배경
"enemy_sprites", // 적 스프라이트
"effect_particles", // 이펙트 파티클
"ui_animations" // UI 애니메이션
};
[Header("지연 로딩 가능 에셋")]
public string[] lowPriorityAssets = {
"cutscene_videos", // 컷신 비디오
"boss_models", // 보스 모델
"extra_music", // 추가 음악
"optional_effects" // 선택적 이펙트
};
public StreamingPriority GetAssetPriority(string assetName)
{
if (System.Array.Exists(immediateAssets, a => a == assetName))
return StreamingPriority.Immediate;
else if (System.Array.Exists(highPriorityAssets, a => a == assetName))
return StreamingPriority.High;
else if (System.Array.Exists(lowPriorityAssets, a => a == assetName))
return StreamingPriority.Low;
else
return StreamingPriority.Medium;
}
}스트리밍 UI 시스템
c#
using UnityEngine;
using UnityEngine.UI;
public class StreamingProgressUI : MonoBehaviour
{
[Header("UI 컴포넌트")]
public Slider progressBar;
public Text progressText;
public Text statusText;
public Button playButton;
public GameObject loadingPanel;
[Header("설정")]
public float minimumDisplayTime = 2f; // 최소 로딩 화면 표시 시간
public bool hideProgressAfterReady = true;
private float displayStartTime;
private bool isGameReady = false;
void Start()
{
displayStartTime = Time.realtimeSinceStartup;
// 이벤트 리스너 등록
AppsInToss.OnEvent += HandleAppsInTossEvent;
// 초기 UI 설정
playButton.interactable = false;
loadingPanel.SetActive(true);
UpdateProgressDisplay();
}
void Update()
{
UpdateProgressDisplay();
// 최소 표시 시간 후 게임 시작 가능
if (isGameReady &&
Time.realtimeSinceStartup - displayStartTime >= minimumDisplayTime)
{
if (hideProgressAfterReady)
{
loadingPanel.SetActive(false);
}
playButton.interactable = true;
}
}
void UpdateProgressDisplay()
{
if (AppsInTossAutoStreaming.Instance == null) return;
float progress = AppsInTossAutoStreaming.Instance.GetLoadingProgress();
bool initialComplete = AppsInTossAutoStreaming.Instance.IsInitialLoadComplete();
// 진행률 업데이트
progressBar.value = progress;
progressText.text = $"{progress * 100f:F0}%";
// 상태 텍스트 업데이트
if (initialComplete)
{
statusText.text = "게임 시작 준비 완료!";
statusText.color = Color.green;
}
else
{
statusText.text = "에셋 로딩 중...";
statusText.color = Color.white;
}
}
void HandleAppsInTossEvent(string eventName, Dictionary<string, object> data)
{
switch (eventName)
{
case "game_ready":
isGameReady = true;
break;
case "streaming_progress":
string assetName = data["asset_name"] as string;
float assetProgress = (float)data["progress"];
// 개별 에셋 진행률 표시
statusText.text = $"로딩 중: {assetName} ({assetProgress * 100f:F0}%)";
break;
}
}
public void OnPlayButtonClicked()
{
if (isGameReady)
{
// 게임 씬 로드
UnityEngine.SceneManagement.SceneManager.LoadScene("GameScene");
}
}
void OnDestroy()
{
AppsInToss.OnEvent -= HandleAppsInTossEvent;
}
}3. 성능 모니터링
스트리밍 성능 추적
c#
public class StreamingPerformanceMonitor : MonoBehaviour
{
[System.Serializable]
public class PerformanceMetrics
{
public float initialLoadTime;
public float totalDownloadSize;
public int failedDownloads;
public float averageDownloadSpeed;
public string networkType;
public Dictionary<string, float> assetLoadTimes;
public PerformanceMetrics()
{
assetLoadTimes = new Dictionary<string, float>();
}
}
public PerformanceMetrics metrics = new PerformanceMetrics();
void Start()
{
AppsInToss.OnAnalytics += HandleAnalyticsEvent;
}
void HandleAnalyticsEvent(string eventName, Dictionary<string, object> data)
{
if (eventName == "streaming_performance")
{
string assetName = data["asset_name"] as string;
bool success = (bool)data["success"];
float downloadTime = (float)data["download_time"];
float sizeMB = (float)data["size_mb"];
if (success)
{
metrics.assetLoadTimes[assetName] = downloadTime;
metrics.totalDownloadSize += sizeMB;
if (downloadTime > 0)
{
float speedMBps = sizeMB / downloadTime;
UpdateAverageSpeed(speedMBps);
}
}
else
{
metrics.failedDownloads++;
}
// 성능 보고서 생성
if (metrics.assetLoadTimes.Count % 10 == 0)
{
GeneratePerformanceReport();
}
}
}
void UpdateAverageSpeed(float newSpeed)
{
if (metrics.averageDownloadSpeed == 0)
{
metrics.averageDownloadSpeed = newSpeed;
}
else
{
metrics.averageDownloadSpeed = (metrics.averageDownloadSpeed + newSpeed) / 2f;
}
}
void GeneratePerformanceReport()
{
var report = new Dictionary<string, object>
{
{"total_assets_loaded", metrics.assetLoadTimes.Count},
{"total_download_size_mb", metrics.totalDownloadSize},
{"failed_downloads", metrics.failedDownloads},
{"average_download_speed_mbps", metrics.averageDownloadSpeed},
{"network_type", Application.internetReachability.ToString()},
{"device_model", SystemInfo.deviceModel},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendAnalytics("streaming_report", report);
Debug.Log($"스트리밍 성능 보고서 전송: {metrics.assetLoadTimes.Count}개 에셋 로딩 완료");
}
void OnDestroy()
{
AppsInToss.OnAnalytics -= HandleAnalyticsEvent;
}
}4. 에디터 도구
AutoStreaming 설정 도구
c#
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
public class AutoStreamingConfigWindow : EditorWindow
{
private AppsInTossAutoStreaming streamingComponent;
private Vector2 scrollPosition;
private bool showAnalytics = true;
[MenuItem("AppsInToss/AutoStreaming 설정")]
public static void ShowWindow()
{
GetWindow<AutoStreamingConfigWindow>("AutoStreaming 설정");
}
void OnGUI()
{
GUILayout.Label("AutoStreaming 설정", EditorStyles.boldLabel);
// 컴포넌트 참조
streamingComponent = EditorGUILayout.ObjectField(
"AutoStreaming 컴포넌트",
streamingComponent,
typeof(AppsInTossAutoStreaming),
true
) as AppsInTossAutoStreaming;
if (streamingComponent == null)
{
EditorGUILayout.HelpBox("AutoStreaming 컴포넌트를 선택해주세요.", MessageType.Warning);
return;
}
EditorGUILayout.Space();
// 에셋 분석 결과 표시
if (GUILayout.Button("프로젝트 에셋 분석"))
{
AnalyzeProjectAssets();
}
EditorGUILayout.Space();
// 스트리밍 설정 표시
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
SerializedObject so = new SerializedObject(streamingComponent);
SerializedProperty streamingAssets = so.FindProperty("streamingAssets");
EditorGUILayout.PropertyField(streamingAssets, true);
so.ApplyModifiedProperties();
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
// 유틸리티 버튼들
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("설정 검증"))
{
ValidateStreamingConfig();
}
if (GUILayout.Button("최적화 제안"))
{
SuggestOptimizations();
}
if (GUILayout.Button("테스트 빌드"))
{
BuildStreamingTest();
}
EditorGUILayout.EndHorizontal();
}
void AnalyzeProjectAssets()
{
var assetGuids = AssetDatabase.FindAssets("t:Object");
var assetSizes = new Dictionary<string, float>();
foreach (var guid in assetGuids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
var fileInfo = new FileInfo(assetPath);
if (fileInfo.Exists)
{
float sizeMB = fileInfo.Length / (1024f * 1024f);
assetSizes[Path.GetFileName(assetPath)] = sizeMB;
}
}
// 크기별 정렬
var sortedAssets = new List<KeyValuePair<string, float>>(assetSizes);
sortedAssets.Sort((a, b) => b.Value.CompareTo(a.Value));
Debug.Log("=== 프로젝트 에셋 크기 분석 ===");
for (int i = 0; i < Mathf.Min(10, sortedAssets.Count); i++)
{
var asset = sortedAssets[i];
Debug.Log($"{i + 1}. {asset.Key}: {asset.Value:F2}MB");
}
float totalSizeMB = 0f;
foreach (var size in assetSizes.Values)
{
totalSizeMB += size;
}
Debug.Log($"총 에셋 크기: {totalSizeMB:F2}MB");
Debug.Log($"권장 즉시 로딩 크기: {streamingComponent.initialLoadThresholdMB}MB");
}
void ValidateStreamingConfig()
{
var issues = new List<string>();
// 설정 검증
if (streamingComponent.streamingAssets.Length == 0)
{
issues.Add("스트리밍 에셋이 설정되지 않았습니다.");
}
// 즉시 로딩 에셋 크기 체크
float immediateSizeMB = 0f;
foreach (var asset in streamingComponent.streamingAssets)
{
if (asset.priority == AppsInTossAutoStreaming.StreamingPriority.Immediate)
{
immediateSizeMB += asset.sizeMB;
}
}
if (immediateSizeMB > streamingComponent.initialLoadThresholdMB)
{
issues.Add($"즉시 로딩 에셋 크기가 임계값을 초과합니다: {immediateSizeMB:F1}MB > {streamingComponent.initialLoadThresholdMB}MB");
}
// 결과 표시
if (issues.Count == 0)
{
EditorUtility.DisplayDialog("검증 완료", "AutoStreaming 설정이 올바릅니다.", "확인");
}
else
{
string message = "다음 문제들을 해결해주세요:\n\n" + string.Join("\n", issues);
EditorUtility.DisplayDialog("검증 실패", message, "확인");
}
}
void SuggestOptimizations()
{
var suggestions = new List<string>();
// 큰 에셋 우선순위 확인
foreach (var asset in streamingComponent.streamingAssets)
{
if (asset.sizeMB > 5f && asset.priority == AppsInTossAutoStreaming.StreamingPriority.Immediate)
{
suggestions.Add($"큰 에셋 '{asset.assetName}' ({asset.sizeMB:F1}MB)의 우선순위를 낮추는 것을 고려해보세요.");
}
}
// 의존성 체크
var dependencyCount = new Dictionary<string, int>();
foreach (var asset in streamingComponent.streamingAssets)
{
foreach (var dep in asset.dependencies)
{
dependencyCount[dep] = dependencyCount.ContainsKey(dep) ? dependencyCount[dep] + 1 : 1;
}
}
foreach (var kvp in dependencyCount)
{
if (kvp.Value > 3)
{
suggestions.Add($"에셋 '{kvp.Key}'이 {kvp.Value}개의 다른 에셋에서 참조됩니다. 공통 번들로 분리를 고려해보세요.");
}
}
// 결과 표시
if (suggestions.Count == 0)
{
EditorUtility.DisplayDialog("최적화 분석", "현재 설정이 최적화되어 있습니다.", "확인");
}
else
{
string message = "다음 최적화를 고려해보세요:\n\n" + string.Join("\n\n", suggestions);
EditorUtility.DisplayDialog("최적화 제안", message, "확인");
}
}
void BuildStreamingTest()
{
if (EditorUtility.DisplayDialog("테스트 빌드", "AutoStreaming 테스트 빌드를 시작하시겠습니까?", "시작", "취소"))
{
// 테스트용 빌드 설정
BuildPlayerOptions buildOptions = new BuildPlayerOptions();
buildOptions.scenes = new[] { EditorBuildSettings.scenes[0].path };
buildOptions.locationPathName = "Builds/StreamingTest/StreamingTest";
buildOptions.target = BuildTarget.WebGL;
buildOptions.options = BuildOptions.Development;
BuildPipeline.BuildPlayer(buildOptions);
Debug.Log("AutoStreaming 테스트 빌드 완료");
}
}
}
#endifAutoStreaming을 통해 사용자가 즉시 게임을 시작할 수 있도록 하되, 백그라운드에서 추가 콘텐츠를 스마트하게 로딩하여 끊김 없는 게임 경험을 제공하세요.
