RealXP Lab Unity C# Style Guide

We’re sharing our internal standards to help you write cleaner, more professional code.

Jul 2, 2025

15 min read

This document outlines how we write and structure Unity C# code at RealXP Lab. It exists to promote readability and maintainability across our projects, and to help our externs build strong habits around clean, professional-grade code. We wanted to make it public to support the broader game dev community, especially those early in their careers.

This guide draws inspiration from Unity’s official naming and code style tips, Unity Code Style Guide by Thomas Jacobsen, and Google C# Style Guide.

Formatting guidelines

Naming conventions

  • PascalCase: Names of classes, structs, public fields, properties, methods, constants, enums, files, directories, namespaces

  • camelCase: Names of local variables, method parameters

  • _camelCase: Names of private fields (no m_, s_, or k_ prefixes)

  • Always explicitly specify access modifiers (e.g. private, public, internal)

  • Variables: Use nouns. Be descriptive and avoid abbreviations. (e.g. playerHealth, enemyCount, scoreMultiplier)

  • Units: Add units to measurable quantities (e.g. rotationRadians, transitionSeconds, colorFactor0To1)

  • Booleans: Prefix with a verb (e.g. isDead, hasPowerUp, canJump)

  • Enums: Use singular nouns (e.g. WeaponType)

    • Bitwise enums marked with [System.Flags] are the exception — use plural names (e.g. AttackModes)

  • Avoid redundant naming: Don’t repeat the class name in member names unnecessarily (e.g. use Score instead of PlayerScore inside a Player class)

  • Methods: Prefix with a verb (e.g. StartGame(), GetDirection())

    • Methods returning bool should ask a question (e.g. IsGameOver(), HasStartedTurn())

  • Interfaces: Prefix with capital I followed by a descriptive adjective or phrase (e.g. IDamageable, ISaveable)

  • Namespaces: Use PascalCase without underscores or special symbols

    • Use namespaces to prevent naming conflicts with global or external types

    • Organize with dot-separated sub-namespaces based on folder structure (e.g. Game/AI/Pathfinding/ should correspond to namespace Game.AI.Pathfinding)

  • Events:

    • Use verb phrases to name events, and make sure they clearly describe the state change

      • Use the present participle (e.g. OpeningDoor) to indicate an event that occurs before the action completes

      • Use the past participle (e.g. DoorOpened) to indicate an event that occurs after the action has completed

    • Use the System.Action delegate by default

    • Prefix event raising methods with On (e.g. OnDoorOpened())

    • Prefix event handler methods with the subject name and underscore (e.g. GameEvents_DoorOpened)

    • Only define custom EventArgs types when necessary

  • MonoBehaviour files: File name must match the MonoBehaviour class. Only one MonoBehaviour per file

Whitespace rules

  • Use Allman style (brace on a new line)

  • Indentation: 4 spaces, no tabs

  • Keep line lengths under 120 characters

  • Always use braces, even for one-line if/else

  • One declaration per line: int health;

  • One statement per line

  • One assignment per statement

  • Space before flow conditions: if (x == y)

  • No space between method name and parentheses: Jump()

  • No space inside array brackets: items[i]

  • Space after commas: DoSomething(x, y, z)

  • No space after opening or before closing parentheses: DoSomething(x)

  • No space after a cast: var x = (float)y

Organization

  • Add using directives at the top of the file to avoid repeated namespace references

    • Always place System imports first, then sort the rest alphabetically

    • Strip unused using directives - keep only the minimally required set

  • Organize class members in the following order:

    1. Fields

    2. Properties

    3. Events

    4. Unity lifecycle methods (Awake, Start, Update, etc.)

    5. Public methods

    6. Private methods

    7. Nested classes or structs

  • Avoid #region unless absolutely necessary

Comments & inspector attributes

  • Use inline comments only when the code isn’t self-explanatory

    • Avoid commenting obvious logic, prefer self-documenting names and structure

    • Assume the what is clear from the code; use comments to explain why — intent or reasoning

    • Use TODO comments to mark follow-ups, pair it with a Notion ticket or owner when possible

    • Start with a capital letter and avoid periods unless it's a full sentence

      // ✅ Prefer this:
      if (player.IsAlive())
      
      // ❌ Instead of:
      // Check if player has enough health to continue
      if (player.health > 0)
      
      // Prevents jumping if already in the air
      if (!isGrounded) return;
      
      // Skip update if game is paused
      if (GameManager.IsPaused) return;
      
      // TODO(TASK-123): Replace with pooled object for performance
      Instantiate(explosionPrefab, transform.position, Quaternion.identity
      
      


  • Use XML summary tags for all public types (classes, structs, enums, interfaces) and their methods

  • Avoid leaving commented-out code in PRs, use version control.

  • Use one inspector attribute per line

  • Place attributes immediately above the member, without blank lines

  • Prefer [Tooltip] over code comments for fields exposed in the Inspector

  • Stick to common decorators:

    • Core: [SerializeField], [Tooltip], [Header], [Space], [Range(min, max)], Min(value)]

    • Conditional display: [HideInInspector]

    • Functionality: [ContextMenu("Menu Text")], [RequireComponent(typeof(Rigidbody))]

    • Text areas: [Multiline], [TextArea(minLines, maxLines)]

    • For auto-properties, use [field: SerializeField] to expose the backing field while maintaining encapsulation

The complete official list of Unity inspector attributes can be found in the Unity Scripting API documentation, starting with [AddComponentMenu].

Coding guidelines

Constants & readonly

  • Use const where possible; otherwise use readonly

  • Prefer named constants to magic numbers

Collections

  • For method inputs, use the most restrictive collection types (IEnumerable<T>, IReadOnlyList<T>, etc.)

  • Use List<T> for outputs unless lazy evaluation is needed

  • Prefer List<T> over arrays unless size is fixed

  • Use arrays for multidimensional data or fixed-size containers

  • Avoid modifying collections while iterating — use RemoveAll() or collect new list

Tuples & return types

  • Prefer named classes over Tuple<> for return types

  • Consider return objects for complex/multi-value returns

Strings

  • Prefer string interpolation for readability (e.g. $"Score: {playerScore}")

  • Use StringBuilder for performance-critical concatenation

LINQ & delegates

  • Avoid long LINQ chains — prefer readability

  • Prefer member extension methods over query syntax (e.g. Where() over from...in...where)

  • Avoid .ForEach() on lists unless it's a one-liner

  • Avoid LINQ in methods that run every frame (Update, FixedUpdate, etc.)

  • Use ?.Invoke() to call delegates

Extension methods

  • Use extension methods only when modifying source is not possible

  • Prefer adding directly to class when feasible

  • Limit extensions to general-purpose methods

Structs vs classes

  • Use structs only for small, immutable, short-lived types

  • Default to class unless performance or memory constraints justify struct

Miscellaneous

  • Use == null for null checks over is null or object.ReferenceEquals(x, null)

  • Use var only when the type is obvious from context

  • Prefer switch over long if-else chains when appropriate

  • Avoid excessively long methods and large parameter lists

  • Avoid method overloading unless necessary

Example


// Using directives are sorted: System first, then alphabetically.
using System;
using UnityEngine;

// Enums use singular nouns.
/// <summary>
/// The state of the player's current action.
/// </summary>
public enum PlayerState
{
    Idle,
    Walking,
    Jumping
}

// Bitwise enums use [Flags] and plural nouns.
/// <summary>
/// Bitwise flags representing the player's available attack modes.
/// </summary>
[Flags]
public enum AttackModes
{
    None = 0,                          // 000000
    Melee = 1,                         // 000001
    Ranged = 2,                        // 000010
    Special = 4,                       // 000100

    MeleeAndSpecial = Melee | Special  // 000101
}

// Interfaces are prefixed with 'I'.
/// <summary>
/// Defines an entity that can receive damage.
/// </summary>
public interface IDamageable
{
    // Public properties use PascalCase.
    int Health { get; }

    // Methods returning bool ask a question.
    bool CanBeDamaged();

    // Method parameters use camelCase.
    void ApplyDamage(float damageAmount);
}

/// <summary>
/// A gold-standard example demonstrating the C# style guide.
/// </summary>
public class StyleGuideExample : MonoBehaviour, IDamageable
{
    // Prescribed member order: Fields -> Properties -> Events -> Unity Methods -> etc.

    // 1. FIELDS
    [Header("Player Stats")]
    [Tooltip("The time elapsed, measured in seconds.")]
    [SerializeField] 
    private float _elapsedTimeInSeconds;

    [Tooltip("Is the player currently immune to damage?")]
    [SerializeField] 
    private bool _isInvincible;

    // Private fields are _camelCase.
    private PlayerState _currentState;

    // 2. PROPERTIES
    [Tooltip("The current health of the player.")]
    // This is the preferred pattern for a field that can be set in the
    // Inspector but should only be changed internally via code.
    [field: SerializeField]
    public int Health { get; private set; } = 100;

    // 3. EVENTS
    public event Action<int> HealthChanging;
    public event Action<int> HealthChanged;

    // 4. UNITY LIFECYCLE METHODS
    private void Update()
    {
        // Keep Unity lifecycle methods clean by calling helper methods.
        HandleInput();
    }

    // 5. PUBLIC METHODS (implementing the interface)
    /// <summary>
    /// Checks if the player is currently able to take damage.
    /// </summary>
    /// <returns>True if the player's health is above 0 and is not invincible.</returns>
    public bool CanBeDamaged()
    {
        return Health > 0 && !_isInvincible;
    }

    /// <summary>
    /// Applies a specified amount of damage to the player.
    /// </summary>
    /// <param name="damageAmount">The amount of damage to apply.</param>
    public void ApplyDamage(float damageAmount)
    {
        if (!CanBeDamaged())
        {
            return;
        }

        // var is used here because the type (int) is obvious.
        var damageAsInt = Mathf.RoundToInt(damageAmount);

        OnHealthChanging(damageAsInt);
        Health -= damageAsInt;
        OnHealthChanged();
    }

    // 6. PRIVATE METHODS (helpers, event raisers, etc.)
    private void HandleInput()
    {
        // Braces are always used for clarity.
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // TODO(TASK-451): Refactor jump logic into its own method.
            Debug.Log("Jump key pressed.");
        }
    }

    // Event-raising methods are prefixed with 'On' and are typically private.
    private void OnHealthChanging(int amount)
    {
        // Use the null-conditional operator to safely invoke events.
        HealthChanging?.Invoke(amount);
    }

    private void OnHealthChanged()
    {
        HealthChanged?.Invoke(Health);

        // String interpolation is preferred for readability.
        Debug.Log($"Player health changed to: {Health}"

We welcome suggestions and contributions. Reach out anytime at team@realxplab.com.

Subscribe to our newsletter

Stay inspired. Get fresh game dev tips, resources, and insights straight to your inbox.

Subscribe to our newsletter

Stay inspired. Get fresh game dev tips, resources, and insights straight to your inbox.

Subscribe to our newsletter

Stay inspired. Get fresh game dev tips, resources, and insights straight to your inbox.

Share this article