The style guidelines and best practices for our engineering team when working with Unity C#.

  1. Style Conventions
  2. C# Guidelines
  3. Unity Guidelines

1. Style Conventions

1.1: Classes use PascalCase

public class MyClass : MonoBehaviour { }

1.2 Methods use PascalCase

private void DoSomething()

1.3 Private fields use camelCase

private int privateField;

1.4 Protected fields use camelCase

protected int protectedField;

1.5 Public Fields: no convention as we don’t use them (see 2.16 Avoid public fields)

1.6 Public Properties use PascalCase

public int PublicField { get; private set; }

1.7 Namespaces use PascalCase

namespace PricklyBear.SDK 

1.8 Parameters use camelCase

public void GetRequestWithURL(bool showWaiting)

1.9 Prefix Events and Actions with On

public Action OnTokenReady { get; private set; }

1.10 Interfaces start with an I

public interface IGameService {}

1.11 Enums use PascalCase

public enum Token 
{
  None,
  Up,
  Down,
  Left,
  Right
}

2. C# Guidelines

2.1 Always use access level modifiers

Always use access level modifiers. Do not omit them. If you don’t write any access modifier, the compiler will assume it’s private, but that doesn’t mean you shouldn’t write the keyword private. Be mindful in our design.

Instead of:

bool isPrivate = true;

void PrivateMethod()
{
}

Do:

private bool isPrivate = true;

private void PrivateMethod()
{
}

2.2 Use readonly modifier for fields that don’t change

private readonly IServiceLocator serviceLocator = new ServiceLocator();

2.3 Use a single declaration per line

Instead of:

private int firstVariable, secondVariable;

Do:

private int firstVariable; 
private int secondVariable;

2.4 Use expression-bodied properties for single-line, read-only properties

// read-only, returns backing field
public int MaxHealth => maxHealth;

// the private backing field
private int maxHealth;

2.5 Add fields before methods

Instead of:

public bool PublicField;

public void SomeMethod()
{
}

protected bool protectedField;
private bool privateField;

Do:

public bool PublicField;
protected bool protectedField;
private bool privateField;

public void SomeMethod()
{
}

2.6 Public fields come before protected fields. Protected fields come before private fields.

Instead of:

private bool privateField;
public bool PublicField;
protected bool protectedField;

Do:

public bool PublicField;
protected bool protectedField;
private bool privateField;

2.7 Public methods come before protected methods. Protected methods come before private methods.

Instead of

private void PrivateMethod()
{
}

public void PublicMethod()
{
}

protected void ProtectedMethod()
{
}

Do:

public void PublicMethod()
{
}

protected void ProtectedMethod()
{
}

private void PrivateMethod()
{
}

2.8 Add one vertical space between methods

Instead of:

public void MethodOne()
{
}
public void MethodTwo()
{
}

Do:

public void MethodOne()
{
}

public void MethodTwo()
{
}

2.9 Always use braces

Instead of:

if(...)
  DoSomething();

Do:

if(...)
{
  DoSomething();
}

2.10 Put braces on a new line

(sorry JS developers)

Instead of:

if(...) {
}

Do:

if(...)
{
}

2.11 Add horizontal spaces to decrease code density

Instead of:

CollectItem(myObject,0,1);

for(inti=0;i<100;i++){DoSomething(i);}

if(x==y)

Do:

CollectItem(myObject, 0, 1);

for (int i = 0; i < 100; i++) { DoSomething(i); }

if(x == y)

2.12 Remove spaces around parenthesis and square brackets

Instead of:

DropPowerUp( myPrefab, 0, 1 );

DoSomething ();

x = dataArray[ index ];

Do:

DropPowerUp(myPrefab, 0, 1);

DoSomething();

x = dataArray[index];

2.13 Indent switch cases

switch (switchCase)
{
  case HandledDomains.PricklyAI:
    var levelID = deeplinkPath[1];
    Debug.Log("Load prickly ai level + " + levelID);
    break;

  case HandledDomains.Unknown:
    Debug.LogWarning("DeepLinkHandler: unhandled deeplink");
    break;
}

2.14 Ensure all cases are handled in a switch statement

Instead of:

enum SwitchCases
{
  Unknown,
  DoOne,
  DoTwo
}

HandledDomains switchCase = HandledDomains.DoOne;

switch (switchCase)
{
  case HandledDomains.DoOne:
    break;

  case HandledDomains.Unknown:
    break;

  // Missing case HandledDomains.DoTwo
}

Do:

enum SwitchCases
{
  Unknown,
  DoOne,
  DoTwo
}

HandledDomains switchCase = HandledDomains.DoOne;

switch (switchCase)
{
  case HandledDomains.DoOne:
    break;

  case HandledDomains.DoTwo:
    break;

  case HandledDomains.Unknown:
    break;
}

2.15 Don’t use strings for branching e.g. switch case value

Instead of:

string switchCase = "PricklyAI";

switch (switchCase)
{
  case "PricklyAI":
    var levelID = deeplinkPath[1];
    Debug.Log("Load prickly ai level + " + levelID);
    break;

  case "Unknown":
    Debug.LogWarning("DeepLinkHandler: unhandled deeplink");
    break;
}

Do:

enum HandledDomains
{
  Unknown,
  PricklyAI
}

HandledDomains switchCase = HandledDomains.PricklyAI;

switch (switchCase)
{
  case HandledDomains.PricklyAI:
    var levelID = deeplinkPath[1];
    Debug.Log("Load prickly ai level + " + levelID);
    break;

  case HandledDomains.Unknown:
    Debug.LogWarning("DeepLinkHandler: unhandled deeplink");
    break;
}

2.16 Avoid public fields

Public fields are generally a bad idea. They’re not encapsulated and it’s hard to tell why they’re public. Are they set in the inspector? Modified by another class? Avoid them. Use private variables whenever possible.

If you need to access something publicly, make it a property with a public getter:

public string MyStringThatNeedsPublicReading { get; private set; }

If you need a private variable, but want to set it in the inspector use SerializeField (on one line):

[SerializeField] private UserAuthenticationToken authenticationToken;

2.17 Avoid public static fields and methods

Use private static, when you need class level data.

Instead of:

public static bool PublicStaticState = true;

The exception to this rule is event fields, where you can set public with properties:

public static Action OnEvent { get; set; }

2.18 Avoid large classes. A class should be responsible for 1 thing. Avoid ‘Manager’ classes that do 100 things in 1 domain.

Large classes are a nightmare to maintain and extend. Avoid them.

As a rough guide, if a class is reaching > 400 lines, it’s time to refactor. Remember the ‘S’ in Solid. A class should only be responsible for one thing,

2.19 Avoid large methods. A method should do 1 thing.

As with large classes, large methods make code resistant to change. Aim to keep function length to < 15 lines.

2.20 Use Guard Clauses

Instead of:

if(userInDB)
{
  if(userAuthorised)
  {
    // User is authorised
  }
}

Do:

if(!userInDB && !userAuthorised)
{
  return;
}

// User is authorised

2.21 All code should live in a namespace

Instead of:

public interface IGameService
{
}

Do:

namespace PricklyBear.SDK
{
  public interface IGameService
  {
  }
}

2.22 Do not commit commented code

Instead of:

private void SetRandomColor()
{
  //Color randomColor = backgroundColors[Random.Range(0, backgroundColors.Length)];
  Sprite bg_sprite = backgroundColors[Random.Range(0, backgroundColors.Length)];
  //PlayerPrefs.SetString("LastBackgroundColor", randomColor.ToString());
  //PlayerPrefs.SetString("LastBackgroundColor", bg_sprite.ToString());
  this.gameObject.GetComponent<Image>().sprite = bg_sprite;
  // LeanTween.value(gameObject, UpdateBackgroundColor, mainCamera.backgroundColor, randomColor, 1);
}

Do:

private void SetRandomColor()
{
  Sprite bg_sprite = backgroundColors[Random.Range(0, backgroundColors.Length)];
  this.gameObject.GetComponent<Image>().sprite = bg_sprite;
}

2.23 Do Not Commit Empty Methods

Especially Unity events. There is a small overhead to having an empty Unity event function.

2.24 Ensure all imports are used

They can safely be removed if greyed out in Visual Studio.

2.25 Add a TODO comment when further work is required but is not currently in scope

//TODO: Add check for edge case X

2.26 Use string interpolation to concatenate short strings.

string firstName = "Rob";
string lastName = "Wells";
string userName = $"{firstName}, {lastName}";
string username = firstname + ", " + lastname;

2.27 Use StringBuilder when appending large number of text

var phrase = "Hello World";
var manyPhrases = new StringBuilder();
for (var i = 0; i < 10000; i++)
{
  manyPhrases.Append(phrase);
}

2.28 Only use Implicit typing when the type is obvious

Instead of:

var type = MethodThatReturnsSomething();

Do:

string type = MethodThatReturnsSomething();
var typeObvious = true;

2.29 Use int rather than unsigned types

The use of int common throughout C#, and it is easier to interact with other libraries when you it.

2.30 Use the concise form of object initialisation

Instead of:

ExampleClass instance1 = new ExampleClass();

Do:

// Either acceptable
var instance1 = new ExampleClass();
ExampleClass instance2 = new();

2.31 Use object initialisers to simplify object creation

var instance1 = new ExampleClass { Name = "Hello", ID = 37414 };

2.32 Program to interface not implementation

Reduce tightly coupled code makes changes easier.

Instead of:

public class ServiceLocator 
{
...       
}

public class ServiceLocatorComp : MonoBehaviour
{
  private readonly ServiceLocator serviceLocator = new();
}

Do:

public class ServiceLocator : IServiceLocator
{
...       
}

public class ServiceLocatorComp : MonoBehaviour
{
  private readonly IServiceLocator serviceLocator = new ServiceLocator();
}

3. Unity Guidelines

3.1 Unity lifecycle event methods come before other methods

Instead of:

public void DoSomething()
{
}

private void Awake()
{
}

private void Start()
{
}

Do:

private void Awake()
{
}

private void Start()
{
}

public void DoSomething()
{
}

3.2 Treat warnings as errors and fix them

Warnings pollute the console and make it difficult to develop new features.

3.3 Do Not Use GameObject.Find();

3.4 Cache Results of GetComponent<>();

3.5 Avoid large hierarchies in scenes

Split your hierarchies. If your GameObjects do not need to be nested in a hierarchy, simplify the parenting. Smaller hierarchies benefit from multithreading to refresh the Transforms in your scene. Complex hierarchies incur unnecessary Transform computations and more cost to garbage collection.

3.6 **Transform once, not twice**

When moving Transforms, use Transform.SetPositionAndRotation to update both position and rotation at once when required. This avoids the overhead of modifying a transform twice.

3.7 Only inherit from Monobehavior when necessary

Only use Monobehaviour when:

  1. You need to attach to a GameObject
  2. You need to hook into Unity’s lifecycle events
  3. You need to receive collision events
  4. You need to receive events from native code
  5. You need to run coroutines

3.8 If you require a Monobehaviour, encapsulate the functionality in a standard C# class.

If you require a Monobehaviour, encapsulate the functionality in a standard C# class and use that:

using UnityEngine;
using UnityEngine.SceneManagement;

namespace PricklyBear.SDK
{
  // ServiceLocatorComp is used just to hook into Unity events. All functionality
  // is handled by the ServiceLocator object
  public class ServiceLocatorComp : MonoBehaviour, IServiceLocator
  {
    public static ServiceLocatorComp Instance { get; private set; }

    // ServiceLocator class contains the required functionality
    private readonly IServiceLocator serviceLocator = new ServiceLocator();

    private void Awake()
    {
      if (Instance != null && Instance != this)
      {
        Destroy(this);
      }
      else
      {
        Instance = this;
      }
    }

    public void AddGlobalService<TGameService>(TGameService serviceInstance)
    {
      serviceLocator.AddGlobalService(serviceInstance);
    }

    public void AddServiceForScene<TGameService>(TGameService serviceInstance)
    {
      serviceLocator.AddServiceForScene(serviceInstance);
    }

    public TGameService Get<TGameService>()
    {
      return serviceLocator.Get<TGameService>();
    }

    private void OnEnable()
    {
      SceneManager.sceneLoaded += OnSceneLoaded;
    }

    private void OnDisable()
    {
      SceneManager.sceneLoaded -= OnSceneLoaded;
    }

    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
      serviceLocator.OnSceneChange(scene.name);
    }
  }
}

They have to be attached to GameObjects making them difficult to share behaviour between classes. If it’s a traditional class, you can just create a new object of that type.

3.9 Avoid Singletons unless they are required in all scenes

Singletons create tightly coupled code, which is hard to change and test. Use the ServiceLocator instead.

3.10 Do not use Debug.Log

It’s seriously slow as it has to pause execution to build a stack trace for each log. It is also a security concern as anything we log is stored as plaintext on the users device.

Use the IDebugLogger service instead. This will log only in the editor and on development builds.

Instead of:

Debug.Log("I'm a log");
Debug.LogWarning("I'm a warning");
Debug.LogError("I'm an error");

Do:

DebugPB.Log("I'm a log", DebugLogLevel.Log);
DebugPB.Log("I'm a warning", DebugLogLevel.Warning);
DebugPB.Log("I'm an error", DebugLogLevel.Error);

Leave a comment