Maintaining a code base for six years is a hard task when you don't start early on to create conventions and paradigms for your coding guidelines, especially if you have fluctuations in your programmers (working students, retirements, freelancers,...). Over the years, about 12 different programmers of various experience contributed to our code base. While doing code reviews with colleagues is a good opportunity to teach other (junior) programmers your coding guidelines, it is also important to listen to your coworkers. In fact in my 10 years of coding I've never experienced a single guideline which wasn't broken at least once, but always with good reason. And you should know the reasons and maybe even adjust your guidelines then.
The main reason for our guidelines is to create readable and easy to debug code, i.e. no too complex instructions or equations and trying not to hide variables in instructions which could be important for debugging. This of course didn't produce always the best or most optimized code, but that was most times better than having someone not understanding how to fix a bug or even introduce a new one.
A thing which irritated me many times when I got applications for an open programmer position was finding code in a non-english language. One thing was that it was sometimes nearly impossible to read some of the code (no, I can't read chinese, russian,... variable names). The other thing we stumbled upon in our team was that it was merely confusing if people talked about the same thing, but named it differently in their own language (our company is located in Germany). So english were forced to be used in code, comments and (technical) documentation all the time to avoid that confusion.
I've split the guidelines into three categories:
General, general guidelines to consider before actually writing code.
C# specific, which were the guidelines to write any C# code.
Unity specific, which were to avoid some pitfalls in writing for Unity3D.
Again, all the guidelines were not set into stone! If there was a good reason to break one, all had to know why and it was okay for everyone. The most important thing was that all programmers were fine with the guidelines and that they worked for the whole team.
So let's take a look at the guidelines:
The coding language is english. Class, variable, function and method names are all in english, as well as all written comments or header information.
Use spaces instead of tabs (that way the code looks the same on every other machine). A tab is 4 spaces.
Names of variables and functions should be descriptive. Long names are no problem, undescriptive names are. Only use abbreviations if they are very clear and commonly known.
No hungarian notation.
Every comma follows a space (e.g. myFunction(int a, int b, int c)).
Use comma to improve readability, but keep it to one comma (e.g. add(1 + 2) instead of add(1+2)).
Avoid making large classes. Extract generic functionality to helper classes. A single class file should not exceed more than 500 lines.
Write short functions, preferably no longer that 100 lines.
Don't write lines that are longer than what fits on a normal 1920*1080 screen.
When working on own branches, update your branch frequently from the master/developer branch to stay up to date and avoid large merge conflicts.
Commit small chunks of changes instead of huge commits. Makes it easier to review and revert.
Every class, interface, struct, enum is implemented in its own file. Only class intern private structs, classes, enums are allowed inside the same file (use within reasons).
Actions and events are declared below, after properties and fields.
Delegate function declaration and constant values are declared right after class declaration.
Functions and methods start with entry functions: Constructor (and for MonoBehaviours: Awake, OnEnable, Start) with an optional Initialize function. Following exit functions: Destructor (OnDisable, OnDestroy).
Class, enumeration and function names are starting with a capital (UpperCamelCase).
Constants are all capital letters.
For private fields which are used for temporary data storage (e.g. between 3rd party plugin callbacks), use "_" before the variable name to display its special usage.
The order for variable declarations is: as SerializedField annotated variables, public and internal properties and fields (first properties, then fields), protected and private properties and fields (first properties, then fields).
Avoid partial classes.
Use extension methods if needed.
Use correct visibility for classes and functions: private, protected, internal, public.
Always add visibility classifier. So instead of "void myFunc()" write "private void myFunc()".
Do not use regions, as long as it doesn't contain code which don't surprise other developers. (e.g. our lazy property pattern is widely used and doesn't contain any surprises).
Avoid flag booleans to describe states of objects if possible. States of objects can most often be checked by other, already present, variables (e.g. "if(joinedTournament != null) playerJoined = true" introduces the redundant flag "playerJoined" as you can simply ask if "joinedTournament != null")
Put curly brackets into extra lines. They may never be left out, except for trivial checks (e.g. if(callback != null) callback() ).
Use curly brackets for switch cases to open and close case implementation.
Use precompiler definitions as less as possible.
Don't use aliases.
Use implicit namespaces. I.e. "var myGameObject = new GameObject();" instead of "var myGameObject = new UnityEngine.GameObject();"
Ternary operator ( ? : ) may be used in trivial cases. Never nest it though, like: var exceptionSupport = exceptionLevel == 0 ? WebGLExceptionSupport.None : exceptionLevel == 1 ? WebGLExceptionSupport.ExplicitlyThrownExceptionsOnly : WebGLExceptionSupport.FullWithoutStacktrace;
Avoid nesting variable declarations in functions. That makes them hard to debug. So instead of: add(multiply(a, b), c); write: var ab = multiply(a, b);
Use explicit null checks (e.g. if(myObject != null) instead of if(myObject))
Use object pooling when it makes sense.
Try to avoid Lambda and closure functions. Instead use reusable delegate functions.
Use for loops whenever possible. Store list variable before using it.
Use "var" when type initializer explicitly defines type (i.e. "var myFloat = 1f;" instead of "float myFloat = 1f;")
Use cached references to Unitys components: transform, gameObject,... by using lazy properties.
Avoid Unitys build in properties to get components. Especially in Update() loops! They are always calling GetComponent<>().
Use Time.timeScale with care. It has many side effects (wanted and unwanted).
Remove Start, Awake, Update,... functions used by Unity. They are called even if they are empty.
These guidelines are meant to be an inspiration for other developers. You can agree and disagree with lots of them, they worked for us but that doesn't mean that they will also work for you. Think about what you want to achieve with the guidelines and where they can help you and your team to avoid problems.
Feel free to write your experience with code guidelines into the comments section. You can also write me on twitter if you want to discuss our guidelines with me.
Also take a look at other very good blog posts from other developers, which inspired me: