BMAD-METHOD/expansion-packs/bmad-2d-godot-game-dev/data/development-guidelines.md

13 KiB

Game Development Guidelines (Godot - GDScript & C#)

Overview

This document establishes coding standards, architectural patterns, and development practices for 2D game development using Godot with GDScript and C#. These guidelines ensure consistency, performance, and maintainability across all game development stories.

Language Selection

When to Use GDScript

  • Rapid prototyping and iteration
  • Simple game logic and scripting
  • Godot-specific features and integrations
  • Smaller projects or game jam entries

When to Use C#

  • Performance-critical systems
  • Complex algorithms and data structures
  • Large-scale projects requiring strong typing
  • When team has existing C# expertise

GDScript Standards

Naming Conventions

Classes and Scripts:

  • PascalCase for class names: class_name PlayerController
  • snake_case for script files: player_controller.gd
  • One class per file, filename matches class name in snake_case

Functions and Variables:

  • snake_case for functions: calculate_damage(), apply_force()
  • snake_case for variables: player_health, movement_speed
  • UPPER_SNAKE_CASE for constants: MAX_HEALTH, GRAVITY
  • Prefix private members with underscore: _internal_state, _process_input()

Signals:

  • snake_case for signal names: signal health_changed(new_health)
  • Past tense for completed events: signal item_collected
  • Present/continuous for states: signal is_falling

Code Style

extends CharacterBody2D
class_name Player

# Constants
const MAX_SPEED: float = 300.0
const JUMP_VELOCITY: float = -400.0

# Exported variables (shown in Inspector)
@export var health: int = 100
@export var acceleration: float = 10.0

# Private variables
var _current_state: String = "idle"
var _velocity: Vector2 = Vector2.ZERO

# Signals
signal health_changed(new_health: int)
signal player_died()

func _ready() -> void:
    # Initialize on scene ready
    _setup_player()

func _physics_process(delta: float) -> void:
    # Handle physics updates
    _handle_movement(delta)
    move_and_slide()

func take_damage(amount: int) -> void:
    health -= amount
    health_changed.emit(health)
    if health <= 0:
        _die()

func _die() -> void:
    player_died.emit()
    queue_free()

func _handle_movement(delta: float) -> void:
    # Movement logic here
    pass

C# Standards (Godot C#)

Naming Conventions

Classes and Files:

  • PascalCase for classes: PlayerController
  • PascalCase for C# files: PlayerController.cs
  • Inherit from appropriate Godot class

Properties and Methods:

  • PascalCase for public members: Health, TakeDamage()
  • camelCase with underscore for private fields: _currentHealth
  • PascalCase for properties: public int MaxHealth { get; set; }

Code Style

using Godot;

public partial class Player : CharacterBody2D
{
    // Constants
    private const float MaxSpeed = 300.0f;
    private const float JumpVelocity = -400.0f;
    
    // Exported properties (shown in Inspector)
    [Export] public int Health { get; set; } = 100;
    [Export] public float Acceleration { get; set; } = 10.0f;
    
    // Signals
    [Signal]
    public delegate void HealthChangedEventHandler(int newHealth);
    
    [Signal]
    public delegate void PlayerDiedEventHandler();
    
    // Private fields
    private string _currentState = "idle";
    
    public override void _Ready()
    {
        // Initialize on scene ready
        SetupPlayer();
    }
    
    public override void _PhysicsProcess(double delta)
    {
        // Handle physics updates
        HandleMovement((float)delta);
        MoveAndSlide();
    }
    
    public void TakeDamage(int amount)
    {
        Health -= amount;
        EmitSignal(SignalName.HealthChanged, Health);
        
        if (Health <= 0)
        {
            Die();
        }
    }
    
    private void Die()
    {
        EmitSignal(SignalName.PlayerDied);
        QueueFree();
    }
}

Godot Node Architecture

Scene Organization

Main (Node2D)
├── World (Node2D)
│   ├── TileMap
│   ├── Platforms (Node2D)
│   └── Props (Node2D)
├── Entities (Node2D)
│   ├── Player (CharacterBody2D)
│   │   ├── Sprite2D
│   │   ├── CollisionShape2D
│   │   └── AnimationPlayer
│   └── Enemies (Node2D)
│       └── Enemy (CharacterBody2D)
├── UI (CanvasLayer)
│   ├── HUD (Control)
│   └── PauseMenu (Control)
└── Systems (Node)
    ├── GameManager (Node)
    └── AudioManager (Node)

Node Lifecycle

# GDScript Node Lifecycle
func _enter_tree() -> void:
    # Called when node enters the scene tree
    pass

func _ready() -> void:
    # Called once when node is ready
    # All children are ready at this point
    pass

func _process(delta: float) -> void:
    # Called every frame
    # Use for non-physics updates
    pass

func _physics_process(delta: float) -> void:
    # Called at fixed intervals
    # Use for physics-related updates
    pass

func _exit_tree() -> void:
    # Called when node leaves the scene tree
    pass

Resource System

Custom Resources

# enemy_data.gd
extends Resource
class_name EnemyData

@export var enemy_name: String = "Goblin"
@export var max_health: int = 50
@export var move_speed: float = 150.0
@export var damage: int = 10
@export var sprite: Texture2D

Using Resources

extends CharacterBody2D
class_name Enemy

@export var enemy_data: EnemyData

var current_health: int

func _ready() -> void:
    if enemy_data:
        current_health = enemy_data.max_health
        $Sprite2D.texture = enemy_data.sprite

Signal System

Signal Patterns

# Defining and emitting signals
extends Node
class_name GameManager

signal game_started()
signal game_over(score: int)
signal level_completed(level_number: int, time: float)

func start_game() -> void:
    # Game start logic
    game_started.emit()

func end_game(final_score: int) -> void:
    # Game end logic
    game_over.emit(final_score)

Connecting Signals

# Connecting signals in code
func _ready() -> void:
    # Connect with callable
    game_manager.game_started.connect(_on_game_started)
    
    # Connect with binds
    button.pressed.connect(_on_button_pressed.bind(button_id))
    
    # One-shot connection
    timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)

func _on_game_started() -> void:
    print("Game started!")

Autoload (Singleton) Pattern

# GameManager.gd - Add to Project Settings > Autoload
extends Node

var score: int = 0
var current_level: int = 1

func _ready() -> void:
    # Singleton is ready
    process_mode = Node.PROCESS_MODE_ALWAYS  # Continue during pause

func add_score(points: int) -> void:
    score += points
    
func reset_game() -> void:
    score = 0
    current_level = 1
    get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")

Performance Optimization

Object Pooling

# ObjectPool.gd
extends Node
class_name ObjectPool

@export var bullet_scene: PackedScene
@export var pool_size: int = 20

var _bullet_pool: Array[Node] = []

func _ready() -> void:
    for i in pool_size:
        var bullet = bullet_scene.instantiate()
        bullet.set_process(false)
        bullet.set_physics_process(false)
        bullet.visible = false
        add_child(bullet)
        _bullet_pool.append(bullet)

func get_bullet() -> Node:
    for bullet in _bullet_pool:
        if not bullet.visible:
            bullet.set_process(true)
            bullet.set_physics_process(true)
            bullet.visible = true
            return bullet
    
    # Pool exhausted, create new instance
    var new_bullet = bullet_scene.instantiate()
    add_child(new_bullet)
    _bullet_pool.append(new_bullet)
    return new_bullet

func return_bullet(bullet: Node) -> void:
    bullet.set_process(false)
    bullet.set_physics_process(false)
    bullet.visible = false

Performance Best Practices

  • Cache node references in _ready() instead of using get_node() repeatedly
  • Use set_physics_process(false) and set_process(false) for inactive objects
  • Implement LOD (Level of Detail) for complex scenes
  • Use VisibleOnScreenNotifier2D to disable off-screen processing
  • Batch draw calls by using texture atlases
  • Minimize use of get_node() in loops

Input Handling

Input Map Configuration

# Configure actions in Project Settings > Input Map
# Then use in code:

func _ready() -> void:
    # Set up input actions in Project Settings
    pass

func _process(_delta: float) -> void:
    if Input.is_action_just_pressed("jump"):
        _jump()
    
    if Input.is_action_pressed("move_left"):
        velocity.x = -SPEED
    elif Input.is_action_pressed("move_right"):
        velocity.x = SPEED
    else:
        velocity.x = 0

func _input(event: InputEvent) -> void:
    # For one-time inputs
    if event.is_action_pressed("pause"):
        get_tree().paused = not get_tree().paused

Error Handling

GDScript Error Handling

func load_save_data() -> Dictionary:
    var save_file = FileAccess.open("user://savegame.dat", FileAccess.READ)
    
    if save_file == null:
        push_error("Failed to open save file")
        return {}
    
    var save_data = save_file.get_var()
    save_file.close()
    
    if save_data == null:
        push_warning("Save file is empty")
        return {}
    
    return save_data

func safe_divide(a: float, b: float) -> float:
    assert(b != 0, "Division by zero!")
    return a / b

Testing

GUT (Godot Unit Test) Framework

# test_player.gd
extends GutTest

var player: Player

func before_each():
    player = preload("res://entities/Player.tscn").instantiate()
    add_child_autofree(player)

func test_player_takes_damage():
    var initial_health = player.health
    player.take_damage(20)
    assert_eq(player.health, initial_health - 20)

func test_player_dies_at_zero_health():
    player.health = 10
    watch_signals(player)
    player.take_damage(10)
    assert_signal_emitted(player, "player_died")

GoDotTest (C# Testing)

using GoDotTest;

[TestClass]
public class PlayerTests : TestClass
{
    private Player _player;
    
    [TestInitialize]
    public void Setup()
    {
        _player = new Player();
        AddChild(_player);
    }
    
    [TestMethod]
    public void PlayerTakesDamage()
    {
        int initialHealth = _player.Health;
        _player.TakeDamage(20);
        AssertEqual(_player.Health, initialHealth - 20);
    }
}

Project Structure

res://
├── scenes/
│   ├── main.tscn
│   ├── levels/
│   │   ├── level_01.tscn
│   │   └── level_02.tscn
│   └── ui/
│       ├── main_menu.tscn
│       └── hud.tscn
├── scripts/
│   ├── player/
│   │   ├── player.gd
│   │   └── player_controller.gd
│   ├── enemies/
│   │   └── enemy_base.gd
│   └── systems/
│       ├── game_manager.gd
│       └── save_system.gd
├── resources/
│   ├── enemy_data/
│   │   ├── goblin.tres
│   │   └── skeleton.tres
│   └── item_data/
├── assets/
│   ├── sprites/
│   ├── audio/
│   │   ├── music/
│   │   └── sfx/
│   └── fonts/
└── tests/
    ├── unit/
    └── integration/

Development Workflow

Story Implementation Process

  1. Understand Requirements:

    • Review story acceptance criteria
    • Identify Godot-specific implementation needs
    • Plan scene structure and node hierarchy
  2. Implementation:

    • Create scenes and scripts following naming conventions
    • Use appropriate node types (CharacterBody2D for players/enemies, Area2D for triggers)
    • Implement in GDScript for rapid iteration, C# for performance-critical code
  3. Testing:

    • Write GUT tests for GDScript
    • Write GoDotTest tests for C#
    • Test on all target platforms
  4. Optimization:

    • Profile using Godot's built-in profiler
    • Optimize draw calls and physics processing
    • Implement object pooling where needed
  5. Documentation:

    • Document complex systems with comments
    • Update story completion status
    • Note any performance considerations

Performance Targets

Frame Rate

  • Desktop: 60 FPS stable
  • Mobile: 60 FPS on modern devices, 30 FPS minimum on older devices
  • Web: 60 FPS in modern browsers

Memory Usage

  • Keep texture memory under control using import settings
  • Monitor scene complexity (node count)
  • Use ResourceLoader for dynamic loading

Build Size

  • Optimize texture compression
  • Remove unused assets
  • Use Godot's export templates appropriately

Platform-Specific Considerations

Mobile

  • Touch input handling with InputEventScreenTouch
  • Screen size adaptation using anchors and containers
  • Performance scaling for different devices

Web

  • Consider download size (use .wasm.gz compression)
  • Handle browser-specific limitations
  • Test in multiple browsers

Desktop

  • Multiple resolution support
  • Windowed/fullscreen toggling
  • Save data in user directory

These guidelines ensure consistent, high-quality game development using Godot's features and best practices while maintaining performance across all target platforms.