Table of Contents

Socially Distant Code Style

Code written for Socially Distant, above all else, must be human. This means that all code in the game should be easily understood by any human being with a reasonable grasp of programming concepts. It should not feel confusing to read for someone with reasonable programming skill. On this page, you'll learn how we write code to reflect that.

Functioning code is good code.

To put it bluntly, it doesn't matter one bit how well-written or "clean" your code is if it doesn't fucking work. Someone will eventually need to read, understand, and debug your code, so it should always be written with that in mind.

This boils down to these core principles:

  • Premature optimization is the root of all evil: Wasting time optimizing your code for a computer before its functionality has actually been battle-tested will most likely result in you having optimized a bug. That bug is still a bug, and will need to eventually be found and fixed. Do not worry about optimizing your code at the cost of readability unless it both functions as-is and its current implementation demonstrably harms the game's performance.

  • Complexity causes problems: Writing complex APIs and abstractions makes troubleshooting a bug more complex. Always focus on getting your code to work before building a complex system around it. If complexity is needed, it will grow organically as your feature becomes more well-integrated with the rest of the codebase.

  • Code fixes problems, code doesn't fix code. If you feel yourself trying to work around part of the game's code, there is a problem with someone's code. You should work to fix that problem directly, not work around it. Sometimes, the problem isn't actually in your own code.

Naming things

The names of classes, variables and methods should, above all else, be understandable by humans.

Out of all things on this page, naming rules in Socially Distant are the most strict. This is because your code will be read by a screen reader, and must be understood when spoken aloud.

Ensuring names can be spoken aloud easily

You must ensure that your names can be spoken aloud by a text-to-speech voice. Here's how:

  1. Avoid abbreviating things. Prefer numberOfPeople over numPeople.
  2. Don't use acronyms. Prefer fileSystem over fs.
  3. Use proper capitalization: Local variables and private variables use camelCase, everything else uses PascalCase. Improper capitalization causes screen readers to guess, which makes things harder to read.
  4. Include unit names when necessary: Prefer angleInRadians over angle, or timeoutInSeconds over timeout.

Appropriate single-letter names, and their assumed meaning

You should never use single-letter names if you can avoid it, as they violate the above rules for screen reader compatibility. Names in this list are acceptable, as long as they're used in the correct context.

Name Meaning Valid context
i Loop counter, or "index" Use when iterating through a collection.
j Nested loop counter Same as i, but for nested loops.
x, y, z, w Components of a 2D, 3D, or 4D vector, or of a quaternion. Math
e Euler's constant Math
e Event arguments Event listeners and callbacks
T Any class, structure, or other type Type parameters and generics

Please note that r, g, b and a (color components) are intentionally absent from that list. You should use red, green, blue, and alpha as names instead.

Nullability

Socially Distant uses nullable reference types. Please write your code with nullability in mind. Do not opt out of nullability with #nullable disable under any circumstances, fix your code instead.

Type structure

When defining any type, such as a class, please use these conventions:

Access level

Access levels should always be specified explicitly. (internal class vs. just class).

All API defined in Socially Distant should use the least-permissive access modifier necessary for the code to function. If it doesn't need to be public, it should not be public.

Always mark as sealed by default

Most types in Socially Distant do not need to be inherited, and this should be communicated. When defining a class, you should always mark it as sealed until someone actually needs to inherit it in another type. This has performance implications and there is no reason to leave an object open for inheritance if it need not be inherited.

Member ordering

When defining the members of a type, please follow this layout to ensure better readability.

  • Fields
  • Properties
  • Events
  • Methods
  • Nested types

Then, order by access level:

  • Public
  • Protected / Internal
  • Private

Then, for fields, order by mutability:

  • Compile-time constant (const)
  • Read-only (`readonly)
  • Writable

Attributes

Attributes are types that you can use to apply special attributes to certain parts of the code.

When applying an attribute to a type or member, always keep attributes on their own lines directly above the member.

This is good:

[Attribute]
public sealed class Class
{
    [Attribute]
    private float field;
    
    [Attribute]
    public void Method()
    {
        this.field += 1;
    }
}

This is bad:

[Attribute] public sealed class Class
{
    [Attribute] private float field;
    
    [Attribute] public void Method()
    {
        this.field += 1;
    }
}

When adding multiple attributes to something, each attribute should be on a separate line:

[Attribute1]
[Attribute2]
[Attribute3]
private float field;

Attributes on method parameters aren't affected by this rule - because they're rare.

Type parameters and Generics

You will run into situations where generics are extremely useful, if not required, as part of your API.

Remember to use descriptive names when writing generic types. Use type constraints and good naming to ensure the correct types are specified by users of your API.

Interfaces vs. Abstract Types

Socially Distant's API uses interfaces all over the place. When designing a system, you should take advantage of this.

Socially Distant uses interfaces to define what a given type must be able to do in order for that type to be valid as part of another API. We do not care how a given thing is done, so long as the implementation conforms to an interface's requirements.

A good example of this is IComputer for representing an in-game computer. It describes what a given type must be able to do in order to be considered a computer. It does not make any concrete assertions on how the computer accomplishes its goal of being a computer.

We do not use abstract types unless there is mandatory concrete behaviour associated with the abstract type. A good example of this is Widget, since all widgets must be able to perform a layout update and must be able to be drawn to the screen, and both these behaviours need to generally behave the same across all widgets.

In other words, if you define an abstract class that only has abstract members with no virtual or concrete ones, then it should instead be defined as an interface.

This is an appropriate use case of abstract types:

public abstract class Animation
{
    private readonly float durationInSeconds;
    private          float progressInSeconds;
    
    protected Animation(TimeSpan duration)
    {
        this.durationInSeconds = (float) duration.TotalSeconds;
    }
    
    public void Update()
    {
        float progress = MathHelper.Clamp(progressInSeconds / durationInSeconds, 0, 1);
        
        OnUpdate(progress);
        
        progressInSeconds += Time.DeltaTime;
    }
    
    protected abstract void OnUpdate(float progressPercentage); 
}

This should be an interface:

public abstract class Animation
{
    public abstract TimeSpan Duration { get; }
    
    public abstract void Update();
}

// Should instead be
public interface IAnimation
{
    public TimeSpan Duration { get; }
    
    public void Update();
}

Sometimes, it's a good idea to have both an interface and an abstract type implementing that interface. The abstract type should implement the interface, so that your API can accept other implementations of said interface.

public interface IAnimation
{
    public TimeSpan Duration { get; }
    public float Progress { get; }
    public bool IsActive { get; }
    
    public void Update();
    public void Cancel();
}

public abstract class Animation : IAnimatio
{
    private readonly TimeSpan duration;
    private          double progressInSeconds;
    private          bool isComplete;
    private          bool isCanceled;
    
    public TimeSpan Duration => duration;
    public float Progress => MathHelper.Clamp((float) (progressInSeconds / duration.TotalSeconds), 0, 1);
    public bool IsActive => !isCompleted && ~isCanceled;
    
    public Animation(TimeSpan duration)
    {
        this.duration = duration;    
    }
    
    public void Update()
    {
        OnUpdate(Progress);
     
        if (progressInSeconds >= duration.TotalSeconds)
        {
            isCompleted = true;
            return;
        }
        
        progressInSeconds += Time.DeltaTime;
    }
    
    protected abstract void OnUpdate(float progress);
}

By designing APIs in this way, when an abstract type is genuinely needed, that type can also be generic.

Whitespace, braces, nesting,, and line breaks

Socially Distant is primarily written in C#, however portions of the game are written in sdsh (the Socially Distant Shell language). These formatting rules depend on what language you're writing in. Learn more about sdsh

Indentation rules

Always use spaces instead of tabs, in both languages.

In C# code, use four spaces for each level of indentation. For sdsh scripts, use two spaces.

Brace style

In C# code, curly braces always belong on their own lines, i.e.

public sealed class Class
{
    public void Method()
    {
        
    }
}

instead of

public sealed class Class {
    public void Method() {
        
    }
}

For sdsh scripts, do the opposite, i.e:

function sayHello() {
  say "Hello, $1!"
}

sayHello Ritchie

This is because sdsh is an interpreted language, so your scripts will take up less space in RAM and less time to execute.

Nesting

In C#, avoid heavy amounts of nesting. If a method nests code more than three levels deep, you should try to fix that.

In sdsh, deeper nesting directly correlates to the script taking more time to execute. Furthermore, nested functions are defined in global scope as the script is executed, so excessive nesting will cause bugs.

Line breaks

In C#, always separate members with a blank line, except for fields with the same access modifiers.

public sealed class Class
{
    public const float Constant = 42;
    
    public readonly float PublicField;
    public readonly float PublicField;
    public readonly float PublicField;
    public readonly float PublicField;
    
    private readonly float Field;
    private readonly float Field;
    private readonly float Field;
    private readonly float Field;
    
    public float Property => Field;
    
    public bool IsDone => false;
    
    public IEnumerable<string> Things
    {
        get
        {
            yield return "One thing";
            yield return "Two thing";
            yield return "Red thing";
            yield return "Blue thing";
        }
    }
    
    public string Name
    {
        get => "Ritchie";
        set => SetName(value);
    }
    
    public event Action? RitchieWasAngered;
    
    public Class()
    {
        Field = Constant;   
    }
    
    private void SetName(string newName)
    {
        RitchieWasAngered?.Invoke();
        Log.Error($"My name is {Name}, not {newName}.");
    }
    
    private struct Struct
    {
        public int Number;
        public string String;
        public bool Boolean;
        
        public void DoScaryThing()
        {
            throw new NotSupportedException("I cannot be scared.");   
        }
    }
}

When writing sdsh scripts, separate groups of similar statements with a blank line.

function Function() {
  command
  command
  command
  
  if condition;
  then
    command Success
  else
    command Fail
  fi
}

VARIABLE=value
VARIABLE=value

export ENV=value
export ENV=value
export ENV=value

Function

say "An octagon has 8 fantastic sides"
say "An octagon also has 8 amazing angles"

playSong --no-loop /Career/BGM/Octagonfire

once SongFinishedEvent exec "logout --force"