quot;Level {level}") .ToReadOnlyReactiveProperty() .AddTo(_disposables); // Health Text with formatting HealthText = _playerModel.Health .CombineLatest(_playerModel.MaxHealth, (current, max) => quot;{current:F0}/{max:F0}") .ToReadOnlyReactiveProperty() .AddTo(_disposables); // Health Bar Fill (0-1 range) HealthBarFill = _playerModel.HealthPercentage .ToReadOnlyReactiveProperty() .AddTo(_disposables); // Dynamic Health Bar Color HealthBarColor = _playerModel.HealthPercentage .Select(GetHealthColor) .ToReadOnlyReactiveProperty() .AddTo(_disposables); // Heal Button State IsHealButtonEnabled = _playerModel.Health .CombineLatest(_playerModel.MaxHealth, (current, max) => current < max) .ToReadOnlyReactiveProperty() .AddTo(_disposables); } private void SetupCommands() { // Heal Command HealCommand = IsHealButtonEnabled .ToReactiveCommand() .AddTo(_disposables); HealCommand.Subscribe(_ => ExecuteHeal()) .AddTo(_disposables); // Take Damage Command TakeDamageCommand = _playerModel.IsAlive .ToReactiveCommand<float>() .AddTo(_disposables); TakeDamageCommand.Subscribe(damage => ExecuteTakeDamage(damage)) .AddTo(_disposables); } private void SubscribeToModelEvents() { // Monitor level changes _playerModel.Level .Pairwise() .Where(pair => pair.Current > pair.Previous) .Subscribe(pair => OnLevelUp(pair.Current)) .AddTo(_disposables); // Monitor death _playerModel.IsAlive .Pairwise() .Where(pair => pair.Previous && !pair.Current) .Subscribe(_ => OnPlayerDied()) .AddTo(_disposables); } private void ExecuteHeal() { var healAmount = 25f; // Could be configurable _playerModel.Heal(healAmount); // Optional: Show heal effect ShowHealEffect(); } private void ExecuteTakeDamage(float damage) { _playerModel.TakeDamage(damage); // Optional: Show damage effect ShowDamageEffect(damage); } private Color GetHealthColor(float healthPercentage) { return healthPercentage switch { > 0.6f => Color.green, > 0.3f => Color.yellow, _ => Color.red }; } private void OnLevelUp(int newLevel) { _levelUpPublisher.Publish(new PlayerLevelUpMessage { NewLevel = newLevel }); } private void OnPlayerDied() { _diedPublisher.Publish(new PlayerDiedMessage { }); } private void ShowHealEffect() { // UI effect logic or message publishing } private void ShowDamageEffect(float damage) { // Damage effect logic } public void Dispose() { _disposables?.Dispose(); } } ``` ### View 계층 구현 ```csharp public class PlayerView : MonoBehaviour { [Header("UI References")] [SerializeField] private Text _levelText; [SerializeField] private Text _healthText; [SerializeField] private Slider _healthBar; [SerializeField] private Image _healthBarFill; [SerializeField] private Button _healButton; [SerializeField] private Button _damageButton; [Header("Animation")] [SerializeField] private Animator _animator; [SerializeField] private ParticleSystem _healEffect; [SerializeField] private ParticleSystem _damageEffect; // ViewModel Dependency [Inject] private PlayerViewModel _viewModel; private readonly CompositeDisposable _disposables = new(); private void Start() { SetupDataBinding(); SetupCommands(); } private void SetupDataBinding() { // Text Binding _viewModel.LevelText .Subscribe(text => _levelText.text = text) .AddTo(_disposables); _viewModel.HealthText .Subscribe(text => _healthText.text = text) .AddTo(_disposables); // Health Bar Binding _viewModel.HealthBarFill .Subscribe(fill => { _healthBar.value = fill; UpdateHealthBarAnimation(fill); }) .AddTo(_disposables); // Health Bar Color _viewModel.HealthBarColor .Subscribe(color => _healthBarFill.color = color) .AddTo(_disposables); // Button State _viewModel.IsHealButtonEnabled .Subscribe(enabled => _healButton.interactable = enabled) .AddTo(_disposables); } private void SetupCommands() { // Heal Button Command _healButton.onClick.AsObservable() .Subscribe(_ => _viewModel.HealCommand.Execute()) .AddTo(_disposables); // Damage Button Command (for testing) _damageButton.onClick.AsObservable() .Subscribe(_ => _viewModel.TakeDamageCommand.Execute(20f)) .AddTo(_disposables); } private void UpdateHealthBarAnimation(float healthPercentage) { if (_animator != null) { _animator.SetFloat("HealthPercentage", healthPercentage); // Trigger different animations based on health if (healthPercentage <= 0.2f) { _animator.SetTrigger("CriticalHealth"); } else if (healthPercentage <= 0.5f) { _animator.SetTrigger("LowHealth"); } } } private void OnDestroy() { _disposables?.Dispose(); } // Called by ViewModel through effects public void PlayHealEffect() { _healEffect?.Play(); // Additional heal animation transform.DOPunchScale(Vector3.one * 0.1f, 0.3f); } public void PlayDamageEffect() { _damageEffect?.Play(); // Screen shake or damage animation transform.DOShakePosition(0.5f, 10f); } } ``` ## 🔧 의존성 주입 (VContainer) 구현 ### DI Container 설정 ```csharp public class GameLifetimeScope : LifetimeScope { [Header("Model References")] [SerializeField] private PlayerModel _playerModel; [SerializeField] private InventoryModel _inventoryModel; [SerializeField] private GameSettings _gameSettings; protected override void Configure(IContainerBuilder builder) { // Register Models as Singletons builder.RegisterInstance(_playerModel).As<IPlayerModel>(); builder.RegisterInstance(_inventoryModel).As<IInventoryModel>(); builder.RegisterInstance(_gameSettings).As<IGameSettings>(); // Register Core Services RegisterCoreServices(builder); // Register ViewModels RegisterViewModels(builder); // Register Controllers RegisterControllers(builder); // Register Infrastructure Services RegisterInfrastructureServices(builder); } private void RegisterCoreServices(IContainerBuilder builder) { // Singleton Services (throughout app lifetime) builder.Register<GameStateManager>(Lifetime.Singleton); builder.Register<AudioManager>(Lifetime.Singleton); builder.Register<SceneManager>(Lifetime.Singleton); // MessagePipe Setup builder.AddMessagePipe(); builder.AddMessageBroker<PlayerLevelUpMessage>(); builder.AddMessageBroker<PlayerDiedMessage>(); builder.AddMessageBroker<InventoryChangedMessage>(); } private void RegisterViewModels(IContainerBuilder builder) { // Scoped ViewModels (per feature/screen) builder.Register<PlayerViewModel>(Lifetime.Scoped); builder.Register<InventoryViewModel>(Lifetime.Scoped); builder.Register<ShopViewModel>(Lifetime.Scoped); builder.Register<SettingsViewModel>(Lifetime.Scoped); } private void RegisterControllers(IContainerBuilder builder) { // Controllers for handling complex business logic builder.Register<GameController>(Lifetime.Singleton); builder.Register<InputController>(Lifetime.Singleton); builder.Register<CameraController>(Lifetime.Singleton); } private void RegisterInfrastructureServices(IContainerBuilder builder) { // Data Services builder.Register<IDataService, LocalDataService>(Lifetime.Singleton); builder.Register<ICloudDataService, FirebaseDataService>(Lifetime.Singleton); builder.Register<ICacheService, MemoryCacheService>(Lifetime.Singleton); // Network Services builder.Register<INetworkService, UnityNetworkService>(Lifetime.Singleton); builder.Register<IApiService, HttpApiService>(Lifetime.Singleton); // Platform Services builder.Register<IPlatformService, MobilePlatformService>(Lifetime.Singleton); builder.Register<IAnalyticsService, FirebaseAnalyticsService>(Lifetime.Singleton); // Utility Services builder.Register<ILoggingService, UnityLoggingService>(Lifetime.Singleton); builder.Register<IEncryptionService, AESEncryptionService>(Lifetime.Singleton); builder.Register<ILocalizationService, LocalizationService>(Lifetime.Singleton); } } // Feature-specific scope public class InventoryLifetimeScope : LifetimeScope { protected override void Configure(IContainerBuilder builder) { // Feature-specific services builder.Register<InventoryService>(Lifetime.Scoped); builder.Register<ItemFactory>(Lifetime.Scoped); builder.Register<InventoryValidator>(Lifetime.Scoped); // Feature ViewModels builder.Register<InventoryViewModel>(Lifetime.Scoped); builder.Register<ItemSlotViewModel>(Lifetime.Transient); // Many instances } } ``` ### 의존성 주입 사용 패턴 ```csharp // Constructor Injection (권장) public class GameController { private readonly IPlayerModel _playerModel; private readonly IGameStateService _gameState; private readonly IMessagePublisher<GameStartMessage> _gameStartPublisher; public GameController( IPlayerModel playerModel, IGameStateService gameState, IMessagePublisher<GameStartMessage> gameStartPublisher) { _playerModel = playerModel ?? throw new ArgumentNullException(nameof(playerModel)); _gameState = gameState ?? throw new ArgumentNullException(nameof(gameState)); _gameStartPublisher = gameStartPublisher ?? throw new ArgumentNullException(nameof(gameStartPublisher)); } public void StartGame() { _playerModel.Initialize(); _gameState.ChangeState(GameState.Playing); _gameStartPublisher.Publish(new GameStartMessage()); } } // Property Injection (MonoBehaviour에서 사용) public class PlayerView : MonoBehaviour { [Inject] private PlayerViewModel _viewModel; [Inject] private IAudioService _audioService; [Inject] private IInputService _inputService; private void Start() { // _viewModel이 자동으로 주입됨 InitializeView(); } } // Method Injection (특정 상황에서만) public class ItemFactory { private IItemDatabase _database; [Inject] public void Initialize(IItemDatabase database) { _database = database; } } ``` ## 📡 메시징 시스템 (MessagePipe) 구현 ### 메시지 정의 ```csharp // 메시지 구조체 정의 public readonly struct PlayerLevelUpMessage { public readonly int NewLevel; public readonly int PreviousLevel; public readonly float Experience; public PlayerLevelUpMessage(int newLevel, int previousLevel, float experience) { NewLevel = newLevel; PreviousLevel = previousLevel; Experience = experience; } } public readonly struct InventoryItemAddedMessage { public readonly string ItemId; public readonly int Count; public readonly int SlotIndex; public InventoryItemAddedMessage(string itemId, int count, int slotIndex) { ItemId = itemId; Count = count; SlotIndex = slotIndex; } } public readonly struct GameStateChangedMessage { public readonly GameState PreviousState; public readonly GameState NewState; public readonly float TransitionTime; public GameStateChangedMessage(GameState previousState, GameState newState, float transitionTime) { PreviousState = previousState; NewState = newState; TransitionTime = transitionTime; } } ``` ### 메시지 발행자 (Publisher) ```csharp public class PlayerController : MonoBehaviour { [Inject] private IMessagePublisher<PlayerLevelUpMessage> _levelUpPublisher; [Inject] private IMessagePublisher<PlayerDiedMessage> _diedPublisher; [Inject] private IPlayerModel _playerModel; private void Start() { // 모델 이벤트 구독 및 메시지 발행 _playerModel.Level .Pairwise() .Where(pair => pair.Current > pair.Previous) .Subscribe(pair => PublishLevelUp(pair.Previous, pair.Current)) .AddTo(this); } private void PublishLevelUp(int previousLevel, int newLevel) { var message = new PlayerLevelUpMessage( newLevel, previousLevel, _playerModel.Experience.Value ); _levelUpPublisher.Publish(message); } public void Die() { _diedPublisher.Publish(new PlayerDiedMessage()); } } ``` ### 메시지 구독자 (Subscriber) ```csharp public class UIManager : MonoBehaviour { [Inject] private IMessageSubscriber<PlayerLevelUpMessage> _levelUpSubscriber; [Inject] private IMessageSubscriber<GameStateChangedMessage> _gameStateSubscriber; [SerializeField] private LevelUpPopup _levelUpPopup; [SerializeField] private GameOverScreen _gameOverScreen; private readonly CompositeDisposable _disposables = new(); private void Start() { SubscribeToMessages(); } private void SubscribeToMessages() { // 레벨업 메시지 구독 _levelUpSubscriber.Subscribe(OnPlayerLevelUp) .AddTo(_disposables); // 게임 상태 변경 메시지 구독 _gameStateSubscriber.Subscribe(OnGameStateChanged) .AddTo(_disposables); } private void OnPlayerLevelUp(PlayerLevelUpMessage message) { _levelUpPopup.Show(message.NewLevel, message.PreviousLevel); // 레벨업 효과음 재생 AudioManager.PlaySFX("LevelUp"); // 축하 애니메이션 ShowCelebrationEffect(); } private void OnGameStateChanged(GameStateChangedMessage message) { switch (message.NewState) { case GameState.GameOver: _gameOverScreen.Show(); break; case GameState.Paused: ShowPauseMenu(); break; case GameState.Playing: HideAllMenus(); break; } } private void OnDestroy() { _disposables?.Dispose(); } } ``` ## 🔄 상태 관리 패턴 ### 상태 머신 구현 ```csharp public enum GameState { Initialize, MainMenu, Loading, Playing, Paused, GameOver, Settings } public enum GameStateTrigger { Initialize, StartGame, PauseGame, ResumeGame, GameOver, OpenSettings, CloseSettings, ReturnToMenu } public class GameStateManager : MonoBehaviour { [Inject] private IMessagePublisher<GameStateChangedMessage> _stateChangePublisher; private StateMachine<GameState, GameStateTrigger> _stateMachine; private GameState _currentState; public ReactiveProperty<GameState> CurrentState { get; private set; } private void Awake() { CurrentState = new ReactiveProperty<GameState>(GameState.Initialize); SetupStateMachine(); } private void SetupStateMachine() { _stateMachine = new StateMachine<GameState, GameStateTrigger>(GameState.Initialize); // Initialize State _stateMachine.Configure(GameState.Initialize) .Permit(GameStateTrigger.Initialize, GameState.MainMenu) .OnEntry(OnInitializeEnter) .OnExit(OnInitializeExit); // Main Menu State _stateMachine.Configure(GameState.MainMenu) .Permit(GameStateTrigger.StartGame, GameState.Loading) .Permit(GameStateTrigger.OpenSettings, GameState.Settings) .OnEntry(OnMainMenuEnter); // Loading State _stateMachine.Configure(GameState.Loading) .Permit(GameStateTrigger.StartGame, GameState.Playing) .OnEntry(OnLoadingEnter); // Playing State _stateMachine.Configure(GameState.Playing) .Permit(GameStateTrigger.PauseGame, GameState.Paused) .Permit(GameStateTrigger.GameOver, GameState.GameOver) .Permit(GameStateTrigger.OpenSettings, GameState.Settings) .OnEntry(OnPlayingEnter) .OnExit(OnPlayingExit); // Paused State _stateMachine.Configure(GameState.Paused) .Permit(GameStateTrigger.ResumeGame, GameState.Playing) .Permit(GameStateTrigger.ReturnToMenu, GameState.MainMenu) .OnEntry(OnPausedEnter); // Game Over State _stateMachine.Configure(GameState.GameOver) .Permit(GameStateTrigger.ReturnToMenu, GameState.MainMenu) .Permit(GameStateTrigger.StartGame, GameState.Loading) .OnEntry(OnGameOverEnter); // Settings State _stateMachine.Configure(GameState.Settings) .Permit(GameStateTrigger.CloseSettings, GameState.MainMenu) // or previous state .OnEntry(OnSettingsEnter); } public void FireTrigger(GameStateTrigger trigger) { if (_stateMachine.CanFire(trigger)) { var previousState = _stateMachine.State; _stateMachine.Fire(trigger); var newState = _stateMachine.State; CurrentState.Value = newState; // Publish state change message _stateChangePublisher.Publish(new GameStateChangedMessage( previousState, newState, Time.time)); } else { Debug.LogWarning(quot;Cannot fire trigger {trigger} from state {_stateMachine.State}"); } } private void OnInitializeEnter() { Debug.Log("Initializing game systems..."); // Initialize core systems InitializeCoreServices(); // Auto-transition to main menu FireTrigger(GameStateTrigger.Initialize); } private void OnMainMenuEnter() { Debug.Log("Entering main menu"); Time.timeScale = 1f; // Load main menu scene additively if needed } private void OnPlayingEnter() { Debug.Log("Starting gameplay"); Time.timeScale = 1f; // Initialize gameplay systems } private void OnPlayingExit() { Debug.Log("Exiting gameplay"); // Cleanup gameplay-specific resources } private void OnPausedEnter() { Debug.Log("Game paused"); Time.timeScale = 0f; } private void OnGameOverEnter() { Debug.Log("Game over"); Time.timeScale = 0f; // Save final score, show game over UI } private void InitializeCoreServices() { // Initialize dependency injection // Load persistent data // Setup audio system } } ``` ## 🔧 서비스 레이어 구현 ### 데이터 서비스 패턴 ```csharp public interface IDataService { UniTask<T> LoadAsync<T>(string key, T defaultValue = default); UniTask SaveAsync<T>(string key, T data); UniTask<bool> DeleteAsync(string key); UniTask<bool> ExistsAsync(string key); } public class LocalDataService : IDataService { private readonly string _persistentPath; private readonly IEncryptionService _encryption; private readonly ILoggingService _logger; public LocalDataService(IEncryptionService encryption, ILoggingService logger) { _persistentPath = Application.persistentDataPath; _encryption = encryption; _logger = logger; } public async UniTask<T> LoadAsync<T>(string key, T defaultValue = default) { try { var filePath = GetFilePath(key); if (!File.Exists(filePath)) { _logger.LogInfo(quot;File not found for key: {key}, returning default value"); return defaultValue; } var encryptedData = await File.ReadAllTextAsync(filePath); var jsonData = _encryption.Decrypt(encryptedData); return JsonUtility.FromJson<T>(jsonData); } catch (Exception ex) { _logger.LogError(quot;Failed to load data for key {key}: {ex.Message}"); return defaultValue; } } public async UniTask SaveAsync<T>(string key, T data) { try { var jsonData = JsonUtility.ToJson(data, true); var encryptedData = _encryption.Encrypt(jsonData); var filePath = GetFilePath(key); var directory = Path.GetDirectoryName(filePath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } await File.WriteAllTextAsync(filePath, encryptedData); _logger.LogInfo(quot;Data saved successfully for key: {key}"); } catch (Exception ex) { _logger.LogError(quot;Failed to save data for key {key}: {ex.Message}"); throw; } } public async UniTask<bool> DeleteAsync(string key) { try { var filePath = GetFilePath(key); if (File.Exists(filePath)) { File.Delete(filePath); _logger.LogInfo(quot;Data deleted for key: {key}"); return true; } return false; } catch (Exception ex) { _logger.LogError(quot;Failed to delete data for key {key}: {ex.Message}"); return false; } } public async UniTask<bool> ExistsAsync(string key) { var filePath = GetFilePath(key); return File.Exists(filePath); } private string GetFilePath(string key) { var fileName = quot;{key}.dat"; return Path.Combine(_persistentPath, "SaveData", fileName); } } ``` ### 네트워크 서비스 구현 ```csharp public interface IApiService { UniTask<TResponse> GetAsync<TResponse>(string endpoint); UniTask<TResponse> PostAsync<TRequest, TResponse>(string endpoint, TRequest data); UniTask<TResponse> PutAsync<TRequest, TResponse>(string endpoint, TRequest data); UniTask<bool> DeleteAsync(string endpoint); } public class HttpApiService : IApiService { private readonly HttpClient _httpClient; private readonly ILoggingService _logger; private readonly string _baseUrl; public HttpApiService(ILoggingService logger) { _logger = logger; _baseUrl = "https://api.yourgame.com/v1/"; _httpClient = new HttpClient(); _httpClient.DefaultRequestHeaders.Add("User-Agent", "YourGame/1.0"); _httpClient.Timeout = TimeSpan.FromSeconds(30); } public async UniTask<TResponse> GetAsync<TResponse>(string endpoint) { try { var url = quot;{_baseUrl}{endpoint}"; _logger.LogInfo(quot;GET request to: {url}"); var response = await _httpClient.GetAsync(url); if (response.IsSuccessStatusCode) { var jsonContent = await response.Content.ReadAsStringAsync(); return JsonUtility.FromJson<TResponse>(jsonContent); } else { _logger.LogError(quot;HTTP Error {response.StatusCode}: {response.ReasonPhrase}"); throw new HttpRequestException(quot;Request failed: {response.StatusCode}"); } } catch (Exception ex) { _logger.LogError(quot;GET request failed for {endpoint}: {ex.Message}"); throw; } } public async UniTask<TResponse> PostAsync<TRequest, TResponse>(string endpoint, TRequest data) { try { var url = quot;{_baseUrl}{endpoint}"; var jsonContent = JsonUtility.ToJson(data); _logger.LogInfo(quot;POST request to: {url}"); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(url, content); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); return JsonUtility.FromJson<TResponse>(responseContent); } else { _logger.LogError(quot;HTTP Error {response.StatusCode}: {response.ReasonPhrase}"); throw new HttpRequestException(quot;Request failed: {response.StatusCode}"); } } catch (Exception ex) { _logger.LogError(quot;POST request failed for {endpoint}: {ex.Message}"); throw; } } } ``` ## 🧪 테스트 구현 패턴 ### 단위 테스트 (Edit Mode) ```csharp [TestFixture] public class PlayerModelTests { private PlayerModel _playerModel; [SetUp] public void SetUp() { _playerModel = ScriptableObject.CreateInstance<PlayerModel>(); _playerModel.Initialize(); } [TearDown] public void TearDown() { if (_playerModel != null) { Object.DestroyImmediate(_playerModel); } } [Test] public void TakeDamage_WithValidDamage_ShouldReduceHealth() { // Arrange var initialHealth = _playerModel.Health.Value; var damage = 25f; // Act _playerModel.TakeDamage(damage); // Assert Assert.AreEqual(initialHealth - damage, _playerModel.Health.Value); } [Test] public void TakeDamage_WithNegativeDamage_ShouldNotChangeHealth() { // Arrange var initialHealth = _playerModel.Health.Value; // Act _playerModel.TakeDamage(-10f); // Assert Assert.AreEqual(initialHealth, _playerModel.Health.Value); } [Test] public void Heal_WhenNotAtMaxHealth_ShouldIncreaseHealth() { // Arrange _playerModel.TakeDamage(50f); // Reduce health first var currentHealth = _playerModel.Health.Value; // Act _playerModel.Heal(25f); // Assert Assert.AreEqual(currentHealth + 25f, _playerModel.Health.Value); } [Test] public void GainExperience_WithValidExp_ShouldIncreaseExperience() { // Arrange var initialExp = _playerModel.Experience.Value; var expGain = 50f; // Act _playerModel.GainExperience(expGain); // Assert Assert.AreEqual(initialExp + expGain, _playerModel.Experience.Value); } } [TestFixture] public class PlayerViewModelTests { private PlayerViewModel _viewModel; private Mock<IPlayerModel> _mockPlayerModel; private Mock<IMessagePublisher<PlayerLevelUpMessage>> _mockLevelUpPublisher; [SetUp] public void SetUp() { // Setup mocks _mockPlayerModel = new Mock<IPlayerModel>(); _mockLevelUpPublisher = new Mock<IMessagePublisher<PlayerLevelUpMessage>>(); // Setup mock properties _mockPlayerModel.Setup(m => m.Level).Returns(new ReactiveProperty<int>(1)); _mockPlayerModel.Setup(m => m.Health).Returns(new ReactiveProperty<float>(100f)); _mockPlayerModel.Setup(m => m.MaxHealth).Returns(new ReactiveProperty<float>(100f)); // Create ViewModel with mocks _viewModel = new PlayerViewModel(); // Inject dependencies (would be done by DI container in real scenario) _viewModel.Initialize(); } [TearDown] public void TearDown() { _viewModel?.Dispose(); } [Test] public void LevelText_WhenLevelChanges_ShouldFormatCorrectly() { // Arrange var newLevel = 5; // Act _mockPlayerModel.Object.Level.Value = newLevel; // Assert Assert.AreEqual(quot;Level {newLevel}", _viewModel.LevelText.Value); } } ``` ### 통합 테스트 (Play Mode) ```csharp [UnityTest] public class PlayerSystemIntegrationTests { private GameObject _playerViewObject; private PlayerView _playerView; private PlayerViewModel _viewModel; private PlayerModel _playerModel; [UnitySetUp] public IEnumerator SetUp() { // Create PlayerModel _playerModel = ScriptableObject.CreateInstance<PlayerModel>(); _playerModel.Initialize(); // Create ViewModel _viewModel = new PlayerViewModel(); _viewModel.Initialize(); // Create PlayerView GameObject _playerViewObject = new GameObject("PlayerView"); _playerView = _playerViewObject.AddComponent<PlayerView>(); // Setup UI components SetupUIComponents(); yield return null; } [UnityTearDown] public IEnumerator TearDown() { if (_playerViewObject != null) { Object.DestroyImmediate(_playerViewObject); } _viewModel?.Dispose(); if (_playerModel != null) { Object.DestroyImmediate(_playerModel); } yield return null; } [UnityTest] public IEnumerator PlayerView_WhenHealthChanges_ShouldUpdateUI() { // Arrange yield return null; // Wait for initialization var initialHealth = _playerModel.Health.Value; var damage = 30f; // Act _playerModel.TakeDamage(damage); // Wait for UI update yield return new WaitForEndOfFrame(); // Assert var expectedHealthPercentage = (initialHealth - damage) / _playerModel.MaxHealth.Value; Assert.AreEqual(expectedHealthPercentage, _playerView.HealthBar.value, 0.01f); } private void SetupUIComponents() { // Create UI elements for testing var canvas = new GameObject("Canvas").AddComponent<Canvas>(); canvas.transform.SetParent(_playerViewObject.transform); // Add required UI components // This would typically be done through prefab instantiation } } ``` ## 📊 성능 최적화 패턴 ### 메모리 최적화 ```csharp public class OptimizedStringBuilder { private static readonly StringBuilder _sharedBuilder = new StringBuilder(256); private static readonly object _lock = new object(); public static string Format(string format, params object[] args) { lock (_lock) { _sharedBuilder.Clear(); _sharedBuilder.AppendFormat(format, args); return _sharedBuilder.ToString(); } } } public class ComponentCache : MonoBehaviour { private readonly Dictionary<Type, Component> _cachedComponents = new(); public T GetCachedComponent<T>() where T : Component { var type = typeof(T); if (_cachedComponents.TryGetValue(type, out var component)) { return (T)component; } var newComponent = GetComponent<T>(); if (newComponent != null) { _cachedComponents[type] = newComponent; } return newComponent; } private void OnDestroy() { _cachedComponents.Clear(); } } ``` --- ## 📚 코드 품질 가이드라인 ### 네이밍 규칙 ```csharp // 클래스: PascalCase public class PlayerController { } // 인터페이스: I + PascalCase public interface IPlayerService { } // 메서드: PascalCase public void CalculateDamage() { } // 프로퍼티: PascalCase public int Health { get; set; } // public 필드: PascalCase public float MoveSpeed; // private 필드: _camelCase private float _currentHealth; // 상수: UPPER_SNAKE_CASE private const float MAX_HEALTH = 100f; // 이벤트: PascalCase public event Action<float> HealthChanged; // 매개변수: camelCase public void SetHealth(float newHealth) { } ``` ### SOLID 원칙 적용 ```csharp // Single Responsibility Principle public class HealthCalculator // Only calculates health { public float CalculateHealthPercentage(float current, float max) { return max > 0 ? current / max : 0f; } } // Open/Closed Principle public abstract class Weapon { public abstract float CalculateDamage(); } public class Sword : Weapon { public override float CalculateDamage() => 50f; } // Interface Segregation Principle public interface IMovable { void Move(Vector3 direction); } public interface IAttackable { void Attack(); } // Dependency Inversion Principle public class PlayerController { private readonly IInputService _input; // Depend on abstraction public PlayerController(IInputService input) { _input = input; } } ``` --- *이 코드 아키텍처 가이드는 Unity MVVM 프로젝트의 핵심 구현 패턴과 베스트 프랙티스를 제시합니다. 팀의 코드 품질과 일관성을 위해 이 가이드를 참고하여 개발해 주세요.*