12 KiB
System Architecture
High-level architecture guide for the Skelly project, explaining system design, architectural patterns, and design decisions.
Quick Links:
- Coding Standards: See CODE_OF_CONDUCT.md
- Testing Protocols: See TESTING.md
- Component APIs: See UI_COMPONENTS.md
Overview
Skelly uses a service-oriented architecture with autoload managers providing global services, scene-based components implementing gameplay, and signal-based communication for loose coupling.
Key Architectural Principles:
- Instance-based design: No global static state for testability
- Signal-driven communication: Loose coupling between systems
- Service layer pattern: Autoloads as singleton services
- State machines: Explicit state management for complex flows
- Defensive programming: Input validation, error recovery, fallback mechanisms
Autoload System
Autoloads provide singleton services accessible globally. Each has a specific responsibility.
GameManager
Responsibility: Scene transitions and game state coordination
Key Features:
- Centralized scene loading with error handling
- Race condition protection via
is_changing_sceneflag - Scene path constants for maintainability
- Gameplay mode validation with whitelist
- Never use
get_tree().change_scene_to_file()directly
Usage:
# ✅ Correct
GameManager.start_game_with_mode("match3")
# ❌ Wrong
get_tree().change_scene_to_file("res://scenes/game/Game.tscn")
Files: src/autoloads/GameManager.gd
SaveManager
Responsibility: Save/load game state with security and data integrity
Key Features:
- Tamper Detection: Deterministic checksums detect save file modification
- Race Condition Protection: Save operation locking prevents concurrent conflicts
- Permissive Validation: Auto-repair system fixes corrupted data
- Type Safety: NaN/Infinity/bounds checking for numeric values
- Memory Protection: File size limits prevent memory exhaustion
- Version Migration: Backward-compatible save format upgrades
- Error Recovery: Multi-layered backup and fallback systems
Security Model:
# Checksum validates data integrity
save_data["_checksum"] = _calculate_checksum(save_data)
# Auto-repair fixes corrupted fields
if not _validate_save_data(data):
data = _repair_save_data(data)
# Race condition protection
if _is_saving:
return # Prevent concurrent saves
Testing: See TESTING.md for comprehensive test suites validating checksums, migration, and integration.
Files: src/autoloads/SaveManager.gd
SettingsManager
Responsibility: User settings persistence and validation
Key Features:
- Input validation with NaN/Infinity checks
- Bounds checking for numeric values
- Security hardening against invalid inputs
- Default fallback values
- Type coercion with validation
Files: src/autoloads/SettingsManager.gd
DebugManager
Responsibility: Unified logging and debug UI coordination
Key Features:
- Structured logging with log levels (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)
- Category-based log organization
- Global debug UI toggle (F12 key)
- Log level filtering for development/production
- Replaces all
print()andpush_error()calls
Usage: See CODE_OF_CONDUCT.md for logging best practices.
Files: src/autoloads/DebugManager.gd
AudioManager
Responsibility: Music and sound effect playback
Key Features:
- Audio bus system (Music, SFX)
- Volume control per bus
- Loop configuration for music
- UI click sounds
Files: src/autoloads/AudioManager.gd
LocalizationManager
Responsibility: Language switching and translation management
Key Features:
- Multi-language support (English, Russian)
- Runtime language switching
- Translation file management
Files: src/autoloads/LocalizationManager.gd
Scene Management Pattern
All scene transitions flow through GameManager to ensure consistent error handling and state management.
Scene Loading Flow
User Action
↓
GameManager.start_game_with_mode(mode)
↓
Validate mode (whitelist check)
↓
Check is_changing_scene flag
↓
Load PackedScene with validation
↓
change_scene_to_packed()
↓
Reset is_changing_scene flag
Race Condition Prevention
var is_changing_scene: bool = false
func start_game_with_mode(gameplay_mode: String) -> void:
# Prevent concurrent scene changes
if is_changing_scene:
DebugManager.log_warn("Scene change already in progress", "GameManager")
return
is_changing_scene = true
# ... scene loading logic ...
is_changing_scene = false
Why This Matters: Multiple rapid button clicks or input events could trigger concurrent scene loads, causing crashes or undefined state.
Modular Gameplay System
Game modes are implemented as separate gameplay modules in scenes/game/gameplays/.
Gameplay Architecture
Game.tscn (Main scene)
↓
├─> Match3Gameplay.tscn (Match-3 mode)
├─> ClickomaniaGameplay.tscn (Clickomania mode)
└─> [Future gameplay modes]
Pattern: Each gameplay mode:
- Extends Control or Node2D
- Emits
score_changedsignal - Implements
_ready()for initialization - Handles input independently
- Includes optional debug menu
Files: scenes/game/gameplays/
Design Patterns Used
Instance-Based Architecture
Problem: Static variables prevent testing and create hidden dependencies.
Solution: Instance-based architecture with explicit dependencies.
# ❌ Bad: Static global state
static var current_gem_pool = [0, 1, 2, 3, 4]
# ✅ Good: Instance-based
var active_gem_types: Array = []
func set_active_gem_types(gem_indices: Array) -> void:
active_gem_types = gem_indices.duplicate()
Benefits:
- Each instance isolated for testing
- No hidden global state
- Explicit dependencies
- Thread-safe by default
Signal-Based Communication
Pattern: Use signals for loose coupling between systems.
# Component emits signal
signal score_changed(new_score: int)
# Parent connects to signal
gameplay.score_changed.connect(_on_score_changed)
Benefits:
- Loose coupling
- Easy to test components in isolation
- Clear event flow
- Flexible subscription model
Service Layer Pattern
Autoloads act as singleton services providing global functionality.
Pattern:
- Autoloads expose public API methods
- Components call autoload methods
- Autoloads emit signals for state changes
- Never nest autoload calls deeply
State Machine Pattern
Complex workflows use explicit state machines.
Example: Match-3 tile swapping
WAITING → SELECTING → SWAPPING → PROCESSING → WAITING
Benefits:
- Clear state transitions
- Easy to debug
- Prevents invalid state combinations
- Self-documenting code
Critical Architecture Decisions
Memory Management: queue_free() Over free()
Decision: Always use queue_free() instead of free() for node cleanup.
Rationale:
free()causes immediate deletion (crashes if referenced)queue_free()waits until safe deletion point- Prevents use-after-free bugs
Implementation:
# ✅ Correct
for child in children_to_remove:
child.queue_free()
await get_tree().process_frame # Wait for cleanup
# ❌ Dangerous
for child in children_to_remove:
child.free() # Can crash
Impact: Eliminated multiple potential crash points. See CODE_OF_CONDUCT.md for standards.
Race Condition Prevention: State Flags
Decision: Use state flags to prevent concurrent operations.
Rationale:
- Async operations can overlap without protection
- Multiple rapid inputs can trigger race conditions
- State flags provide simple, effective protection
Implementation: Used in GameManager (scene loading), SaveManager (save operations), and gameplay systems (tile swapping).
Error Recovery: Fallback Mechanisms
Decision: Provide fallback behavior for all critical failures.
Rationale:
- Games should degrade gracefully, not crash
- User experience > strict validation
- Log errors but continue operation
Examples:
- Settings file corrupted? Load defaults
- Scene load failed? Return to main menu
- Audio file missing? Continue without sound
Input Validation: Whitelist Approach
Decision: Validate all user inputs against known-good values.
Rationale:
- Security hardening
- Prevent invalid state
- Clear error messages
- Self-documenting code
Implementation: Used in SettingsManager (volume, language), GameManager (gameplay modes), Match3Gameplay (grid movements).
System Interactions
Typical Game Flow
Main Menu
↓
[GameManager.start_game_with_mode("match3")]
↓
Game Scene Loads
↓
Match3Gameplay initializes
↓
├─> SettingsManager (load difficulty)
├─> AudioManager (play background music)
└─> DebugManager (setup debug UI)
↓
Gameplay Loop
↓
├─> Input handling
├─> Score updates (emit score_changed signal)
├─> SaveManager (autosave high score)
└─> DebugManager (log important events)
Signal Flow Example: Score Change
Match3Gameplay detects match
↓
[emit score_changed(new_score)]
↓
Game.gd receives signal
↓
Updates score display UI
↓
SaveManager.save_high_score(score)
Debug System Integration
User presses F12
↓
DebugManager.toggle_debug_ui()
↓
[emit debug_ui_toggled(visible)]
↓
All debug menus receive signal
↓
Show/hide debug panels
Quality Improvements
The project implements high-quality code standards from the start:
Key Quality Features:
- Memory Safety: Uses
queue_free()pattern for safe node cleanup - Error Handling: Comprehensive error handling with fallbacks
- Race Condition Protection: State flag protection for async operations
- Instance-Based Architecture: No global static state for testability
- Code Reuse: Base class architecture (DebugMenuBase) for common functionality
- Input Validation: Complete validation coverage for all user inputs
These standards are enforced through the CODE_OF_CONDUCT.md quality checklist.
Best Practices
Extending the Architecture
When adding new features:
-
New autoload?
- Only if truly global and used by many systems
- Consider dependency injection instead
- Keep autoloads focused on single responsibility
-
New gameplay mode?
- Create in
scenes/game/gameplays/ - Extend appropriate base class
- Emit
score_changedsignal - Add to GameManager mode whitelist
- Create in
-
New UI component?
- Create in
scenes/ui/components/ - Follow UI_COMPONENTS.md patterns
- Support keyboard/gamepad navigation
- Emit signals for state changes
- Create in
Architecture Review Checklist
Before committing architectural changes:
- No new global static state introduced
- All autoload access justified and documented
- Signal-based communication used appropriately
- Error handling with fallbacks implemented
- Input validation in place
- Memory management uses
queue_free() - Race condition protection if async operations
- Testing strategy defined
Related Documentation
- CODE_OF_CONDUCT.md: Coding standards and best practices
- TESTING.md: Testing protocols and procedures
- UI_COMPONENTS.md: Component API reference
- CLAUDE.md: LLM assistant quick start guide