Code Style

This page is the single source of truth for how C# is written in this project. It is enforced partly by .editorconfig (ReSharper-compatible) and partly by convention. When automated tooling and this page disagree, fix the tooling config and this page together — they should never drift apart.

Files and Formatting

Most of this is enforced by .editorconfig; it is restated here so the intent is discoverable.

Aspect Rule

Indentation

4 spaces in .cs and .uxml; 2 spaces in .asmdef, .json, .yaml.

Encoding

UTF-8 with BOM.

Line endings

CRLF.

Final newline

None — files do not end with a trailing newline (insert_final_newline = false).

Trailing whitespace

Not trimmed automatically (trim_trailing_whitespace = false); don’t add it.

General Rules

  • Prefer var when the type is apparent from the right-hand side.

  • For class methods, always prefer full bodies over expression-bodied () members.

    // Don't
    public void DoSomething() => Debug.Log("Hello");
    
    // Do
    public void DoSomething()
    {
        Debug.Log("Hello");
    }
    This applies to methods. Expression-bodied properties (public BodyStance Stance ⇒ …​) are fine.
  • Prefer pattern matching over switch statements when returning values.

  • Add a blank line after any scope block (such as an if branch).

Naming

Element Convention Example

Classes, structs

PascalCase + semantic suffix

BodyController, BlinkSettings

Interfaces

I prefix + PascalCase

IService, IMessagingEvent

Private fields

_camelCase

_animator, _lookAtEnabled

Public / serialized fields

camelCase (avoid bare public; see Serialization)

blendshapeSmoothing

Boolean fields

is-prefixed where it reads naturally

isActive, isDone

Properties

PascalCase

HeadLookIK, IsDone

const and static readonly

PascalCase

AudioReadStallThresholdSec, StanceIdHash

Enums (type and members)

PascalCase

TurnState.Waiting, BodyStance.Left

Methods and coroutines

PascalCase

SetPoseCorrectionWeights, WatchProgress

Parameters and locals

camelCase

vertical, deltaTime

Namespaces

Hierarchical PascalCase

Avanim.Face.Geyser.Blink

Common class suffixes: Manager, Controller, Handler, Settings (ScriptableObject), Drawer (editor drawer), Hub.

Modifiers

Access modifiers are always explicit — never rely on implicit private.

Modifier order (matches .editorconfig):

public / private / protected / internal → new → static → abstract → virtual → sealed → readonly → override

Serialization

Prefer [SerializeField] private over bare public for any field that needs to appear in the Unity Inspector. Only use public fields when the field must also be accessible from other scripts without a property wrapper.

Prefer exposing Inspector serialization and validation through Odin Inspector attributes ([ShowInInspector], [Required], etc.) rather than writing full custom inspectors.

Attributes

  • [SerializeField] goes on the same line as its field.

  • All other attributes ([Header], [Range], [Unit], [BurstCompile], [CreateAssetMenu], etc.) go on their own line above the member.

  • When multiple non-SerializeField attributes stack, each gets its own line.

[Header("Frequency")]
[Range(0f, 1f)]
[SerializeField] private float blinkRate = 2.57f;

Braces and Single-Line Bodies

Braces are required for any multi-line body. Single-statement bodies omit braces and place the statement on the next line:

if (!_animator)
    return;

File Structure

Using directives

Placed outside the namespace declaration, grouped and sorted:

  1. System / System.*

  2. Third-party libraries (alphabetical)

  3. Project namespaces (alphabetical)

  4. UnityEngine / UnityEngine.* / UnityEditor

Two blank lines separate the using block from the namespace declaration (enforced by .editorconfig).

Namespace

Always block-scoped (namespace Foo.Bar { }) — never file-scoped (namespace Foo.Bar;).

Class Body Layout

Classes are divided into three major sections, each separated by three blank lines:

  1. Properties and fields

  2. Unity messages (Awake, OnEnable, Start, Update, LateUpdate, OnDisable, OnDestroy, etc.)

  3. Custom methods — subdivided into groups (// Public, // Private, // Utils, etc.), also separated by three blank lines

Within the first section, fields are sorted into named categories, each introduced by a // Category comment header. Always present:

  • // References — external dependencies: Inspector-assigned [SerializeField] fields and runtime-retrieved service/component references.

  • // Data — all internal state (private fields that are not external references).

Additional categories (e.g. // Settings, // Systems) may be added when the class warrants it. Properties come before the field categories, without a comment header.

One blank line separates each field category from the next, and each method from the next within a group.

public class BodyController : MonoBehaviour, IService
{
    public BodyStance Stance => (BodyStance)_animator.GetInteger(StanceIdHash);
    public bool HeadLookIK
    {
        get => _lookAtEnabled;
        set => _lookAtEnabled = value;
    }

    // References
    [SerializeField] private BodyAnimationSettings settings;
    [SerializeField] private LookAtIK lookAtIK;
    private Animator _animator;

    // Data
    private float _timeToNextEvent;
    private bool _reactionPlaying;
    private TurnState _turnState;



    private void Awake()
    {
        ServiceLocator.Register(this);
        _animator = GetComponent<Animator>();
    }

    private void Update()
    {
        _timeToNextEvent -= Time.deltaTime;
    }



    // Public

    public void SetPoseCorrectionWeights(float vertical, float horizontal) { ... }


    // Private

    private void ChangeStance() { ... }

    private float SampleDist(float mean, float stdFrac) { ... }
}

Comments

Code carries roughly one comment line per fifteen. Above that, the explanation belongs in Documentation/ instead.

  • /// <summary> on public API whose contract is not obvious from its signature — threading requirements, units, lifetime, non-obvious failure modes. One or two lines. A method whose name and parameters already say it needs no summary; do not restate the signature in prose.

  • Never <para>, <example>, or multi-paragraph XML doc. Design rationale, background and cross-system behaviour go in the matching Antora page, not in the source.

  • // inline comments for non-obvious logic only — do not narrate obvious code.

  • Section header comments (// References, // Public, etc.) are always used as described in Class Body Layout.