While the following standards should be adhered to, should a scenario arise that these standards did not foresee, it is in the judgment of the developer to momentarily ignore them, possibly provide an explanation code comment, and discuss amendments to the standard.
- Unless otherwise specified, the code adheres to the naming conventions of C# provided by Microsoft.
- Use the standard formatting of the Resharper file provided by the project. (Rider or Visual Studio with Resharper required)
- The language of the code including code documentation and code comments is English without exceptions.
¶ Additional code standards
Code standards mentioning the private
access modifier are also applicable to the protected
access modifier should inheritance require it.
Code standards mentioning the public
access modifier are also applicable to the internal
access modifier should the project structure require it.
¶ General
- Code warnings are not ignored. All warnings are resolved until the IDE gives the file a ✅.
- If warnings are 'false-alarms' or are considered a compromise they are ignored through a Resharper ignore comment using 💡. A short comment separated by
-
to justify the ignoration is always provided.
Example:SceneChangeManager.Instance.InteractionDefineFade(Fade(CurrentInteraction.FadeDuration.Value));
Warning here ^// ReSharper disable once PossibleInvalidOperationException - FadeDuration will always have a value at this point
SceneChangeManager.Instance.InteractionDefineFade(Fade(CurrentInteraction.FadeDuration.Value));
- If warnings are 'false-alarms' or are considered a compromise they are ignored through a Resharper ignore comment using 💡. A short comment separated by
if
,else
,for
, etc. always have their{ }
and are on separate lines.- The only exception is the switch case.
if
clauses that function as abort-checks at the beginning of a file are written on a single line including their{ return; }
if they contain only a single return statement.- In such cases, the
if
clause is formed in a way to reduce nesting andelse
clauses.
- In such cases, the
- When accessing static functions or fields of build-in types, CLR type names are used.
- Example:
float
.Epsilon → Single.Epsilon
- Example:
- Comments always start with a space and a capital letter.
- Example:
// This is a comment
- Example:
Fields and Properties
- All fields that are exposed in the inspector are
private
and marked with[SerializeField]
unless there valid reasons not to. - Values that are accessible by other classes are
public
are properties.- Auto-properties are used if possible.
- Properties with explicit backing fields are
private
and have an underscore (_
) prefix.
- Properties with explicit backing fields are
- In the case of an inspector field that is accessible by other classes, the field remains
private
and acts as a backing field for apublic
property. - Restrict property accessors if possible.
- Additional logic inside the accessors are kept to a minimum and only ensure the stability of the data structures or logic.
- Example:
protected float CurrentHealth
{
get => _currentHealth;
set => _currentHealth = Math.Max(value, 0);
}
- Example:
- Auto-properties are used if possible.
- Values only used inside the containing class are
private
fields, unless they are properties that require additional logic inside their accessors or are expression-bodied properties.- Examples:
- Components acquired by
GetComponent<>()
- Internal fields such as for temporary values.
- Components acquired by
- Examples:
¶ MonoBehaviours
[DissallowMultipleComponents]
is used on all MonoBehaviours that qualify.- If a execution order of a MonoBehaviour is changed in the project settings a explanatory comment has to be put on the class.
- All inspector fields contain a
[Tooltip]
explaining their function. - All inspector fields contain sensible initialization values if they are known.
- Inspector fields must not have an explicitly set initialization value of
null
unless they are intended to benull
at some point during execution. Inspector fields without an initialization value are used to signal that they are required to be set before execution. - Use
[Required]
for inspector fields that must be set in the inspector before execution. - If a inspector fields numeric value expresses a common unit such as "seconds" or "Unity Units/minute" etc. use
[SuffixLabel]
using the classHats.Labels
.
- Inspector fields must not have an explicitly set initialization value of
- If a MonoBehaviour contains more than a few inspector fields, they are grouped by
[FoldoutGroup]
.- Additional sub-grouping can be done by employing other groups such
[BoxGroup]
as or[TitleGroup]
etc. - The use of
[Header]
or[Title]
should be avoided. - If present, a "Gizmo" foldout is always located at the bottom of the inspector.
- Additional sub-grouping can be done by employing other groups such
¶ Namespaces & Regions
- All classes are located inside the projects namespace
Hats
- Further division by sub-namespaces is done on a case to case basis.
- Classes that are considered "Utility" that would be put in a
Hats.Utility
namespace are instead put directly in theHats
namespace. - Using directives of external namespaces are grouped and separated with a blank line by their top-level namespace name and are ordered alphabetically.
- Using directives of sub-namespaces of the project are kept as short as possible by placing them inside the declared namespace
- Example:
namespace Hats.Characters.Player
{
using Movement;
using Enemies;// ...
}
- Example:
- Using directives of sub-namespaces of the project are kept as short as possible by placing them inside the declared namespace
- All preprocessor directives are not indented
- An exception is
#region
which has the same indention as it's immediate content.
- An exception is
- All but the most trivial MonoBehaviours are structured by using
#region
grouped by functionality and regions of responsibility (And not by the type of code with the exception of the Unity event functions and inspector values described below)- Regions are named in Title Case with spaces between words.
#region
and#endregion
are surrounded by a blank line, unless they are other preprocessor directives that are not#region
or#endregion
- There are regions that are always present, located relatively at the top of the class.
- Inspector - containing all inspector fields or other controls that are visible in the inspector. Is always present located relatively at the top of the class.
- Unity Event Functions - containing all Unity event functions. Is always present located relatively at the top of the class.
- Inspector Helpers - contain functions etc. specifically to facilitate the function of attributes for the inspector values.
- If present a Gizmos region (or multiple) exists, it is located at the very bottom (Order = 999) of the inspector (or group if nested). A template for such a region can be found at bottom of this page.
¶ Additional best practices
¶ General
- When creating a custom data structure or utility classes, code documentation is highly encouraged!
- Code documentation for all classes function and fields is encouraged, especially under the aspect that other developers may need to use this code as well.
- The usage of
var
or explicit types is very situational and is at the discretion of the developer, but a good rule of thumb should be that if usingvar
, the type should be evident by the right hand of the assignment (e.g. based on the name of the function call).
¶ MonoBehaviour
[RequireComponent]
should only be used sparingly when there is a strong connection between a Monobehaviour and a required Component/MonoBehaviour, such as a helper component that manages a collider.- Attributes on fields such as
[Range]/[ValidRange]
,[MinMaxSlider]
,[MinValue]
or[MaxValue]
are recommended when a sensible range can be determined and. Keep in mind, number inspector fields with regular input boxes can also be dragged with the mouse. - Components required by a MonoBehaviour that are ensured to be on the same GameObject should be acquired by code, while components required on (grand)child-GameObjects should be assigned through an inspector field, as they may move in the hierarchy during development.
- Acquiring components outsides a GameObjects hierarchy should be avoided using
FindGameObjectsWithTag
orFind
and should be rather acquired usingFindObjectOfType
. - Unity event functions should contain as little logic as possible, and rather make calls to functions.
- Big exceptions tho this are the acquiring of components though
GetComponent<>()
and the like.
- Big exceptions tho this are the acquiring of components though
- When using Gizmos it is often a good idea to allow it to be toggled or even set to be only visible if the containing GameObject is selected. An example code structure using the GizmoVisibility enum is the following:
#if UNITY_EDITOR
#region Gizmos
[SerializeField] private GizmoVisibility gizmoVisibility;
private void OnDrawGizmos()
{
if (gizmoVisibility != GizmoVisibility.DrawAlways) { return; }
DrawGizmos();
}
private void OnDrawGizmosSelected()
{
if (gizmoVisibility != GizmoVisibility.DrawSelected) { return; }
DrawGizmos();
}
private void DrawGizmos()
{
// ...
}
#endregion
#endif