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 |
Encoding |
UTF-8 with BOM. |
Line endings |
CRLF. |
Final newline |
None — files do not end with a trailing newline ( |
Trailing whitespace |
Not trimmed automatically ( |
General Rules
-
Prefer
varwhen 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
switchstatements when returning values. -
Add a blank line after any scope block (such as an
ifbranch).
Naming
| Element | Convention | Example |
|---|---|---|
Classes, structs |
PascalCase + semantic suffix |
|
Interfaces |
|
|
Private fields |
|
|
Public / serialized fields |
|
|
Boolean fields |
|
|
Properties |
PascalCase |
|
|
PascalCase |
|
Enums (type and members) |
PascalCase |
|
Methods and coroutines |
PascalCase |
|
Parameters and locals |
|
|
Namespaces |
Hierarchical PascalCase |
|
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-
SerializeFieldattributes 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:
-
System/System.* -
Third-party libraries (alphabetical)
-
Project namespaces (alphabetical)
-
UnityEngine/UnityEngine.*/UnityEditor
Two blank lines separate the using block from the namespace declaration (enforced by .editorconfig).
Class Body Layout
Classes are divided into three major sections, each separated by three blank lines:
-
Properties and fields
-
Unity messages (
Awake,OnEnable,Start,Update,LateUpdate,OnDisable,OnDestroy, etc.) -
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.