Coding Standards
This document outlines the coding standards and conventions for our Godot 4.4.1 C# (.NET 9.0) game project. These standards ensure code consistency, maintainability, and team collaboration.
Table of Contents
- General Principles
- File Organization
- C# Coding Standards
- Godot-Specific Guidelines
- Naming Conventions
- Code Formatting
- Comments and Documentation
- Error Handling
- Performance Guidelines
General Principles
Code Quality
- Readability First: Write code that tells a story. Code is read more often than it's written.
- KISS (Keep It Simple, Stupid): Favor simple solutions over complex ones.
- DRY (Don't Repeat Yourself): Avoid code duplication through proper abstraction.
- YAGNI (You Aren't Gonna Need It): Don't implement features until they're actually needed.
Consistency
- Follow the established patterns in the codebase.
- Use the same naming conventions and code structure throughout the project.
- Maintain consistent indentation and formatting as defined in
.editorconfig
.
File Organization
Directory Structure
Scripts/
├── Actors/ # Player, enemies, NPCs
├── Components/ # Reusable game components
├── Controllers/ # Input handlers, game controllers
├── Data/ # Data classes, scriptable objects
├── Managers/ # Singleton managers (GameManager, AudioManager, etc.)
├── UI/ # User interface scripts
├── Utilities/ # Helper classes and extensions
└── Systems/ # Game systems (inventory, dialogue, etc.)
File Naming
- Use PascalCase for C# files:
PlayerController.cs
- Use snake_case for GDScript files:
player_controller.gd
- Use descriptive names that clearly indicate the file's purpose
- Avoid abbreviations unless they're widely understood
C# Coding Standards
Class Structure
Organize class members in the following order:
- Constants
- Static fields
- Fields (private first, then protected, then public)
- Constructors
- Properties
- Events
- Methods (private first, then protected, then public)
- Nested types
public class PlayerController : CharacterBody2D
{
// Constants
private const float DefaultSpeed = 300.0f;
// Fields
private float _speed;
private Vector2 _velocity;
// Properties
public float Speed
{
get => _speed;
set => _speed = Mathf.Max(0, value);
}
// Godot lifecycle methods
public override void _Ready()
{
_speed = DefaultSpeed;
}
public override void _PhysicsProcess(double delta)
{
HandleMovement();
}
// Private methods
private void HandleMovement()
{
// Implementation
}
}
Access Modifiers
- Always specify access modifiers explicitly
- Use the most restrictive access level possible
- Prefer
private
for internal implementation details - Use
protected
for members that should be accessible to derived classes - Use
public
only for the class's interface
Properties vs Fields
- Use properties for public data access
- Use auto-properties when no additional logic is needed
- Use full properties when validation or side effects are required
// Good - Auto property
public int Health { get; set; }
// Good - Property with validation
public int MaxHealth
{
get => _maxHealth;
set => _maxHealth = Mathf.Max(1, value);
}
// Avoid - Public fields
public int health; // Don't do this
Godot-Specific Guidelines
Node References
- Use
GetNode<T>()
for type-safe node access - Cache node references in
_Ready()
when possible - Use
@export
for designer-configurable values
public partial class PlayerController : CharacterBody2D
{
[Export] public float Speed = 300.0f;
[Export] public float JumpVelocity = -400.0f;
private AnimationPlayer _animationPlayer;
private CollisionShape2D _collisionShape;
public override void _Ready()
{
_animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
_collisionShape = GetNode<CollisionShape2D>("CollisionShape2D");
}
}
Signals
- Use PascalCase for signal names
- Provide clear, descriptive signal names
- Document signal parameters
[Signal]
public delegate void HealthChangedEventHandler(int newHealth, int maxHealth);
[Signal]
public delegate void PlayerDiedEventHandler();
Scene Management
- Use
GetTree().ChangeSceneToFile()
for scene transitions - Keep scene paths in constants or configuration files
- Handle scene loading errors gracefully
Naming Conventions
Classes and Interfaces
- Classes: PascalCase (
PlayerController
,InventorySystem
) - Interfaces: PascalCase with 'I' prefix (
IInteractable
,IDamageable
) - Abstract Classes: PascalCase, consider 'Base' prefix (
BaseWeapon
)
Methods and Properties
- Methods: PascalCase (
GetPlayerInput
,ProcessMovement
) - Properties: PascalCase (
CurrentHealth
,IsGrounded
) - Events: PascalCase with event-like names (
HealthChanged
,PlayerDied
)
Fields and Variables
- Private fields: camelCase with underscore prefix (
_currentHealth
,_isGrounded
) - Local variables: camelCase (
deltaTime
,inputVector
) - Constants: PascalCase (
MaxPlayerCount
,DefaultSpeed
) - Static readonly: PascalCase (
DefaultSettings
)
Godot-Specific
- Exported fields: PascalCase (
Speed
,MaxHealth
) - Signal names: PascalCase (
HealthChanged
,ItemCollected
) - Node paths: Use descriptive names in PascalCase
Code Formatting
Braces and Indentation
- Use Allman style (braces on new lines)
- 4 spaces for indentation in C#
- Tabs for GDScript files
// Good
if (condition)
{
DoSomething();
}
else
{
DoSomethingElse();
}
// Good - Single line statements can omit braces for simple cases
if (isDebug)
GD.Print("Debug message");
Line Length and Spacing
- Maximum 120 characters per line for C#
- Add spaces around operators and after keywords
- No trailing whitespace
- Empty line between logical sections
// Good spacing
public void ProcessInput(double delta)
{
Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
if (direction != Vector2.Zero)
{
_velocity.X = direction.X * Speed;
}
else
{
_velocity.X = Mathf.MoveToward(_velocity.X, 0, Speed);
}
}
Comments and Documentation
XML Documentation
Use XML documentation for public APIs:
/// <summary>
/// Applies damage to the character and triggers appropriate responses.
/// </summary>
/// <param name="damage">The amount of damage to apply</param>
/// <param name="damageType">The type of damage being applied</param>
/// <returns>True if the character was killed by this damage</returns>
public bool TakeDamage(int damage, DamageType damageType)
{
// Implementation
}
Inline Comments
- Explain why, not what
- Use
//
for single-line comments - Use
/* */
for multi-line comments - Keep comments up-to-date with code changes
// Calculate movement with coyote time to improve player experience
if (_timeSinceGrounded < CoyoteTime)
{
_velocity.Y = JumpVelocity;
}
/*
* This complex calculation handles the non-linear relationship
* between input sensitivity and camera movement speed
*/
float sensitivity = Mathf.Pow(rawInput, SensitivityCurve);
TODO Comments
Use consistent format for temporary comments:
// TODO: Implement save/load functionality
// FIXME: Character sometimes clips through walls on steep slopes
// HACK: Temporary workaround for Godot audio bug - remove in v4.5
Error Handling
Exception Handling
- Use specific exception types when possible
- Handle exceptions at the appropriate level
- Log errors with sufficient context
- Don't catch exceptions you can't handle meaningfully
try
{
var saveData = SaveManager.LoadGame(saveSlot);
ApplyGameState(saveData);
}
catch (FileNotFoundException)
{
GD.PrintErr($"Save file not found for slot {saveSlot}");
StartNewGame();
}
catch (JsonException ex)
{
GD.PrintErr($"Corrupted save file: {ex.Message}");
ShowCorruptedSaveDialog();
}
Godot Error Handling
- Check for null before using node references
- Validate export variables in
_Ready()
- Use Godot's built-in error codes when appropriate
public override void _Ready()
{
_animationPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
if (_animationPlayer == null)
{
GD.PrintErr("AnimationPlayer node not found!");
return;
}
if (Speed <= 0)
{
GD.PrintErr("Speed must be greater than 0");
Speed = 300.0f; // Fallback value
}
}
Performance Guidelines
Memory Management
- Minimize allocations in frequently called methods (
_Process
,_PhysicsProcess
) - Reuse objects when possible (object pooling for bullets, particles, etc.)
- Dispose of resources properly
- Avoid boxing/unboxing in hot paths
// Good - Reuse Vector2 instances
private Vector2 _tempVector = Vector2.Zero;
public override void _PhysicsProcess(double delta)
{
_tempVector.X = Input.GetActionStrength("move_right") - Input.GetActionStrength("move_left");
_tempVector.Y = Input.GetActionStrength("move_down") - Input.GetActionStrength("move_up");
if (_tempVector.LengthSquared() > 0.01f) // Use LengthSquared() instead of Length()
{
ProcessMovement(_tempVector);
}
}
Godot Performance
- Use
_PhysicsProcess
for physics-related updates - Use
_Process
for frame-rate dependent updates - Cache node references instead of calling
GetNode()
repeatedly - Use appropriate collision layers and masks
- Consider using
PackedScene.Instantiate()
with object pooling for frequently spawned objects
General Performance Tips
- Prefer early returns to reduce nesting
- Use appropriate data structures (Dictionary vs Array)
- Profile before optimizing
- Avoid premature optimization
// Good - Early return reduces nesting
public void ProcessCollision(KinematicCollision2D collision)
{
if (collision == null)
return;
if (!collision.GetCollider().IsInGroup("interactive"))
return;
// Process interaction
HandleInteraction(collision.GetCollider());
}
Enforcement
These standards are enforced through:
- EditorConfig: Automatic formatting rules
- Code Reviews: Manual verification during pull requests
- Static Analysis: Consider tools like SonarQube or Rider inspections
- Team Discussions: Regular reviews and updates to standards
Remember: These standards are guidelines to improve code quality and team productivity. When in doubt, prioritize readability and maintainability over strict adherence to rules.