Files
BMAD-METHOD/expansion-packs/bmad-2d-unity-game-dev/data/development-guidelines.md
PinkyD a7038d43d1 feat: Overhaul and Enhance 2D Unity Game Dev Expansion Pack (#350)
* Updated game-sm agent to match the new core framework patterns

* feat:Created more comprehensive game story matching new format system as well

* feat:Added Game specific course correct task

* feat:Updated dod-checklist to match new DoD format

* feat:Added new Architect agent for appropriate architecture doc creation and design

* feat:Overhaul of game-architecture-tmpl template

* feat:Updated rest of templates besides level which doesnt really need it

* feat: Finished extended architecture documentation needed for new game story tasks

* feat: Updated game Developer to new format

* feat: Updated last agent to new format and updated bmad-kb. bmad-kb I did my best with but im not sure of it's valid usage in the expansion pack, the AI generated more of the file then myself. I made sure to include it due to the new core-config file

* feat: Finished updating designer agent to new format and cleaned up template linting errors

* Built dist for web bundle

* Increased expansion pack minor verison number

* Updated architecht and design for sharding built-in

* chore: bump bmad-2d-unity-game-dev version (minor)

* updated config.yaml for game-specific pieces to supplement core-config.yaml

* Updated game-core-config and epic processing for game story and game design. Initial implementation was far too generic

* chore: bump bmad-2d-unity-game-dev version (patch)

* feat: Fixed issue with multi-configs being needed. chore: bump bmad-2d-unity-game-dev version (patch)

* Chore: Built web-bundle

* feat: Added the ability to specify the unity editor install location.\nchore: bump bmad-2d-unity-game-dev version (patch)

* feat: core-config must be in two places to support inherited tasks at this time so added instructions to copy and create one in expansion pack folder as well. chore: bump bmad-2d-unity-game-dev version (patch)
2025-07-23 07:14:06 -05:00

16 KiB

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:

// 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:

// 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:

// 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<PlayerMovement>();
        Health = GetComponent<PlayerHealth>();
    }
}

// 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:

// 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<SpriteRenderer>().sprite = _enemyData.sprite;
    }

    // ... other enemy logic
}

System Management

Singleton Managers:

// 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):

// 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<GameObject> _pool = new Queue<GameObject>();

    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.
// 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<Vector2>();
    }

    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.
// Load a sprite and use a fallback if it fails
Sprite playerSprite = Resources.Load<Sprite>("Sprites/Player");
if (playerSprite == null)
{
    Debug.LogError("Player sprite not found! Using default.");
    playerSprite = Resources.Load<Sprite>("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.
// Example of using an assertion to ensure a component exists.
private Rigidbody2D _rb;

void Awake()
{
    _rb = GetComponent<Rigidbody2D>();
    Debug.Assert(_rb != null, "Rigidbody2D component not found on player!");
}

Testing Standards

Unit Testing (Edit Mode)

Game Logic Testing:

// 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<PlayerHealth>();
        // 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.
// 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<PlayerController>();
        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.