565 lines
13 KiB
Markdown
565 lines
13 KiB
Markdown
# 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
|
|
|
|
```gdscript
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
# 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
|
|
|
|
```gdscript
|
|
# 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
|
|
|
|
```gdscript
|
|
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
|
|
|
|
```gdscript
|
|
# 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
|
|
|
|
```gdscript
|
|
# 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
|
|
|
|
```gdscript
|
|
# 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
|
|
|
|
```gdscript
|
|
# 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
|
|
|
|
```gdscript
|
|
# 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
|
|
|
|
```gdscript
|
|
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
|
|
|
|
```gdscript
|
|
# 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)
|
|
|
|
```csharp
|
|
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. |