13 KiB
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 usingget_node()repeatedly - Use
set_physics_process(false)andset_process(false)for inactive objects - Implement LOD (Level of Detail) for complex scenes
- Use
VisibleOnScreenNotifier2Dto 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
-
Understand Requirements:
- Review story acceptance criteria
- Identify Godot-specific implementation needs
- Plan scene structure and node hierarchy
-
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
-
Testing:
- Write GUT tests for GDScript
- Write GoDotTest tests for C#
- Test on all target platforms
-
Optimization:
- Profile using Godot's built-in profiler
- Optimize draw calls and physics processing
- Implement object pooling where needed
-
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.