Skip to content

Latest commit

 

History

History
436 lines (324 loc) · 9.25 KB

CONTRIBUTING.md

File metadata and controls

436 lines (324 loc) · 9.25 KB

Contributing

Below are some guidelines for contributing to this project. They are mostly suggestions rather than rules.

Feel free to submit pull requests, referencing any relevant issues and listing a brief overview of the changes in the pull request.

Commits

Commit messages should target around a 50 character long headline, with any addition information following two line breaks. Fork and GitHub use this format when you are writing commit messages.

🎨 Cleanup and formatting of code

Used automatic formatting tools to cleanup and unify code style.

Gitmoji

Gitmoji

This project follows commit guidelines specified by Gitmoji, with the exception of using the raw unicode character (✨) rather than a string (:sparkles:).

Description
Introduce new features
🚧 Work in progress
🎨 Improve structure / format of the code
Improve performance
🐛 Fix a bug
🔥 Remove code or files
📝 Add or update documentation
Add or update tests
find out more...

Software Design Patterns

This project utilises software design patterns to create clean and flexible code.

Fluent interface

Use a fluent interface where appropriate. Fluent interfaces can be used to replace complex constructors.

schema = ProjectManifestBuilder.Create()
    .UseFrameworkTypes()
    .UseType(typeof(SerializerBaseObject))
    .UseType(typeof(SerializerChildObject))
    .Build();

More complex factories can be used to construct more complex objects. Delegates can be invoked to allow nesting of factories.

var worldEngine = new WorldEngineFactory()
    .UseEntity(EntityTypes.Unit, options =>
    {
        options.AddComponent<TransformComponent>();
        options.AddComponent<UnitComponent>();
    })
    .UseEntity(EntityTypes.TerrainChunk, options =>
    {
        options.AddComponent<TransformComponent>();
        options.AddComponent<UnitComponent>();
    })
    .Build();

Service pattern

Use the "service" patterns to allow behaviours to be abstracted to alternate implementations.

public interface ISomethingService
{
    // Insert method declarations here
}

public class StaticSomethingService : ISomethingService
{
    // Insert method implementations here
}

public class DynamicSomethingService : ISomethingService
{
    // Insert method implementations here
}

Procedure pattern

The procedure pattern involves not making changes directly to an object; instead opting for authoring a "procedure" which is applied to an object.

Procedures that can be serialized can be transported over the network and be used as RPCs. Procedures can be authored ahead-of-time and either applied or rejected.

Procedure pattern "world" example

Let's say you have an object that you want to modify. In our case it's the World object (as defined below).

public class World
{
    public List<Character> Characters { get; } = new();
}

public class Character
{
    public int Health { get; set; } = 100;
}

We can create a set of IWorldProcedures that can be used to modify the World object and provide them with implementations.

public interface IWorldProcedure
{
    void ApplyToWorld(World world);
}

public class CharacterCreateProcedure : IWorldProcedure
{
    public void ApplyToWorld(World world)
    {
        world.Characters.Add(new Character());
    }
}

public class CharacterTakeDamageProcedure : IWorldProcedure
{
    public int Index { get; set; } 
    public int Damage { get; set; } 

    public void ApplyToWorld(World world)
    {
        var character = world.Characters[Index];

        character.Health -= Damage;
    }
}

Finally, instead of applying changes directly to the World object, we can author IWorldProcedures and use them to modify the World.

var world = new World();

var procedures = new Queue<IWorldProcedure>();

procedures.Enqueue(new CharacterCreateProcedure());
procedures.Enqueue(new CharacterTakeDamageProcedure()
{
    Index = 0,
    Damage = 10
});

// Transport procedures across the network...

while (procedures.Count > 0)
{
    var procedure = procedures.Dequeue();

    procedure.ApplyToWorld(world);
}

Code Hygiene

Encapsulation

Avoid using public fields in favour of public properties, unless you are creating highly-optimized, SIMD-optimized, or byref fields.

// Prefer:
public string Name { get; }

// Over:
public string name;

Use DebuggerBrowsable to hide unnecessary fields

When a field is represented by a property it will appear twice in the debugger. Using DebuggerBrowsable to hide unnecessary fields makes the debugger easier to read.

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string name;

public string Name => name;

Create unit and integration tests

Code should have as much unit test and integration test coverage as reasonably possible.

Unit and integration tests are created with NUnit.

using NUnit.Framework;
using ProjectNamespace;

namespace ProjectNamespace.UnitTests
{
    [TestFixture(TestOf = typeof(ProjectClass))]
    public class ProjectClassShould
    {
        [Test, Parallelizable]
        public void PassTests()
        {
            Assert.Pass();
        }
    }
}

Code Style

Visual Studio Visual Studio JetBrains Rider

An .editorconfig is included in the project that will ensure that your IDE follows the projects code style. I recommend you use an IDE that supports .editorconfig files such as Visual Studio.

detailed breakdown of rules

Spacing

Don't insert a space between method name and its opening parenthesis for method declaration or method calls.

// Prefer:
private void Bar(int x)
{
    Foo();
}

// Over:
private void Bar (int x)
{
    Foo ();
}

Insert space after keywords in control flow statements (for, foreach, using, while, if, e.t.c).

// Prefer:
for (int i; i < 10; i++)
{
}

// Over:
for(int i; i < 10; i++)
{
}

Indentation

Indentation should be denoted using tabs rather than spaces.

// Prefer:// Over:
․․․․

Switch statement code blocks should be indentented once.

// Prefer:
switch (value)
{
    case 1:
    {
        Console.WriteLine("one");
        break;
    }
}

// Over:
switch (value)
{
    case 1:
        {
            Console.WriteLine("one");
            break;
        }
}

Code Blocks

New lines should be inserted before the opening parenthesis of method blocks and control flow statements.

// Prefer:
if (condition)
{
    Console.WriteLine("Output");
}

// Over:
if (condition) {
    Console.WriteLine("Output");
}

Single-line code-blocks should have explicit parenthesis.

// Prefer:
if (condition)
{
    Console.WriteLine("Output");
}

// Over:
if (condition)
    Console.WriteLine("Output");

Naming

Use var over explicit type names when the type isn't a framework type.

// Prefer:
var stream = new MemoryStream();

// Over:
MemoryStream stream = new MemoryStream();

Use predefined type names over framework type names for data types.

// Prefer:
string fieldA;
int fieldB;
bool fieldC;

// Over:
String fieldA;
Int32 fieldB;
Boolean fieldC;

Use predefined type names over framework type names for accessing a type's static members.

// Prefer:
string.Join(", ", names);
int.Parse(input);

// Over:
String.Join(", ", names);
Int32.Parse(input);

Interfaces should be prefixed with I.

// Prefer:
public interface IInterfaceName
{
}

// Over:
public interface InterfaceName
{
}

Class and struct names should be PascalCase.

// Prefer:
public class ClassName
{
}

// Over:
public class className
{
}

Methods (instance and static) should be PascalCase.

// Prefer:
public void Run();

// Over:
public void run();

Properties (instance and static) should be PascalCase.

// Prefer:
public string PropertyA { get; set; }
public string PropertyB => PropertyA;

// Over:
public string propertyA { get; set; }
public string propertyB => propertyA;

Fields (instance and static) should be camelCase.

// Prefer:
private string field;

// Over:
private string Field;

Constants should be PascalCase.

// Prefer:
private const string ConstantValue;

// Over:
private const string CONSTANT_VALUE;