# Game Development Guidelines (Unity & C#) ## Overview This document establishes coding standards, architectural patterns, and development practices for 2D game development using Unity and C#. These guidelines ensure consistency, performance, and maintainability across all game development stories. ## C# Standards ### Naming Conventions **Classes, Structs, Enums, and Interfaces:** - PascalCase for types: `PlayerController`, `GameData`, `IInteractable` - Prefix interfaces with 'I': `IDamageable`, `IControllable` - Descriptive names that indicate purpose: `GameStateManager` not `GSM` **Methods and Properties:** - PascalCase for methods and properties: `CalculateScore()`, `CurrentHealth` - Descriptive verb phrases for methods: `ActivateShield()` not `shield()` **Fields and Variables:** - `private` or `protected` fields: camelCase with an underscore prefix: `_playerHealth`, `_movementSpeed` - `public` fields (use sparingly, prefer properties): PascalCase: `PlayerName` - `static` fields: PascalCase: `Instance`, `GameVersion` - `const` fields: PascalCase: `MaxHitPoints` - `local` variables: camelCase: `damageAmount`, `isJumping` - Boolean variables with is/has/can prefix: `_isAlive`, `_hasKey`, `_canJump` **Files and Directories:** - PascalCase for C# script files, matching the primary class name: `PlayerController.cs` - PascalCase for Scene files: `MainMenu.unity`, `Level01.unity` ### Style and Formatting - **Braces**: Use Allman style (braces on a new line). - **Spacing**: Use 4 spaces for indentation (no tabs). - **`using` directives**: Place all `using` directives at the top of the file, outside the namespace. - **`this` keyword**: Only use `this` when necessary to distinguish between a field and a local variable/parameter. ## Unity Architecture Patterns ### Scene Lifecycle Management **Loading and Transitioning Between Scenes:** ```csharp // SceneLoader.cs - A singleton for managing scene transitions. using UnityEngine; using UnityEngine.SceneManagement; using System.Collections; public class SceneLoader : MonoBehaviour { public static SceneLoader Instance { get; private set; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } public void LoadGameScene() { // Example of loading the main game scene, perhaps with a loading screen first. StartCoroutine(LoadSceneAsync("Level01")); } private IEnumerator LoadSceneAsync(string sceneName) { // Load a loading screen first (optional) SceneManager.LoadScene("LoadingScreen"); // Wait a frame for the loading screen to appear yield return null; // Begin loading the target scene in the background AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); // Don't activate the scene until it's fully loaded asyncLoad.allowSceneActivation = false; // Wait until the asynchronous scene fully loads while (!asyncLoad.isDone) { // Here you could update a progress bar with asyncLoad.progress if (asyncLoad.progress >= 0.9f) { // Scene is loaded, allow activation asyncLoad.allowSceneActivation = true; } yield return null; } } } ``` ### MonoBehaviour Lifecycle **Understanding Core MonoBehaviour Events:** ```csharp // Example of a standard MonoBehaviour lifecycle using UnityEngine; public class PlayerController : MonoBehaviour { // AWAKE: Called when the script instance is being loaded. // Use for initialization before the game starts. Good for caching component references. private void Awake() { Debug.Log("PlayerController Awake!"); } // ONENABLE: Called when the object becomes enabled and active. // Good for subscribing to events. private void OnEnable() { // Example: UIManager.OnGamePaused += HandleGamePaused; } // START: Called on the frame when a script is enabled just before any of the Update methods are called the first time. // Good for logic that depends on other objects being initialized. private void Start() { Debug.Log("PlayerController Start!"); } // FIXEDUPDATE: Called every fixed framerate frame. // Use for physics calculations (e.g., applying forces to a Rigidbody). private void FixedUpdate() { // Handle Rigidbody movement here. } // UPDATE: Called every frame. // Use for most game logic, like handling input and non-physics movement. private void Update() { // Handle input and non-physics movement here. } // LATEUPDATE: Called every frame, after all Update functions have been called. // Good for camera logic that needs to track a target that moves in Update. private void LateUpdate() { // Camera follow logic here. } // ONDISABLE: Called when the behaviour becomes disabled or inactive. // Good for unsubscribing from events to prevent memory leaks. private void OnDisable() { // Example: UIManager.OnGamePaused -= HandleGamePaused; } // ONDESTROY: Called when the MonoBehaviour will be destroyed. // Good for any final cleanup. private void OnDestroy() { Debug.Log("PlayerController Destroyed!"); } } ``` ### Game Object Patterns **Component-Based Architecture:** ```csharp // Player.cs - The main GameObject class, acts as a container for components. using UnityEngine; [RequireComponent(typeof(PlayerMovement), typeof(PlayerHealth))] public class Player : MonoBehaviour { public PlayerMovement Movement { get; private set; } public PlayerHealth Health { get; private set; } private void Awake() { Movement = GetComponent(); Health = GetComponent(); } } // PlayerHealth.cs - A component responsible only for health logic. public class PlayerHealth : MonoBehaviour { [SerializeField] private int _maxHealth = 100; private int _currentHealth; private void Awake() { _currentHealth = _maxHealth; } public void TakeDamage(int amount) { _currentHealth -= amount; if (_currentHealth <= 0) { Die(); } } private void Die() { // Death logic Debug.Log("Player has died."); gameObject.SetActive(false); } } ``` ### Data-Driven Design with ScriptableObjects **Define Data Containers:** ```csharp // EnemyData.cs - A ScriptableObject to hold data for an enemy type. using UnityEngine; [CreateAssetMenu(fileName = "NewEnemyData", menuName = "Game/Enemy Data")] public class EnemyData : ScriptableObject { public string enemyName; public int maxHealth; public float moveSpeed; public int damage; public Sprite sprite; } // Enemy.cs - A MonoBehaviour that uses the EnemyData. public class Enemy : MonoBehaviour { [SerializeField] private EnemyData _enemyData; private int _currentHealth; private void Start() { _currentHealth = _enemyData.maxHealth; GetComponent().sprite = _enemyData.sprite; } // ... other enemy logic } ``` ### System Management **Singleton Managers:** ```csharp // GameManager.cs - A singleton to manage the overall game state. using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } public int Score { get; private set; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); // Persist across scenes } public void AddScore(int amount) { Score += amount; } } ``` ## Performance Optimization ### Object Pooling **Required for High-Frequency Objects (e.g., bullets, effects):** ```csharp // ObjectPool.cs - A generic object pooling system. using UnityEngine; using System.Collections.Generic; public class ObjectPool : MonoBehaviour { [SerializeField] private GameObject _prefabToPool; [SerializeField] private int _initialPoolSize = 20; private Queue _pool = new Queue(); private void Start() { for (int i = 0; i < _initialPoolSize; i++) { GameObject obj = Instantiate(_prefabToPool); obj.SetActive(false); _pool.Enqueue(obj); } } public GameObject GetObjectFromPool() { if (_pool.Count > 0) { GameObject obj = _pool.Dequeue(); obj.SetActive(true); return obj; } // Optionally, expand the pool if it's empty. return Instantiate(_prefabToPool); } public void ReturnObjectToPool(GameObject obj) { obj.SetActive(false); _pool.Enqueue(obj); } } ``` ### Frame Rate Optimization **Update Loop Optimization:** - Avoid expensive calls like `GetComponent`, `FindObjectOfType`, or `Instantiate` inside `Update()` or `FixedUpdate()`. Cache references in `Awake()` or `Start()`. - Use Coroutines or simple timers for logic that doesn't need to run every single frame. **Physics Optimization:** - Adjust the "Physics 2D Settings" in Project Settings, especially the "Layer Collision Matrix", to prevent unnecessary collision checks. - Use `Rigidbody2D.Sleep()` for objects that are not moving to save CPU cycles. ## Input Handling ### Cross-Platform Input (New Input System) **Input Action Asset:** Create an Input Action Asset (`.inputactions`) to define controls. **PlayerInput Component:** - Add the `PlayerInput` component to the player GameObject. - Set its "Actions" to the created Input Action Asset. - Set "Behavior" to "Invoke Unity Events" to easily hook up methods in the Inspector, or "Send Messages" to use methods like `OnMove`, `OnFire`. ```csharp // PlayerInputHandler.cs - Example of handling input via messages. using UnityEngine; using UnityEngine.InputSystem; public class PlayerInputHandler : MonoBehaviour { private Vector2 _moveInput; // This method is called by the PlayerInput component via "Send Messages". // The action must be named "Move" in the Input Action Asset. public void OnMove(InputValue value) { _moveInput = value.Get(); } private void Update() { // Use _moveInput to control the player transform.Translate(new Vector3(_moveInput.x, _moveInput.y, 0) * Time.deltaTime * 5f); } } ``` ## Error Handling ### Graceful Degradation **Asset Loading Error Handling:** - When using Addressables or `Resources.Load`, always check if the loaded asset is null before using it. ```csharp // Load a sprite and use a fallback if it fails Sprite playerSprite = Resources.Load("Sprites/Player"); if (playerSprite == null) { Debug.LogError("Player sprite not found! Using default."); playerSprite = Resources.Load("Sprites/Default"); } ``` ### Runtime Error Recovery **Assertions and Logging:** - Use `Debug.Assert(condition, "Message")` to check for critical conditions that must be true. - Use `Debug.LogError("Message")` for fatal errors and `Debug.LogWarning("Message")` for non-critical issues. ```csharp // Example of using an assertion to ensure a component exists. private Rigidbody2D _rb; void Awake() { _rb = GetComponent(); Debug.Assert(_rb != null, "Rigidbody2D component not found on player!"); } ``` ## Testing Standards ### Unit Testing (Edit Mode) **Game Logic Testing:** ```csharp // HealthSystemTests.cs - Example test for a simple health system. using NUnit.Framework; using UnityEngine; public class HealthSystemTests { [Test] public void TakeDamage_ReducesHealth() { // Arrange var gameObject = new GameObject(); var healthSystem = gameObject.AddComponent(); // Note: This is a simplified example. You might need to mock dependencies. // Act healthSystem.TakeDamage(20); // Assert // This requires making health accessible for testing, e.g., via a public property or method. // Assert.AreEqual(80, healthSystem.CurrentHealth); } } ``` ### Integration Testing (Play Mode) **Scene Testing:** - Play Mode tests run in a live scene, allowing you to test interactions between multiple components and systems. - Use `yield return null;` to wait for the next frame. ```csharp // PlayerJumpTest.cs using System.Collections; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; public class PlayerJumpTest { [UnityTest] public IEnumerator PlayerJumps_WhenSpaceIsPressed() { // Arrange var player = new GameObject().AddComponent(); var initialY = player.transform.position.y; // Act // Simulate pressing the jump button (requires setting up the input system for tests) // For simplicity, we'll call a public method here. // player.Jump(); // Wait for a few physics frames yield return new WaitForSeconds(0.5f); // Assert Assert.Greater(player.transform.position.y, initialY); } } ``` ## File Organization ### Project Structure ``` Assets/ ├── Scenes/ │ ├── MainMenu.unity │ └── Level01.unity ├── Scripts/ │ ├── Core/ │ │ ├── GameManager.cs │ │ └── AudioManager.cs │ ├── Player/ │ │ ├── PlayerController.cs │ │ └── PlayerHealth.cs │ ├── Editor/ │ │ └── CustomInspectors.cs │ └── Data/ │ └── EnemyData.cs ├── Prefabs/ │ ├── Player.prefab │ └── Enemies/ │ └── Slime.prefab ├── Art/ │ ├── Sprites/ │ └── Animations/ ├── Audio/ │ ├── Music/ │ └── SFX/ ├── Data/ │ └── ScriptableObjects/ │ └── EnemyData/ └── Tests/ ├── EditMode/ │ └── HealthSystemTests.cs └── PlayMode/ └── PlayerJumpTest.cs ``` ## Development Workflow ### Story Implementation Process 1. **Read Story Requirements:** - Understand acceptance criteria - Identify technical requirements - Review performance constraints 2. **Plan Implementation:** - Identify files to create/modify - Consider Unity's component-based architecture - Plan testing approach 3. **Implement Feature:** - Write clean C# code following all guidelines - Use established patterns - Maintain stable FPS performance 4. **Test Implementation:** - Write edit mode tests for game logic - Write play mode tests for integration testing - Test cross-platform functionality - Validate performance targets 5. **Update Documentation:** - Mark story checkboxes complete - Document any deviations - Update architecture if needed ### Code Review Checklist - [ ] C# code compiles without errors or warnings. - [ ] All automated tests pass. - [ ] Code follows naming conventions and architectural patterns. - [ ] No expensive operations in `Update()` loops. - [ ] Public fields/methods are documented with comments. - [ ] New assets are organized into the correct folders. ## Performance Targets ### Frame Rate Requirements - **PC/Console**: Maintain a stable 60+ FPS. - **Mobile**: Maintain 60 FPS on mid-range devices, minimum 30 FPS on low-end. - **Optimization**: Use the Unity Profiler to identify and fix performance drops. ### Memory Management - **Total Memory**: Keep builds under platform-specific limits (e.g., 200MB for a simple mobile game). - **Garbage Collection**: Minimize GC spikes by avoiding string concatenation, `new` keyword usage in loops, and by pooling objects. ### Loading Performance - **Initial Load**: Under 5 seconds for game start. - **Scene Transitions**: Under 2 seconds between scenes. Use asynchronous scene loading. These guidelines ensure consistent, high-quality game development that meets performance targets and maintains code quality across all implementation stories.