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:
- Avoid abbreviating things. Prefer
numberOfPeople
overnumPeople
. - Don't use acronyms. Prefer
fileSystem
overfs
. - Use proper capitalization: Local variables and private variables use
camelCase
, everything else usesPascalCase
. Improper capitalization causes screen readers to guess, which makes things harder to read. - Include unit names when necessary: Prefer
angleInRadians
overangle
, ortimeoutInSeconds
overtimeout
.
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"