# Unity C# 코딩 표준 ## 📋 개요 Unity 프로젝트에서 일관된 코드 품질과 가독성을 위한 C# 코딩 표준입니다. project rule과 완전히 일치하는 규칙들을 정의합니다. --- ## 🏷️ 네이밍 규칙 (Naming Conventions) ### 클래스, 구조체, 인터페이스, 열거형 ```csharp // ✅ 올바른 예시 public class PlayerController : MonoBehaviour public struct GameData public interface IPlayerService public enum GameState // ❌ 잘못된 예시 public class playerController // 첫 글자 소문자 public interface PlayerService // I 접두사 누락 ``` ### 메서드, 프로퍼티, 이벤트 ```csharp // ✅ 올바른 예시 public void GetHealth() public int MaxHealth { get; set; } public event Action<float> HealthChanged; // ❌ 잘못된 예시 public void getHealth() // 첫 글자 소문자 public int maxHealth; // 프로퍼티가 아닌 public 필드 ``` ### 변수 및 매개변수 ```csharp // ✅ 올바른 예시 public void MovePlayer(float moveSpeed, Vector3 targetPosition) { float currentSpeed = moveSpeed * Time.deltaTime; int playerLevel = GetPlayerLevel(); } // ❌ 잘못된 예시 public void MovePlayer(float MoveSpeed, Vector3 target_position) // PascalCase 및 snake_case 사용 ``` ### Private 필드 (중요!) ```csharp // ✅ 올바른 예시 - camelCase with underscore prefix [SerializeField] private float _moveSpeed = 5f; private PlayerData _playerData; private bool _isInitialized = false; // ❌ 잘못된 예시 [SerializeField] private float moveSpeed; // underscore 누락 private PlayerData playerData; // underscore 누락 private bool IsInitialized; // PascalCase 사용 ``` ### 상수 ```csharp // ✅ 올바른 예시 private const float MAX_HEALTH = 100f; private const int DEFAULT_LEVEL = 1; public static readonly string GAME_VERSION = "1.0.0"; // ❌ 잘못된 예시 private const float maxHealth = 100f; // UPPER_SNAKE_CASE 미사용 private const int Default_Level = 1; // 혼합 케이스 ``` --- ## 🏗️ Unity 특화 규칙 ### MonoBehaviour 생명주기 순서 ```csharp public class PlayerController : MonoBehaviour { [SerializeField] private float _moveSpeed = 5f; [SerializeField] private Transform _playerTransform; // ✅ 올바른 순서 private void Awake() { // 컴포넌트 초기화, GetComponent 캐싱 _playerTransform = GetComponent<Transform>(); } private void Start() { // 다른 오브젝트 참조, 초기 설정 InitializePlayerData(); } private void Update() { // 매 프레임 업데이트 HandleInput(); UpdateMovement(); } private void FixedUpdate() { // 물리 업데이트 ApplyPhysics(); } private void LateUpdate() { // 카메라 업데이트, UI 업데이트 UpdateCamera(); } } ``` ### SerializeField 사용법 ```csharp // ✅ 올바른 사용 [SerializeField] private float _health = 100f; [SerializeField] private PlayerSettings _settings; [SerializeField] private Button _startButton; // ❌ 피해야 할 사용 public float health = 100f; // public 필드 대신 SerializeField 사용 [SerializeField] float health = 100f; // private 키워드 생략 [SerializeField] private float Health = 100f; // PascalCase 사용 ``` ### GetComponent 최적화 ```csharp public class OptimizedController : MonoBehaviour { // ✅ 컴포넌트 캐싱 private Rigidbody _rigidbody; private Animator _animator; private AudioSource _audioSource; private void Awake() { // Awake에서 한 번만 캐싱 _rigidbody = GetComponent<Rigidbody>(); _animator = GetComponent<Animator>(); _audioSource = GetComponent<AudioSource>(); } private void Update() { // ✅ 캐시된 참조 사용 _rigidbody.velocity = Vector3.zero; // ❌ 매 프레임 GetComponent 호출 // GetComponent<Rigidbody>().velocity = Vector3.zero; } } ``` --- ## 🎯 MVVM 패턴 규칙 ### Model 클래스 ```csharp // ✅ ScriptableObject 기반 Model [CreateAssetMenu(fileName = "PlayerModel", menuName = "Game/Player")] public class PlayerModel : ScriptableObject, IPlayerModel { [Header("Player Stats")] [SerializeField] private int _level = 1; [SerializeField] private float _health = 100f; [SerializeField] private float _maxHealth = 100f; // ReactiveProperty로 UI 바인딩 지원 public ReactiveProperty<int> Level { get; private set; } public ReactiveProperty<float> Health { get; private set; } public ReactiveProperty<float> MaxHealth { get; private set; } // 비즈니스 로직 메서드 public void TakeDamage(float damage) { if (damage < 0) return; // 유효성 검사 var newHealth = Mathf.Max(0, Health.Value - damage); Health.Value = newHealth; } public void Initialize() { Level = new ReactiveProperty<int>(_level); Health = new ReactiveProperty<float>(_health); MaxHealth = new ReactiveProperty<float>(_maxHealth); } } ``` ### ViewModel 클래스 ```csharp // ✅ ViewModel 구현 public class PlayerViewModel : IDisposable { // 의존성 주입 [Inject] private IPlayerModel _playerModel; [Inject] private IMessagePublisher<PlayerLevelUpMessage> _levelUpPublisher; // UI 바인딩용 프로퍼티 public ReadOnlyReactiveProperty<string> LevelText { get; private set; } public ReadOnlyReactiveProperty<float> HealthBarValue { get; private set; } // 커맨드 public ReactiveCommand HealCommand { get; private set; } private readonly CompositeDisposable _disposables = new(); public void Initialize() { // 데이터 바인딩 설정 LevelText = _playerModel.Level .Select(level => quot;Level {level}") .ToReadOnlyReactiveProperty() .AddTo(_disposables); HealthBarValue = _playerModel.Health .CombineLatest(_playerModel.MaxHealth, (current, max) => max > 0 ? current / max : 0) .ToReadOnlyReactiveProperty() .AddTo(_disposables); // 커맨드 설정 HealCommand = new ReactiveCommand().AddTo(_disposables); HealCommand.Subscribe(_ => ExecuteHeal()).AddTo(_disposables); } private void ExecuteHeal() { _playerModel.Heal(25f); } public void Dispose() { _disposables?.Dispose(); } } ``` ### View 클래스 ```csharp // ✅ View 구현 public class PlayerView : MonoBehaviour { [Header("UI References")] [SerializeField] private Text _levelText; [SerializeField] private Slider _healthBar; [SerializeField] private Button _healButton; [Inject] private PlayerViewModel _viewModel; private readonly CompositeDisposable _disposables = new(); private void Start() { SetupDataBinding(); SetupCommands(); } private void SetupDataBinding() { // ViewModel → UI 데이터 바인딩 _viewModel.LevelText .Subscribe(text => _levelText.text = text) .AddTo(_disposables); _viewModel.HealthBarValue .Subscribe(value => _healthBar.value = value) .AddTo(_disposables); } private void SetupCommands() { // UI → ViewModel 커맨드 바인딩 _healButton.onClick.AsObservable() .Subscribe(_ => _viewModel.HealCommand.Execute()) .AddTo(_disposables); } private void OnDestroy() { _disposables?.Dispose(); } } ``` --- ## 🚀 성능 최적화 규칙 ### GC 최적화 ```csharp // ✅ StringBuilder 사용 private readonly StringBuilder _stringBuilder = new StringBuilder(256); public string FormatHealthText(float current, float max) { _stringBuilder.Clear(); _stringBuilder.Append("Health: "); _stringBuilder.Append(current.ToString("F0")); _stringBuilder.Append("/"); _stringBuilder.Append(max.ToString("F0")); return _stringBuilder.ToString(); } // ❌ string 연산 사용 public string FormatHealthText(float current, float max) { return "Health: " + current + "/" + max; // GC 발생 } ``` ### 오브젝트 풀링 ```csharp // ✅ 오브젝트 풀 사용 public class BulletPool : MonoBehaviour { [SerializeField] private GameObject _bulletPrefab; [SerializeField] private int _poolSize = 100; private Queue<GameObject> _bulletPool = new Queue<GameObject>(); private void Start() { InitializePool(); } private void InitializePool() { for (int i = 0; i < _poolSize; i++) { GameObject bullet = Instantiate(_bulletPrefab); bullet.SetActive(false); _bulletPool.Enqueue(bullet); } } public GameObject GetBullet() { if (_bulletPool.Count > 0) { GameObject bullet = _bulletPool.Dequeue(); bullet.SetActive(true); return bullet; } // 풀이 비었을 때는 새로 생성 return Instantiate(_bulletPrefab); } public void ReturnBullet(GameObject bullet) { bullet.SetActive(false); _bulletPool.Enqueue(bullet); } } ``` ### Update 함수 최적화 ```csharp public class OptimizedUpdate : MonoBehaviour { private float _updateInterval = 0.1f; // 0.1초마다 업데이트 private float _lastUpdateTime; private void Update() { // ✅ 모든 프레임마다 실행할 필요 없는 로직은 간격 두기 if (Time.time - _lastUpdateTime >= _updateInterval) { CheckForEnemies(); _lastUpdateTime = Time.time; } // ✅ 매 프레임 실행이 필요한 로직 HandleInput(); } private void CheckForEnemies() { // 무거운 로직 (적 탐지, AI 판단 등) } private void HandleInput() { // 가벼운 입력 처리 } } ``` --- ## 🔍 에러 핸들링 규칙 ### Null 체크 ```csharp // ✅ 올바른 null 체크 public void ProcessPlayerData(PlayerData data) { if (data == null) { Debug.LogError("PlayerData is null!"); return; } if (string.IsNullOrEmpty(data.PlayerName)) { Debug.LogWarning("Player name is empty, using default."); data.PlayerName = "Unknown Player"; } // 처리 로직... } // ✅ Unity Object null 체크 private void Start() { if (_playerTransform == null) { _playerTransform = GetComponent<Transform>(); if (_playerTransform == null) { Debug.LogError("Transform component not found!"); enabled = false; return; } } } ``` ### Try-Catch 사용 ```csharp // ✅ 예외 발생 가능한 작업에 try-catch 사용 public async UniTask<PlayerData> LoadPlayerDataAsync(string filePath) { try { string jsonData = await File.ReadAllTextAsync(filePath); PlayerData data = JsonUtility.FromJson<PlayerData>(jsonData); return data; } catch (FileNotFoundException) { Debug.LogWarning(quot;Player data file not found: {filePath}"); return CreateDefaultPlayerData(); } catch (JsonException ex) { Debug.LogError(quot;Failed to parse player data: {ex.Message}"); return CreateDefaultPlayerData(); } catch (Exception ex) { Debug.LogError(quot;Unexpected error loading player data: {ex.Message}"); throw; } } // ✅ 네트워크 통신 에러 핸들링 public async UniTask<bool> SendDataToServerAsync(GameData data) { try { var response = await _httpClient.PostAsync(endpoint, jsonContent); return response.IsSuccessStatusCode; } catch (HttpRequestException ex) { Debug.LogError(quot;Network error: {ex.Message}"); return false; } catch (TaskCanceledException ex) { Debug.LogError(quot;Request timeout: {ex.Message}"); return false; } } ``` --- ## 📏 코드 품질 규칙 ### 함수 길이 제한 ```csharp // ✅ 짧고 명확한 함수 (20줄 이하 권장) public void InitializePlayer() { LoadPlayerData(); SetupPlayerUI(); RegisterEventHandlers(); } private void LoadPlayerData() { // 5-10줄의 데이터 로딩 로직 } private void SetupPlayerUI() { // 5-10줄의 UI 설정 로직 } private void RegisterEventHandlers() { // 5-10줄의 이벤트 핸들러 등록 } // ❌ 너무 긴 함수 (30줄 초과) public void DoEverything() { // 50줄의 복잡한 로직... (분리 필요) } ``` ### 매직 넘버 제거 ```csharp // ✅ 상수 사용 public class PlayerConfig { public const float MAX_HEALTH = 100f; public const float DEFAULT_SPEED = 5f; public const int MAX_INVENTORY_SIZE = 48; public const float RESPAWN_TIME = 3f; } public class PlayerController : MonoBehaviour { private void Start() { SetHealth(PlayerConfig.MAX_HEALTH); SetMoveSpeed(PlayerConfig.DEFAULT_SPEED); } } // ❌ 매직 넘버 사용 public class PlayerController : MonoBehaviour { private void Start() { SetHealth(100f); // 무슨 의미인지 불분명 SetMoveSpeed(5f); // 매직 넘버 if (level > 10) // 왜 10인가? { // ... } } } ``` ### 중첩 깊이 제한 (최대 3단계) ```csharp // ✅ Guard Clause 사용으로 중첩 최소화 public void ProcessInput(InputData input) { if (input == null) return; if (!_isPlayerActive) return; if (_isProcessingInput) return; // 실제 처리 로직 HandleMovement(input.movement); HandleActions(input.actions); } // ❌ 깊은 중첩 public void ProcessInput(InputData input) { if (input != null) { if (_isPlayerActive) { if (!_isProcessingInput) { if (input.movement != Vector3.zero) { // 너무 깊은 중첩... } } } } } ``` --- ## 📝 주석 작성 규칙 ### 한글 주석 사용 ```csharp /// <summary> /// 플레이어의 체력을 회복시킵니다. /// </summary> /// <param name="amount">회복할 체력양</param> /// <returns>실제 회복된 체력양</returns> public float HealPlayer(float amount) { // 체력이 이미 최대치인 경우 처리 if (_currentHealth >= _maxHealth) { Debug.Log("체력이 이미 최대치입니다."); return 0f; } // 회복량 계산 (최대치 초과 방지) float actualHealing = Mathf.Min(amount, _maxHealth - _currentHealth); _currentHealth += actualHealing; // UI 업데이트 이벤트 발행 OnHealthChanged?.Invoke(_currentHealth, _maxHealth); return actualHealing; } ``` ### TODO 주석 사용 ```csharp public class PlayerController : MonoBehaviour { // TODO: 스킬 시스템 구현 필요 (2024-01-15) // FIXME: 점프 애니메이션이 간헐적으로 끊어짐 // NOTE: 이 부분은 향후 AI 시스템과 연동될 예정 private void Update() { HandleMovement(); // TODO: 스킬 입력 처리 추가 } } ``` --- ## 🧪 단위 테스트 규칙 ### 테스트 네이밍 ```csharp [TestFixture] public class PlayerControllerTests { // ✅ 명확한 테스트 이름 (한글 허용) [Test] public void TakeDamage_정상적인_데미지_입력시_체력이_감소한다() { // Arrange var player = new PlayerController(); player.SetHealth(100f); // Act player.TakeDamage(30f); // Assert Assert.AreEqual(70f, player.CurrentHealth); } [Test] public void TakeDamage_음수_데미지_입력시_체력이_변하지_않는다() { // Arrange & Act & Assert } } ``` --- ## 📊 코드 리뷰 체크리스트 ### 필수 확인 사항 - [ ] **네이밍 규칙 준수** (PascalCase, camelCase, _underscore) - [ ] **Unity 라이프사이클 순서** (Awake → Start → Update → FixedUpdate → LateUpdate) - [ ] **SerializeField 적절 사용** (public 필드 대신) - [ ] **GetComponent 캐싱** (Update에서 직접 호출 금지) - [ ] **null 체크** (외부 참조는 반드시) - [ ] **예외 처리** (파일 I/O, 네트워크 등) - [ ] **메모리 누수 방지** (이벤트 구독 해제, Dispose 호출) - [ ] **매직 넘버 제거** (상수 사용) - [ ] **함수 길이** (20줄 이하 권장, 30줄 초과 금지) - [ ] **중첩 깊이** (3단계 이하) ### MVVM 패턴 체크리스트 - [ ] **Model**: ScriptableObject 기반, ReactiveProperty 사용 - [ ] **ViewModel**: IDisposable 구현, 의존성 주입 활용 - [ ] **View**: MonoBehaviour, ViewModel만 참조 - [ ] **데이터 바인딩**: R3 Observable 활용 - [ ] **커맨드 패턴**: ReactiveCommand 사용 --- *이 코딩 표준을 준수하여 일관되고 유지보수 가능한 Unity 코드를 작성해주세요!* --- *최종 업데이트: 2024-01-03* *작성자: Unity Development Team*