825 lines
35 KiB
C#
825 lines
35 KiB
C#
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UVC.Core;
|
|
|
|
namespace Sample
|
|
{
|
|
/// <summary>
|
|
/// Injector 샘플 테스트 클래스 - 4가지 타입별 의존성 주입 사용법을 테스트합니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>[ Unity Editor 설정 가이드 ]</b></para>
|
|
///
|
|
/// <para><b>1. 씬 구조 설정</b></para>
|
|
/// <code>
|
|
/// [SampleScene]
|
|
/// ├── AppContext ← InjectorSampleAppContext 컴포넌트 추가
|
|
/// ├── SceneContext ← InjectorSampleSceneContext 컴포넌트 추가
|
|
/// ├── NetworkManager ← InjectorSampleNetworkManager 컴포넌트 추가 (선택적, 자동 생성 가능)
|
|
/// ├── Player ← InjectorSamplePlayerController 컴포넌트 추가
|
|
/// ├── TestRunner ← InjectorSampleTest 컴포넌트 추가 (이 클래스)
|
|
/// └── EnemySpawner ← InjectorSampleEnemySpawnerExample 컴포넌트 추가
|
|
/// </code>
|
|
///
|
|
/// <para><b>2. 프리팹 생성 (Assets/Sample/Prefabs 폴더 권장)</b></para>
|
|
/// <list type="table">
|
|
/// <item><term>InjectorSampleUIManager</term><description>Type C: Prefab 기반 MonoBehaviour (AppContext에 연결)</description></item>
|
|
/// <item><term>InjectorSampleSceneUI</term><description>Scene용 UI 프리팹 (SceneContext에 연결)</description></item>
|
|
/// <item><term>InjectorSampleEnemySpawner</term><description>적 스포너 프리팹 (SceneContext에 연결)</description></item>
|
|
/// <item><term>InjectorSampleEnemy</term><description>동적 생성될 적 프리팹 (EnemySpawnerExample에 연결)</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>3. AppContext Inspector 설정</b></para>
|
|
/// <list type="bullet">
|
|
/// <item><description>UI Manager Prefab: InjectorSampleUIManager 프리팹 연결</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>4. SceneContext Inspector 설정</b></para>
|
|
/// <list type="bullet">
|
|
/// <item><description>Scene UI Prefab: InjectorSampleSceneUI 프리팹 연결 (선택)</description></item>
|
|
/// <item><description>Enemy Spawner Prefab: InjectorSampleEnemySpawner 프리팹 연결 (선택)</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>5. EnemySpawnerExample Inspector 설정</b></para>
|
|
/// <list type="bullet">
|
|
/// <item><description>Enemy Prefab: InjectorSampleEnemy 프리팹 연결</description></item>
|
|
/// <item><description>Spawn Point: 스폰 위치 Transform 연결 (선택)</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>6. Singleton 타입별 설정 방법</b></para>
|
|
/// <list type="table">
|
|
/// <item>
|
|
/// <term>InjectorSampleSettingsManager</term>
|
|
/// <description>Singleton<T> (순수 C#) - 씬 설정 불필요, 코드에서 Instance 접근 시 자동 생성</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>InjectorSampleNetworkManager</term>
|
|
/// <description>SingletonApp<T> (MonoBehaviour) - 씬에 배치 가능 또는 Instance 접근 시 자동 생성</description>
|
|
/// </item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>7. 실행 순서 (자동 설정됨)</b></para>
|
|
/// <list type="number">
|
|
/// <item><description>InjectorAppContext (-1000) - App 서비스 등록</description></item>
|
|
/// <item><description>InjectorSceneContext (-900) - Scene 서비스 등록</description></item>
|
|
/// <item><description>일반 MonoBehaviour (0) - 의존성 주입된 상태로 Start 실행</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>[ 등록 순서와 인스턴스 생성 시점 ]</b></para>
|
|
/// <para>Register와 Resolve는 별개의 작업입니다:</para>
|
|
/// <list type="bullet">
|
|
/// <item><description><b>Register:</b> 서비스 메타데이터만 등록 (인스턴스 생성 안 함)</description></item>
|
|
/// <item><description><b>Resolve:</b> 처음 호출될 때 인스턴스 생성 (Lazy Instantiation)</description></item>
|
|
/// </list>
|
|
/// <para>따라서 등록 순서는 중요하지 않으며, Resolve 시점에 의존성이 등록되어 있기만 하면 됩니다.</para>
|
|
///
|
|
/// <para><b>[ Unity 실행 순서 타임라인 ]</b></para>
|
|
/// <code>
|
|
/// ┌─────────────────────────────────────────────────────────────────────┐
|
|
/// │ InjectorAppContext.Awake (-1000) │
|
|
/// │ └→ Injector 생성 + App 서비스 메타데이터 등록 │
|
|
/// ├─────────────────────────────────────────────────────────────────────┤
|
|
/// │ InjectorSceneContext.Awake (-900) │
|
|
/// │ └→ Scene 서비스 메타데이터 등록 │
|
|
/// │ └→ 씬 오브젝트들에 의존성 주입 (InjectGameObject) │
|
|
/// │ └→ 이 시점에 [Inject] 필드에 Resolve하여 인스턴스 할당 │
|
|
/// ├─────────────────────────────────────────────────────────────────────┤
|
|
/// │ InjectorSampleTest.Awake (0) │
|
|
/// │ └→ [Inject] 필드(_logger, _audioManager 등)에 이미 인스턴스 주입됨 │
|
|
/// │ └→ ⚠ 다른 MonoBehaviour의 Awake 실행 순서는 보장 안 됨 │
|
|
/// ├─────────────────────────────────────────────────────────────────────┤
|
|
/// │ InjectorSampleTest.Start │
|
|
/// │ └→ ✅ 안전: 모든 Awake 완료 후 실행, 모든 의존성 사용 가능 │
|
|
/// │ └→ RunAllTests() 호출 │
|
|
/// └─────────────────────────────────────────────────────────────────────┘
|
|
/// </code>
|
|
///
|
|
/// <para><b>[ 의존성 접근 권장 시점 ]</b></para>
|
|
/// <list type="table">
|
|
/// <listheader>
|
|
/// <term>메서드</term>
|
|
/// <description>안전성</description>
|
|
/// </listheader>
|
|
/// <item>
|
|
/// <term>Awake()</term>
|
|
/// <description>⚠ 주의 - [Inject] 필드 사용 가능하나, 다른 컴포넌트 Awake 순서 보장 안 됨</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>Start()</term>
|
|
/// <description>✅ 안전 - 모든 Awake 완료 후 실행, 의존성 사용 권장 (이 클래스에서 사용)</description>
|
|
/// </item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>[ Awake에서 반드시 의존성이 필요한 경우 해결 방법 ]</b></para>
|
|
/// <list type="table">
|
|
/// <listheader>
|
|
/// <term>방법</term>
|
|
/// <description>설명 및 예시</description>
|
|
/// </listheader>
|
|
/// <item>
|
|
/// <term>1. DefaultExecutionOrder</term>
|
|
/// <description>컴포넌트 실행 순서 명시적 지정 (가장 권장)</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>2. 직접 접근</term>
|
|
/// <description>InjectorAppContext.Instance.Get<T>() 사용</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>3. Lazy 패턴</term>
|
|
/// <description>프로퍼티로 첫 접근 시 초기화</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>4. 코루틴 대기</term>
|
|
/// <description>조건 충족까지 대기 후 초기화</description>
|
|
/// </item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>방법 1 예시: DefaultExecutionOrder</b></para>
|
|
/// <code>
|
|
/// [DefaultExecutionOrder(-100)] // InjectorSceneContext(-900) 이후, 일반(0) 이전
|
|
/// public class EarlyInitService : MonoBehaviour
|
|
/// {
|
|
/// [Inject] private ILogService _logger;
|
|
/// private void Awake() { _logger?.Log("Early init"); }
|
|
/// }
|
|
///
|
|
/// [DefaultExecutionOrder(-50)] // EarlyInitService 이후
|
|
/// public class LateInitService : MonoBehaviour
|
|
/// {
|
|
/// [Inject] private EarlyInitService _early; // 이미 초기화됨
|
|
/// private void Awake() { _early.DoSomething(); } // ✅ 안전
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>방법 2 예시: 직접 접근</b></para>
|
|
/// <code>
|
|
/// public class MyComponent : MonoBehaviour
|
|
/// {
|
|
/// private ILogService _logger;
|
|
/// private void Awake()
|
|
/// {
|
|
/// _logger = InjectorAppContext.Instance.Get<ILogService>();
|
|
/// _logger.Log("Awake에서 직접 접근");
|
|
/// }
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>방법 3 예시: Lazy 패턴</b></para>
|
|
/// <code>
|
|
/// public class MyComponent : MonoBehaviour
|
|
/// {
|
|
/// private ILogService _logger;
|
|
/// private ILogService Logger => _logger ??= InjectorAppContext.Instance.Get<ILogService>();
|
|
/// private void Awake() { Logger.Log("Lazy 접근"); }
|
|
/// }
|
|
/// </code>
|
|
///
|
|
/// <para><b>[ 권장 순서 ]</b></para>
|
|
/// <list type="number">
|
|
/// <item><description>Start() 사용 (가장 단순하고 안전)</description></item>
|
|
/// <item><description>DefaultExecutionOrder (명시적 순서 필요 시)</description></item>
|
|
/// <item><description>직접 접근 또는 Lazy 패턴 (Awake 필수인 경우)</description></item>
|
|
/// <item><description>코루틴 대기 (복잡한 초기화 의존성)</description></item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>[ RegisterSingleton vs RegisterInstance 사용 가이드 ]</b></para>
|
|
/// <para>Singleton 타입을 등록할 때는 항상 <b>RegisterSingleton<T>()</b>를 사용하세요.</para>
|
|
///
|
|
/// <list type="table">
|
|
/// <listheader>
|
|
/// <term>상황</term>
|
|
/// <description>등록 방법</description>
|
|
/// </listheader>
|
|
/// <item>
|
|
/// <term>Singleton<T> (순수 C#)</term>
|
|
/// <description>RegisterSingleton<T>()</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>SingletonApp<T> (씬 배치 or 자동 생성)</term>
|
|
/// <description>RegisterSingleton<T>()</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>SingletonScene<T> (씬 배치 or 자동 생성)</term>
|
|
/// <description>RegisterSingleton<T>()</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>일반 MonoBehaviour (씬에 미리 배치)</term>
|
|
/// <description>RegisterInstance<T>(instance)</description>
|
|
/// </item>
|
|
/// </list>
|
|
///
|
|
/// <para><b>[ 왜 SingletonScene도 RegisterSingleton을 사용하나요? ]</b></para>
|
|
/// <para>SingletonScene<T>.Instance 프로퍼티가 내부적으로 FindFirstObjectByType을 사용하여</para>
|
|
/// <para>씬에 이미 배치된 인스턴스를 자동으로 찾기 때문입니다.</para>
|
|
///
|
|
/// <code>
|
|
/// // SingletonScene.Instance 내부 동작
|
|
/// public static T Instance
|
|
/// {
|
|
/// get
|
|
/// {
|
|
/// if (instance == null)
|
|
/// {
|
|
/// instance = (T)FindFirstObjectByType(typeof(T)); // 씬에서 검색
|
|
/// if (instance == null)
|
|
/// instance = new GameObject(typeof(T).Name).AddComponent<T>(); // 없으면 생성
|
|
/// }
|
|
/// return instance;
|
|
/// }
|
|
/// }
|
|
///
|
|
/// // 등록 예시
|
|
/// // ✅ 올바름 - Singleton 타입
|
|
/// Injector.RegisterSingleton<InjectorSampleSettingsManager>(); // Singleton<T>
|
|
/// Injector.RegisterSingleton<InjectorSampleNetworkManager>(); // SingletonApp<T>
|
|
/// Injector.RegisterSingleton<InjectorSampleLevelManager>(); // SingletonScene<T>
|
|
///
|
|
/// // ✅ 올바름 - 일반 MonoBehaviour
|
|
/// [SerializeField] private MyManager myManager;
|
|
/// Injector.RegisterInstance<IMyManager>(myManager);
|
|
///
|
|
/// // ❌ 불필요 - Singleton에 RegisterInstance
|
|
/// // Injector.RegisterInstance(FindObjectOfType<MySceneSingleton>());
|
|
/// </code>
|
|
///
|
|
/// <para><b>8. 테스트 실행 확인</b></para>
|
|
/// <para>Play 모드 실행 시 Console에 다음 로그가 출력되면 성공:</para>
|
|
/// <code>
|
|
/// [InjectorSampleAppContext] All App services registered
|
|
/// ========== Injector Sample Tests ==========
|
|
/// ----- Type A: Pure C# Class -----
|
|
/// [ConsoleLogger] Type A Test: Logger is working!
|
|
/// ...
|
|
/// ========== All Tests Completed ==========
|
|
/// </code>
|
|
/// </remarks>
|
|
public class InjectorSampleTest : MonoBehaviour
|
|
{
|
|
[Header("Test Settings")]
|
|
[SerializeField] private bool runTestOnStart = true;
|
|
|
|
[Inject] private ILogService _logger;
|
|
[Inject] private IAudioManager _audioManager;
|
|
[Inject] private IGameService _gameService;
|
|
[Inject(Optional = true)] private IUIManager _uiManager;
|
|
[Inject] private InjectorSampleSettingsManager _settings;
|
|
|
|
private void Start()
|
|
{
|
|
if (runTestOnStart)
|
|
RunAllTests();
|
|
}
|
|
|
|
[ContextMenu("Run All Tests")]
|
|
public void RunAllTests()
|
|
{
|
|
Debug.Log("========== Injector Sample Tests ==========");
|
|
|
|
TestTypeA_PureCSharp();
|
|
TestTypeB_MonoBehaviour();
|
|
TestTypeC_Prefab();
|
|
TestTypeD_Singleton();
|
|
TestLifecycles();
|
|
|
|
Debug.Log("========== All Tests Completed ==========");
|
|
}
|
|
|
|
private void TestTypeA_PureCSharp()
|
|
{
|
|
Debug.Log("----- Type A: Pure C# Class -----");
|
|
_logger?.Log("Type A Test: Logger is working!");
|
|
_gameService?.Initialize();
|
|
_gameService?.SaveGame();
|
|
}
|
|
|
|
private void TestTypeB_MonoBehaviour()
|
|
{
|
|
Debug.Log("----- Type B: MonoBehaviour (Dynamic) -----");
|
|
_audioManager?.PlayBGM("main_theme");
|
|
_audioManager?.PlaySFX("click");
|
|
}
|
|
|
|
private void TestTypeC_Prefab()
|
|
{
|
|
Debug.Log("----- Type C: Prefab-based MonoBehaviour -----");
|
|
_uiManager?.ShowLoading();
|
|
_uiManager?.HideLoading();
|
|
_uiManager?.ShowPopup("Test", "This is a test popup");
|
|
}
|
|
|
|
private void TestTypeD_Singleton()
|
|
{
|
|
Debug.Log("----- Type D: Existing Singleton -----");
|
|
|
|
// 1. Inject를 통한 접근 테스트
|
|
_logger?.Log($"[Inject] Master Volume: {_settings?.MasterVolume}");
|
|
_logger?.Log($"[Inject] SFX Volume: {_settings?.SFXVolume}");
|
|
_logger?.Log($"[Inject] BGM Volume: {_settings?.BGMVolume}");
|
|
_logger?.Log($"[Inject] Language: {_settings?.Language}");
|
|
|
|
// 2. 직접 Instance 접근 테스트 (순수 C# Singleton)
|
|
var directSettings = InjectorSampleSettingsManager.Instance;
|
|
_logger?.Log($"[Direct] Master Volume: {directSettings.MasterVolume}");
|
|
|
|
// 3. 동일 인스턴스 확인
|
|
bool isSameInstance = ReferenceEquals(_settings, directSettings);
|
|
_logger?.Log($"Same instance check: {isSameInstance}");
|
|
|
|
// 4. 값 변경 및 저장 테스트
|
|
if (_settings != null)
|
|
{
|
|
float originalVolume = _settings.MasterVolume;
|
|
_settings.MasterVolume = 0.5f;
|
|
_settings.SFXVolume = 0.7f;
|
|
_settings.Language = "en";
|
|
_settings.Save();
|
|
|
|
_logger?.Log($"Changed Master Volume: {originalVolume} -> {_settings.MasterVolume}");
|
|
_logger?.Log($"Changed Language: ko -> {_settings.Language}");
|
|
|
|
// 직접 접근으로도 변경된 값 확인
|
|
_logger?.Log($"[Direct] Verify changed value: {directSettings.MasterVolume}");
|
|
|
|
// 값 복원
|
|
_settings.MasterVolume = originalVolume;
|
|
_settings.Language = "ko";
|
|
_settings.Load();
|
|
}
|
|
}
|
|
|
|
private void TestLifecycles()
|
|
{
|
|
Debug.Log("----- Lifecycle Tests -----");
|
|
_logger?.Log("App services persist across scenes");
|
|
|
|
var levelManager = InjectorAppContext.Instance?.TryGet<InjectorSampleLevelManager>();
|
|
if (levelManager != null)
|
|
{
|
|
_logger?.Log($"Current Level: {levelManager.CurrentLevel}");
|
|
levelManager.AddScore(100);
|
|
}
|
|
else
|
|
{
|
|
_logger?.Log("LevelManager not available (Scene service)");
|
|
}
|
|
}
|
|
|
|
[ContextMenu("Test Dynamic Instantiation")]
|
|
public void TestDynamicInstantiation()
|
|
{
|
|
Debug.Log("----- Dynamic Instantiation Test -----");
|
|
|
|
var go = new GameObject("DynamicObject");
|
|
var component = go.AddComponent<InjectorSampleDynamicTestComponent>();
|
|
|
|
InjectorAppContext.Instance?.InjectInto(go);
|
|
component.Test();
|
|
|
|
Destroy(go, 2f);
|
|
}
|
|
|
|
#region Scene Transition & Memory Test
|
|
|
|
/// <summary>
|
|
/// 씬 전환 테스트용 메서드
|
|
/// </summary>
|
|
/// <param name="sceneName">이동할 씬 이름</param>
|
|
/// <remarks>
|
|
/// <para><b>[ 사용법 ]</b></para>
|
|
/// <code>
|
|
/// // 코드에서 호출
|
|
/// GetComponent<InjectorSampleTest>().MoveScene("NextScene");
|
|
///
|
|
/// // 또는 Inspector에서 Button OnClick에 연결
|
|
/// </code>
|
|
/// </remarks>
|
|
public void MoveScene(string sceneName)
|
|
{
|
|
Debug.Log($"[SceneTransition] Moving to scene: {sceneName}");
|
|
LogMemoryStatus("Before Scene Load");
|
|
SceneManager.LoadScene(sceneName);
|
|
}
|
|
|
|
public void TestMoveScene()
|
|
{
|
|
string sceneName = "InjectorSample2";
|
|
Debug.Log($"========== Scene Move Test to {sceneName} ==========");
|
|
LogMemoryStatus("Before Scene Load");
|
|
LogServiceStatus("Before Scene Load");
|
|
|
|
SceneManager.sceneLoaded += OnSceneLoadedForTest;
|
|
SceneManager.LoadScene(sceneName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 씬 전환 테스트 - 현재 씬을 다시 로드하여 Scene 라이프사이클 서비스 정리 확인
|
|
/// </summary>
|
|
[ContextMenu("Test Scene Reload (Memory Check)")]
|
|
public void TestSceneReload()
|
|
{
|
|
Debug.Log("========== Scene Reload Test ==========");
|
|
LogMemoryStatus("Before Reload");
|
|
LogServiceStatus("Before Reload");
|
|
|
|
string currentScene = SceneManager.GetActiveScene().name;
|
|
Debug.Log($"[SceneTransition] Reloading current scene: {currentScene}");
|
|
|
|
// 씬 로드 완료 이벤트 등록
|
|
SceneManager.sceneLoaded += OnSceneLoadedForTest;
|
|
SceneManager.LoadScene(currentScene);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 씬 전환 테스트 - 다른 씬으로 이동 후 다시 돌아오기
|
|
/// </summary>
|
|
/// <param name="targetScene">이동할 대상 씬</param>
|
|
/// <param name="returnAfterSeconds">돌아올 때까지 대기 시간 (초)</param>
|
|
[ContextMenu("Test Scene Round Trip")]
|
|
public void TestSceneRoundTrip()
|
|
{
|
|
TestSceneRoundTrip("InjectorTestScene2", 2f);
|
|
}
|
|
|
|
public void TestSceneRoundTrip(string targetScene, float returnAfterSeconds)
|
|
{
|
|
Debug.Log("========== Scene Round Trip Test ==========");
|
|
LogMemoryStatus("Before Round Trip");
|
|
LogServiceStatus("Before Round Trip");
|
|
|
|
string originalScene = SceneManager.GetActiveScene().name;
|
|
Debug.Log($"[SceneTransition] {originalScene} -> {targetScene} -> {originalScene}");
|
|
|
|
// 원래 씬 이름 저장 (static으로 씬 전환 후에도 유지)
|
|
_returnSceneName = originalScene;
|
|
_returnDelay = returnAfterSeconds;
|
|
|
|
SceneManager.sceneLoaded += OnSceneLoadedForRoundTrip;
|
|
SceneManager.LoadScene(targetScene);
|
|
}
|
|
|
|
private static string _returnSceneName;
|
|
private static float _returnDelay;
|
|
private static bool _isReturning;
|
|
private static bool _pendingSceneReloadTest;
|
|
private static bool _pendingRoundTripTest;
|
|
private static readonly WaitForSeconds _waitOneSecond = new(1f);
|
|
|
|
/// <summary>
|
|
/// 씬 로드 완료 후 새로운 씬의 InjectorSampleTest 인스턴스에서 처리
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
// 씬 로드 후 대기 중인 테스트가 있으면 실행
|
|
if (_pendingSceneReloadTest)
|
|
{
|
|
_pendingSceneReloadTest = false;
|
|
OnSceneReloadCompleted();
|
|
}
|
|
else if (_pendingRoundTripTest)
|
|
{
|
|
OnRoundTripSceneLoaded();
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
// 이벤트 핸들러 정리 (씬 전환 시 호출됨)
|
|
SceneManager.sceneLoaded -= OnSceneLoadedForTest;
|
|
SceneManager.sceneLoaded -= OnSceneLoadedForRoundTrip;
|
|
}
|
|
|
|
private static void OnSceneLoadedForTest(Scene scene, LoadSceneMode mode)
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoadedForTest;
|
|
Debug.Log($"[SceneTransition] Scene loaded: {scene.name}");
|
|
|
|
// 새로운 씬의 InjectorSampleTest에서 처리하도록 플래그 설정
|
|
_pendingSceneReloadTest = true;
|
|
}
|
|
|
|
private void OnSceneReloadCompleted()
|
|
{
|
|
LogMemoryStatusStatic("After Reload");
|
|
LogServiceStatusStatic("After Reload");
|
|
|
|
// GC 강제 실행 후 메모리 재확인
|
|
StartCoroutine(ForceGCAndLogMemory());
|
|
}
|
|
|
|
private static void OnSceneLoadedForRoundTrip(Scene scene, LoadSceneMode mode)
|
|
{
|
|
SceneManager.sceneLoaded -= OnSceneLoadedForRoundTrip;
|
|
|
|
Debug.Log($"[SceneTransition] Arrived at: {scene.name}");
|
|
LogMemoryStatusStatic($"At {scene.name}");
|
|
LogServiceStatusStatic($"At {scene.name}");
|
|
|
|
// 새로운 씬의 InjectorSampleTest에서 처리하도록 플래그 설정
|
|
_pendingRoundTripTest = true;
|
|
}
|
|
|
|
private void OnRoundTripSceneLoaded()
|
|
{
|
|
if (!_isReturning && SceneManager.GetActiveScene().name != _returnSceneName)
|
|
{
|
|
_isReturning = true;
|
|
_pendingRoundTripTest = false;
|
|
// 잠시 대기 후 원래 씬으로 복귀
|
|
StartCoroutine(ReturnToOriginalScene());
|
|
}
|
|
else if (_isReturning)
|
|
{
|
|
_isReturning = false;
|
|
_pendingRoundTripTest = false;
|
|
Debug.Log("========== Round Trip Completed ==========");
|
|
StartCoroutine(ForceGCAndLogMemory());
|
|
}
|
|
}
|
|
|
|
private System.Collections.IEnumerator ReturnToOriginalScene()
|
|
{
|
|
Debug.Log($"[SceneTransition] Waiting {_returnDelay}s before returning...");
|
|
yield return new WaitForSeconds(_returnDelay);
|
|
|
|
Debug.Log($"[SceneTransition] Returning to: {_returnSceneName}");
|
|
LogMemoryStatusStatic("Before Return");
|
|
|
|
SceneManager.sceneLoaded += OnSceneLoadedForRoundTrip;
|
|
SceneManager.LoadScene(_returnSceneName);
|
|
}
|
|
|
|
private System.Collections.IEnumerator ForceGCAndLogMemory()
|
|
{
|
|
Debug.Log("[Memory] Forcing GC collection...");
|
|
|
|
// GC 강제 실행
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
System.GC.Collect();
|
|
|
|
// 1프레임 대기
|
|
yield return null;
|
|
|
|
// Resources.UnloadUnusedAssets는 코루틴으로 실행
|
|
var unloadOperation = Resources.UnloadUnusedAssets();
|
|
yield return unloadOperation;
|
|
|
|
LogMemoryStatus("After GC");
|
|
Debug.Log("========== Memory Check Completed ==========");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 메모리 상태를 로그로 출력
|
|
/// </summary>
|
|
/// <param name="label">로그 라벨</param>
|
|
[ContextMenu("Log Memory Status")]
|
|
public void LogMemoryStatus()
|
|
{
|
|
LogMemoryStatus("Current");
|
|
}
|
|
|
|
public void LogMemoryStatus(string label)
|
|
{
|
|
// Unity 메모리 정보
|
|
long totalMemory = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong();
|
|
long usedMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong();
|
|
long monoHeap = UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong();
|
|
long monoUsed = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|
|
|
Debug.Log($"[Memory] === {label} ===");
|
|
Debug.Log($"[Memory] Total Reserved: {FormatBytes(totalMemory)}");
|
|
Debug.Log($"[Memory] Total Allocated: {FormatBytes(usedMemory)}");
|
|
Debug.Log($"[Memory] Mono Heap: {FormatBytes(monoHeap)}");
|
|
Debug.Log($"[Memory] Mono Used: {FormatBytes(monoUsed)}");
|
|
|
|
// GC 정보
|
|
Debug.Log($"[Memory] GC Total Memory: {FormatBytes(System.GC.GetTotalMemory(false))}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 등록된 서비스 상태를 로그로 출력
|
|
/// </summary>
|
|
/// <param name="label">로그 라벨</param>
|
|
[ContextMenu("Log Service Status")]
|
|
public void LogServiceStatus()
|
|
{
|
|
LogServiceStatus("Current");
|
|
}
|
|
|
|
public void LogServiceStatus(string label)
|
|
{
|
|
Debug.Log($"[Services] === {label} ===");
|
|
|
|
var appContext = InjectorAppContext.Instance;
|
|
if (appContext == null)
|
|
{
|
|
Debug.Log("[Services] InjectorAppContext not found!");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[Services] AppContext Initialized: {appContext.IsInitialized}");
|
|
|
|
// App 라이프사이클 서비스 확인
|
|
var logger = appContext.TryGet<ILogService>();
|
|
var audio = appContext.TryGet<IAudioManager>();
|
|
var game = appContext.TryGet<IGameService>();
|
|
var ui = appContext.TryGet<IUIManager>();
|
|
var settings = appContext.TryGet<InjectorSampleSettingsManager>();
|
|
|
|
Debug.Log($"[Services] App Services:");
|
|
Debug.Log($" - ILogService: {(logger != null ? "OK" : "NULL")}");
|
|
Debug.Log($" - IAudioManager: {(audio != null ? "OK" : "NULL")} {GetMonoBehaviourStatus(audio)}");
|
|
Debug.Log($" - IGameService: {(game != null ? "OK" : "NULL")}");
|
|
Debug.Log($" - IUIManager: {(ui != null ? "OK" : "NULL")} {GetMonoBehaviourStatus(ui)}");
|
|
Debug.Log($" - SettingsManager: {(settings != null ? "OK" : "NULL")}");
|
|
|
|
// Scene 라이프사이클 서비스 확인
|
|
var sceneContext = FindAnyObjectByType<InjectorSceneContext>();
|
|
Debug.Log($"[Services] SceneContext: {(sceneContext != null ? $"Found ({sceneContext.IsInitialized})" : "NULL")}");
|
|
|
|
var levelManager = appContext.TryGet<InjectorSampleLevelManager>();
|
|
var enemySpawner = appContext.TryGet<InjectorSampleEnemySpawner>();
|
|
|
|
Debug.Log($"[Services] Scene Services:");
|
|
Debug.Log($" - LevelManager: {(levelManager != null ? "OK" : "NULL (expected after scene change)")}");
|
|
Debug.Log($" - EnemySpawner: {(enemySpawner != null ? "OK" : "NULL (expected after scene change)")} {GetMonoBehaviourStatus(enemySpawner)}");
|
|
}
|
|
|
|
private string GetMonoBehaviourStatus(object obj)
|
|
{
|
|
if (obj is MonoBehaviour mb)
|
|
{
|
|
if (mb == null) return "(Destroyed)";
|
|
return $"(GameObject: {mb.gameObject.name})";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private static string FormatBytes(long bytes)
|
|
{
|
|
string[] sizes = { "B", "KB", "MB", "GB" };
|
|
int order = 0;
|
|
double size = bytes;
|
|
|
|
while (size >= 1024 && order < sizes.Length - 1)
|
|
{
|
|
order++;
|
|
size /= 1024;
|
|
}
|
|
|
|
return $"{size:0.##} {sizes[order]}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static 버전의 메모리 상태 로그 - 씬 전환 중에도 호출 가능
|
|
/// </summary>
|
|
private static void LogMemoryStatusStatic(string label)
|
|
{
|
|
long totalMemory = UnityEngine.Profiling.Profiler.GetTotalReservedMemoryLong();
|
|
long usedMemory = UnityEngine.Profiling.Profiler.GetTotalAllocatedMemoryLong();
|
|
long monoHeap = UnityEngine.Profiling.Profiler.GetMonoHeapSizeLong();
|
|
long monoUsed = UnityEngine.Profiling.Profiler.GetMonoUsedSizeLong();
|
|
|
|
Debug.Log($"[Memory] === {label} ===");
|
|
Debug.Log($"[Memory] Total Reserved: {FormatBytes(totalMemory)}");
|
|
Debug.Log($"[Memory] Total Allocated: {FormatBytes(usedMemory)}");
|
|
Debug.Log($"[Memory] Mono Heap: {FormatBytes(monoHeap)}");
|
|
Debug.Log($"[Memory] Mono Used: {FormatBytes(monoUsed)}");
|
|
Debug.Log($"[Memory] GC Total Memory: {FormatBytes(System.GC.GetTotalMemory(false))}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Static 버전의 서비스 상태 로그 - 씬 전환 중에도 호출 가능
|
|
/// </summary>
|
|
private static void LogServiceStatusStatic(string label)
|
|
{
|
|
Debug.Log($"[Services] === {label} ===");
|
|
|
|
var appContext = InjectorAppContext.Instance;
|
|
if (appContext == null)
|
|
{
|
|
Debug.Log("[Services] InjectorAppContext not found!");
|
|
return;
|
|
}
|
|
|
|
Debug.Log($"[Services] AppContext Initialized: {appContext.IsInitialized}");
|
|
|
|
// App 라이프사이클 서비스 확인
|
|
var logger = appContext.TryGet<ILogService>();
|
|
var audio = appContext.TryGet<IAudioManager>();
|
|
var game = appContext.TryGet<IGameService>();
|
|
var ui = appContext.TryGet<IUIManager>();
|
|
var settings = appContext.TryGet<InjectorSampleSettingsManager>();
|
|
|
|
Debug.Log($"[Services] App Services:");
|
|
Debug.Log($" - ILogService: {(logger != null ? "OK" : "NULL")}");
|
|
Debug.Log($" - IAudioManager: {(audio != null ? "OK" : "NULL")} {GetMonoBehaviourStatusStatic(audio)}");
|
|
Debug.Log($" - IGameService: {(game != null ? "OK" : "NULL")}");
|
|
Debug.Log($" - IUIManager: {(ui != null ? "OK" : "NULL")} {GetMonoBehaviourStatusStatic(ui)}");
|
|
Debug.Log($" - SettingsManager: {(settings != null ? "OK" : "NULL")}");
|
|
|
|
// Scene 라이프사이클 서비스 확인
|
|
var sceneContext = Object.FindAnyObjectByType<InjectorSceneContext>();
|
|
Debug.Log($"[Services] SceneContext: {(sceneContext != null ? $"Found ({sceneContext.IsInitialized})" : "NULL")}");
|
|
|
|
var levelManager = appContext.TryGet<InjectorSampleLevelManager>();
|
|
var enemySpawner = appContext.TryGet<InjectorSampleEnemySpawner>();
|
|
|
|
Debug.Log($"[Services] Scene Services:");
|
|
Debug.Log($" - LevelManager: {(levelManager != null ? "OK" : "NULL (expected after scene change)")}");
|
|
Debug.Log($" - EnemySpawner: {(enemySpawner != null ? "OK" : "NULL (expected after scene change)")} {GetMonoBehaviourStatusStatic(enemySpawner)}");
|
|
}
|
|
|
|
private static string GetMonoBehaviourStatusStatic(object obj)
|
|
{
|
|
if (obj is MonoBehaviour mb)
|
|
{
|
|
if (mb == null) return "(Destroyed)";
|
|
return $"(GameObject: {mb.gameObject.name})";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 메모리 누수 테스트 - 여러 번 씬 전환 후 메모리 증가 확인
|
|
/// </summary>
|
|
/// <param name="iterations">반복 횟수</param>
|
|
[ContextMenu("Test Memory Leak (5 iterations)")]
|
|
public void TestMemoryLeak()
|
|
{
|
|
TestMemoryLeak(5);
|
|
}
|
|
|
|
public void TestMemoryLeak(int iterations)
|
|
{
|
|
StartCoroutine(MemoryLeakTestCoroutine(iterations));
|
|
}
|
|
|
|
private System.Collections.IEnumerator MemoryLeakTestCoroutine(int iterations)
|
|
{
|
|
Debug.Log($"========== Memory Leak Test ({iterations} iterations) ==========");
|
|
string currentScene = SceneManager.GetActiveScene().name;
|
|
|
|
long initialMemory = System.GC.GetTotalMemory(true);
|
|
Debug.Log($"[MemoryLeak] Initial GC Memory: {FormatBytes(initialMemory)}");
|
|
|
|
for (int i = 0; i < iterations; i++)
|
|
{
|
|
Debug.Log($"[MemoryLeak] Iteration {i + 1}/{iterations}");
|
|
|
|
// 씬 리로드
|
|
var loadOp = SceneManager.LoadSceneAsync(currentScene);
|
|
yield return loadOp;
|
|
|
|
// 1초 대기
|
|
yield return _waitOneSecond;
|
|
|
|
// GC 실행
|
|
System.GC.Collect();
|
|
System.GC.WaitForPendingFinalizers();
|
|
System.GC.Collect();
|
|
|
|
yield return Resources.UnloadUnusedAssets();
|
|
|
|
long currentMemory = System.GC.GetTotalMemory(false);
|
|
long diff = currentMemory - initialMemory;
|
|
Debug.Log($"[MemoryLeak] Iteration {i + 1} - GC Memory: {FormatBytes(currentMemory)} (diff: {FormatBytes(diff)})");
|
|
}
|
|
|
|
long finalMemory = System.GC.GetTotalMemory(true);
|
|
long totalDiff = finalMemory - initialMemory;
|
|
|
|
Debug.Log($"[MemoryLeak] Final GC Memory: {FormatBytes(finalMemory)}");
|
|
Debug.Log($"[MemoryLeak] Total Difference: {FormatBytes(totalDiff)}");
|
|
|
|
if (totalDiff > 1024 * 1024) // 1MB 이상 증가
|
|
{
|
|
Debug.LogWarning($"[MemoryLeak] Potential memory leak detected! Memory increased by {FormatBytes(totalDiff)}");
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("[MemoryLeak] No significant memory leak detected.");
|
|
}
|
|
|
|
Debug.Log("========== Memory Leak Test Completed ==========");
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// 동적 생성 테스트용 컴포넌트
|
|
/// </summary>
|
|
public class InjectorSampleDynamicTestComponent : MonoBehaviour
|
|
{
|
|
[Inject] private ILogService _logger;
|
|
[Inject] private IAudioManager _audioManager;
|
|
|
|
public void Test()
|
|
{
|
|
_logger?.Log("DynamicTestComponent: Injection successful!");
|
|
_audioManager?.PlaySFX("test_sound");
|
|
}
|
|
}
|
|
}
|