* 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)
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:
GameStateManagernotGSM
Methods and Properties:
- PascalCase for methods and properties:
CalculateScore(),CurrentHealth - Descriptive verb phrases for methods:
ActivateShield()notshield()
Fields and Variables:
privateorprotectedfields: camelCase with an underscore prefix:_playerHealth,_movementSpeedpublicfields (use sparingly, prefer properties): PascalCase:PlayerNamestaticfields: PascalCase:Instance,GameVersionconstfields: PascalCase:MaxHitPointslocalvariables: 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).
usingdirectives: Place allusingdirectives at the top of the file, outside the namespace.thiskeyword: Only usethiswhen 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, orInstantiateinsideUpdate()orFixedUpdate(). Cache references inAwake()orStart(). - 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
PlayerInputcomponent 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 andDebug.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
-
Read Story Requirements:
- Understand acceptance criteria
- Identify technical requirements
- Review performance constraints
-
Plan Implementation:
- Identify files to create/modify
- Consider Unity's component-based architecture
- Plan testing approach
-
Implement Feature:
- Write clean C# code following all guidelines
- Use established patterns
- Maintain stable FPS performance
-
Test Implementation:
- Write edit mode tests for game logic
- Write play mode tests for integration testing
- Test cross-platform functionality
- Validate performance targets
-
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,
newkeyword 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.