I have a large third-party structural approach to work. Game programmers have to create complex systems and therefore it is not enough just to go with the flow. I wrote two documents about how we write code in
Ronimo , which every programmer and trainee should learn on the first day of work. Our
methodology paper explains the workflow, and
the style guide talks about our code system. First, I want to reveal the methodology and talk about the reasons behind the rules presented in the document.
In our two-dimensional MOBA Awesomenauts, there are currently two thousand classes and more than 400 thousand lines of code. To prevent such a voluminous code base from becoming chaos, a structural approach is needed.It should be noted that the content of this document is not very original: for the most part it is a combination of Agile practices I liked.
Let's start by examining the methodology paper itself:
')
Ronimo code methodology
The standard way to implement a new feature:
- Analyze what needs to be done. Discuss the approach with end users (usually artists and / or designers) and with the lead developer.
- Divide the work into small tasks that can be solved a maximum of a day.
- Create a simple plan for small tasks. Implementation of the core of the functional and the most difficult parts put in the beginning of the plan
- Implement each of the smaller tasks.
- Rate results with end users and lead developer.
- Explain to the end user what the new function is doing to actually use it.
Implementation of the petty task :
- Analyze what needs to be done.
- Make sure that there is no error in the existing code (run the tests!).
- Explore and experiment with all the new technologies needed for this feature.
- Create a new feature code design. Do not overly emphasize theoretical use in the future, but keep it in mind.
- Refactor existing code to create space for a new function.
- Run tests to verify that the old functions still work.
- Implement a new feature.
- Test the work of the new function.
- Finish the code: add comments, clean the code and check the destructors.
- Check with the tests that the new feature is still working.
- Test the work of the old functions.
- Analyze the result. If possible, show the intermediate result to the end user.
Other rules:
- Keep a personal list of all the small tasks that need to be done. If a problem arises that cannot be solved immediately, or when you are asked to add a function that you promise to add later, write it in the list. Do not think that you can remember everything.
- Do not continue to work on the task if there is a failure or a serious error. Always correct the failure first.
- “Premature optimization is the root of all evil” (Donald Knut). First, implement a new function in the simplest or cleanest possible way, analyze whether it works, then analyze the execution speed, and only make optimization if necessary.
- Don't worry about adding unknown future extensions to your code. If necessary, the code can be refactored. Of course, if you do not need a lot of work for this, then do everything as general as possible.
Most of the principles are clear enough, but it is interesting to discuss the reasons behind these rules. Quite often I saw intern coders with a bunch of unfinished parts in the code, because they worked on five tasks at the same time and forgot to test, clean and complete some of them. Therefore, our code methodology requires programmers to complete tasks, and only then move on to the following. That is why I demand that large tasks be divided into several smaller ones: a person’s memory is limited, and the more tasks you perform at the same time, the more likely you are to miss something important.
At the same time, I prefer the agile approach: do only what you really need and expand the code base in the process. At the beginning of development, you do not know all the functions that you will need, nor do you know how to solve all the problems. However, the gradual addition of functionality often inflates the code and blurs its meaning, creates vague responsibilities of classes. Therefore, the code needs to be refactored frequently. Refactoring requires discipline. Often, you can write a hack of a new function in just a few hours, or first spend half a day on refactoring so that this function can be successfully integrated into the code. It is very easy to skip or postpone refactoring until later, but in this case, very often in the long run, such code becomes inoperative. This is why refactoring is clearly mentioned in our methodology.
But do not take the call to work only on one small task too literally. Although I think that it is important not to do anything else until you finish and make the code clean, this does not mean that you should not look ahead. When creating something complex, it is important to think about how you make the entire system work. Once I had a trainee who had to redo a large part of the tool, because he took the code methodology too literally and did not think at all beforehand. The most complex functions of the tool were completely impossible to implement with what he created. The most important thing here is the right balance between thinking about the future and focusing on one task.
When creating a tool that should quickly process a tree of 100 thousand settings, you need to think about the execution speed from the very beginning, although our methodology says the opposite. Rules should not be taken too literally.Another important aspect for me is the direct communication of programmers with designers and artists. We believe that detailed design documents rarely come in handy, so the only way to know for sure is to speak with a designer or artist who needs a new function. Often this person can not decide on the exact requirements for the function, so the coder needs to communicate with him, find out all the features from a design and artistic point of view. If a programmer has some kind of design and artistic experience, it helps a lot in communication, but even if there is no such experience, I think that the programmer’s task is to speak the language of the artist or designer, but not vice versa. It is very difficult for a designer to talk about code, and a programmer can tell about his work in accessible English (or Dutch, in our case).
Our code methodology has surprisingly no unit testing. We very strictly adhere to thorough testing of our own code, but the document says nothing about writing unit tests. The reason is that I consider the gameplay too chaotic and unpredictable, so that it can be sufficiently well captured by unit tests. Some functions can be checked by unit tests, but errors we encounter are often not detected by them. Often these are fragments that work fine, but lead to the wrong gameplay.
I realize that the lack of unit tests makes us more vulnerable to errors than developers who write unit tests. That is why we emphasize that when a failure or a serious bug is detected, it must be corrected immediately. We may have more bugs than software developers writing detailed unit tests, but at least we fix them quickly. I think we should use unit tests more for such aspects. as server architecture. Unit testing is not inherent in our company, but perhaps it should be used at least a little.
Regardless of whether or not you agree with the rules of our methodology, I believe that it is important for all programmers to think about their workflow. It is not enough just to do what seems right to you. Discipline and structure are important for all who work on large and complex systems. What is your code methodology? If you work for a company, does it have a similar official document?
So here it is - the Ronimo code methodology! Below I will talk about the code style guide, which is a little stricter than usual for most programmers.
Code Style Guide
Now we look at how our code looks, as described in
the code style guide . The main idea of ​​the style guide is that if all the code is formatted in a general style, then the code of your colleagues is easier to read and change. For example, getting used to another way of placing curly brackets or naming takes time. Thanks to the strict style guide that all Ronimo programmers must follow, this can be avoided.
In a few years, 18 programmers (including interns) contributed to the Awesomenauts code base. Due to strict rules of design, all code is read in the same wayI have not seen so many manuals on the style of other companies, but from what I heard, I realized that our management is much stricter than usual. I'm not sure if this is true, but I can believe it, because I am known for my meticulousness (sometimes too strong). However, our leadership is not "immortalized in stone": every rule has exceptions. If, in some situation, the style guide does not make sense, the coder may well ignore it sometimes. But only if this is a good reason.
Some selected solutions in this document are rather arbitrary. Sometimes the alternatives are just as good, but without a clearly selected option, it is impossible to achieve the same formatting for all programmers. This is especially true for braces. I know that this is a hot topic, and although I have clear preferences, I know that good arguments can be made in favor of alternative styles of arrangement. (And it would be nice if the defenders of any other popular style did not call him the Only True ...;))
The most important element of our style guide is that I want to read the code as much as possible in English. The names of variables and functions must be descriptive, only the most common abbreviations are allowed. I am concerned not with brevity, but readability.
However, not all points of our style guide relate to formatting. Part describes language constructs. C ++ is a rich language with enormous possibilities, but some of them are too confusing or represent the danger of errors. For example, in C ++ it is quite possible to use nested ternary operators, but the result is rarely convenient to read, so we completely abandoned them.
This relatively simple example of a nested ternary operator is already too difficult to read. Our style guide prefers readability.In addition, our guide contains rules for simplifying cross-platform development. On consoles, it is usually impossible to choose a compiler, so you have to work with the fact that you chose Nintendo, Sony or Microsoft, taking into account the limitations of their compilers. We studied what features of C ++ are supported by all these compilers and have banned the use of some new C ++ constructs that, in our opinion, may not work on one of the consoles. Since we are not currently engaged in development for some consoles, we draw conclusions only on the documentation, but it is better to be too strict than too lenient.
In the style guide, you can also notice my dislike for complex language constructs. C ++ allows you to perform quite impressive tricks, especially with the use of templates and macros. Of course, I agree that sometimes such tricks are very useful, but in general I reject them when it becomes too difficult to read them. In rare cases, when they are really necessary, they are allowed, but usually I prefer to avoid complex structures.
An example of what a C ++ header looks like, written according to the rules of our style guide.One of the most hotly discussed items in the style of writing code is whether to mark variables of class elements. If the Car class has
float speed
, do we need to call it
speed
,
mSpeed
,
_speed
or something else? I decided to call it just
speed
. Here again I adhere to the fact that the code should resemble the English text as much as possible. The more prefixes and underscores, the farther we go from natural language and the harder it is to just read the code and understand it.
However, there is a logical reason why many programmers mark variables of class elements: in code, it is very important to know whether a variable is an element of a class, a parameter of a function, or a local variable. This point of view is also valid, but I think we solved this problem differently: in our style guide there are restrictions on the length of classes and functions. If the function is short and fits on one screen, then it’s very easy to immediately see where the variables come from. I believe that if classes and functions are short enough, then labels of variable elements are not really needed.
By the way, notice that the rule about the length of functions and classes in a company is violated most often. Sometimes it is very difficult to separate a class or function beautifully. In the end, the goal of our style guide is to write clean code, rather than complicating it with awkward separations. For the beautiful division of classes into smaller, more convenient, accompanied modules, real art is required. So if you are not too experienced, then most often you will not see possible beautiful splitting options. In my opinion, the ideal class size is somewhere between 200 and 400 lines, but such a strict rule cannot be fulfilled, therefore it is formulated more softly in the style manual.
We discussed the reasons for the decisions chosen in the code style guide, let's finally see what it looks like by itself!
Ronimo Code Style Guide Every rule has exceptions. However, whenever possible it is worth adhering to these rules in order to maintain a permanent system and style throughout the entire code base. In many ways, they depend on taste, therefore, maintaining a constant code structure requires you to find your own taste and adhere to these rules. If you follow the rules, it becomes easier to read the code.
When working with other languages ​​(not C ++), try to adhere as closely as possible to the C ++ code standard, but, of course, within logical limits. At the end of the guide are special notes on C #.
C ++
- All code and comments are written in British English. (Not in American English.) Correct: color, center, initialiser . Wrong: color, center, initializer .
- Each comma is followed by a space, for example:
doStuff(5, 7, 8) - Operators are separated by spaces, for example:
5 + 7 instead of 5+7 - Tabs are four spaces long. Tabs are stored as tabs, not as spaces.
- Functions and static variables are specified in the same order in the .h and .cpp files.
- Use
#pragma once instead of #include guard (we switched to this recently, so you will see many old #include guard in our code). - Try to write short functions, preferably no longer than 50 lines.
- Avoid creating too large classes. If you expect the class to grow large and it can be logically divided, then divide it. Strive to ensure that the standard class size does not exceed 750 lines. However, you should not divide the class if the code becomes less readable. Here are examples of unreadable partitions: too closely related classes,
friend classes, and complex class hierarchies. - Do not write long lines that do not fit on a standard 1920 * 1080 screen (do not forget that there is a Solution Explorer window on this screen).
- When splitting a long line into several lines, make sure the indents correspond to the brackets. For example, like this:
myReallyLongFunctionName(Vector2(bananaXPos + xOffset, bananaYPos * multiplier), explodingKiwi); - If possible, use a preliminary announcement: the header files should be as small as possible include. For example, use
class Bert; and move #include "Bert.h" to the .cpp file. - Include and preliminary declarations must have the following order:
- Start with all preliminary announcements from your own code (in alphabetical order)
- Then all include from your own code (in alphabetical order)
- Empty line
- Then for each library:
- Preliminary announcements from this library (in alphabetical order)
- All include from this library (in alphabetical order)
- The include of the .h file belonging to the .cpp file is always at the top.
- Do not define
static variables in a function. Use class element variables (possibly static). - Everything must be correct in terms of
const . - The names of variables and functions should be descriptive. You can easily use long names, you can not use vague names. Use abbreviations only if they are very clear and standard.
- The first letter of the class names, the struct and enum types is the capital letter. Variables and functions begin with an uppercase. Each next word in the title begins with a capital letter. Do not use underscores (_) in variable names. Here is an example:
class MyClass { void someFunction(); int someVariable; }; - Element variables are not flagged in any way, neither the letter m before the name, nor _ after. Functions must be short enough so that you can keep track of what is declared in the function and what is in the class. Do not mark the variables of the elements
this-> . - Implementations of functions should never be in .h files.
- Template functions that cannot be implemented in a .cpp file are implemented in an additional header file, which is included from the main header file. Such a class can have three files: MyClass.h, MyClassImplementation.h and MyClass.cpp. For example:
class MyClass { template <typename T> void doStuff(T thingy); }; #include "MyClassImplementation.h" - Start the template type names with the letter T. If you need more information, then you can add words after it, for example
TString . - You cannot declare multiple classes in a single header file. Exception: the file is part of another class (for example, declared inside another class).
- There must be two blank lines between the functions (in the .cpp file).
- Use blank lines to structure and group code for readability.
- Accompany the code with detailed comments.
- Above each class give a short description of the class. In particular, it is worth describing relationships (for example, “This class helps class X by doing Y for it”).
- The curly brackets
{ and } always occupy a separate line, that is, they cannot be placed on a single line with if or for . Also, they can never miss them. The only exception: several similar single-line if constructions, going one under the other. In this case, it is allowed to put them in one line. As in this example:
if ( banana && kiwi && length > 5) return cow; else if ( banana && !kiwi && length > 9) return pig; else if (!banana && !kiwi && length < 7) return duck; else return dragon; - When writing the
do-while while function, it should be in the same line with a closing bracket:
do { blabla; } while (bleble); - Indents in
switch constructions should be arranged as follows:
switch (giraffeCount) { case 1: text = "one giraffe"; break; case 2: text = "two giraffes"; break; case 3: - Function parameters must have the same name in the .h and .cpp files.
- If the function parameter and the variable of the class element have the same name, then you need to either come up with another name, or add
_ at the end of the function parameter. For example, like this:
void setHealth(float health_) { health = health_; } - The number of preprocessor instructions (everything starting with #) should be minimal, of course, except for
#include and #pragma once . - Do not write macros.
- Variables within functions are declared where they are needed, not all of them need to be declared at the beginning of a function.
- In the body of constructors, it is better to use initialization lists (initialiser list), rather than setting variables. Each initialization in the initialization list should occupy a separate line. Make sure the variables in the initialization list are in the same order as in the class definition in the .h file.
- Do not use exceptions (unless you use a library that requires them).
- Do not use dynamic data type identification (RTTI) (that is, do not use
dynamic_cast ). RTTI slows down a bit, but more importantly, RTTI is almost always a sign of poor object-oriented design. - Use
reinterpret_cast and const_cast only when absolutely necessary. - Do not commit code that does not compile without errors or warnings (and do not turn off warning / error messages).
- Do not commit commits breaking existing functionality.
- No global variables. Use variable elements of the
static type instead. - Instead of
std::abs use our own function MathTools::abs . Reason: implementation of std::abs is different on different platforms, which leads to hard-to-find errors. - Always use namespaces explicitly. Do not insert into the code anything like
using namespace std . - Don't even think about using
go-to designs. We have a thought recognizer and in such cases it beats the programmer with current. - Do not use commas as delimiters. Example:
if (i += 7, i < 10) - Do not use union unions.
- Do not use function pointers unless required by other libraries (for example,
sort from STL). - Use ternary operators only in the simplest cases. Never use nested ternary operators. Example of permitted simple use:
print(i > 5 ? "big" : "small"); - The counters used by the artist or designer must begin with 0, as in ordinary arrays in code. Some old tools may still start at 1, but all new tools developed for artists start at 0.
- When checking for the existence of a pointer, perform this operation explicitly. Therefore, use
if (myPointer != nullptr) , and not if (myPointer) . - If possible, use RAII (Resource Acquisition Is Initialization). That is, instead of creating a separate
initialise() function, completely initialize the class in its constructor so that it is never in an incomplete state. - Write the constructor and destructor at the same time: write each corresponding
delete for each new , so as not to forget about them later. - If you are writing a temporary debugging code, then add a comment with
QQQ to it. Never commit with QQQ : delete all debugging code before committing. - If you want to leave a note on what needs to be done later, use
QQToDo . If you need to do this, add your letter to the comment, for example, QQToDoJ . Make commits with QQToDo only if it is impractical to shut down now. - Class definitions begin with all functions, then all variables follow. The order should be as follows:
public/protected/private . This can sometimes mean that you need to use the same public/protected/private keywords in the same header file several times (first for functions, then for variables). - The class begins with its constructors, followed by the destructor. If they are
private or protected , they should still be at the top. - If something has two options, but not exactly
true/false , then it is better to use the class enum , rather than boolean . For example, to indicate the direction, do not use bool isRight . Instead, use enum Direction with the values Left and Right . - Instead of
std::string and std::stringstream use our own RString , RStringstream , WString and WStringstream (they are tied to our own memory management system). - The variable
float time always means the time after the last frame in seconds. If time means something else, then express it explicitly, for example, by giving the name timeExisting . - Make sure that all code is independent of the frame rate, so actively use the
float time variable for this. - Make the structures explicit and understandable. For example, avoid this:
if (yellow) { return banana; } return kiwi; This means that, depending on the yellow, banana or kiwi is returned. More readable code will be, if you express it explicitly:
if (yellow) { return banana; } else { return kiwi; } - Use
nullptr , not NULL . - We use
auto only for cases such as complex iterator types. For everything else, we specify the type explicitly. - Use where applicable, range-based for, for example:
for (const Banana& ultimateFruit : myList) (Note that when you do not work with pointers, it is important to type this link, because otherwise Banana will be copied in this case.) - As applicable, use
override and final . - If the function is virtual, then the
virtual keyword should always be added to it, not only in the parent class, but also in each of the versions of the functions in the descendant classes. - Use
enum with strong typing, the same with the word class . The register in the names is the same as in the classes. For example:
enum class Fruit { Banana, Kiwi, ApplePie }; - Use rvalue references (
&& ), if absolutely necessary. - If possible, use
unique_ptr . If the object does not have an owner, then it is stored as a regular pointer. - Avoid instantiating complex types in the initialization list. In the initialization list, simple copying and assignment is used, and more complex code, for example, the call
new , should be in the body of the constructor. Here is an example of how this works:
FruitManager::FruitManager(Kiwi* notMyFruit): notMyFruit(notMyFruit) { bestFruitOwnedHere.reset(new Banana()); } - Use
shared_ptr only in cases where common ownership is really necessary. In general, try to avoid common ownership and use unique_ptr . - Lambda functions that are too long to be written in one line should have the following indents:
auto it = std::find_if(myVec.begin(), myVec.end(), [id = 42] (const Element& e) { return e.id() == id; }); - Do not use
MyConstructor = delete (this does not seem to be supported in some of the compilers we use). - Do not use the C ++ 11 features to initialize lists (it seems that this is not supported in some of the compilers we use). Therefore, we do not use this:
std::vector<int> thingy = {1, 2, 3}; - Do not use features added in C ++ 14.
C #- Variables should be at the beginning of the file, not at the bottom. We strictly separate functions and variables (as in C ++), so all variables are listed first, then all functions.
- Names
async -functions must always end with Async , for example eatBananaAsync .
|
That's all, this is our code style guide! I think you may disagree with some of the rules, but it’s still useful for any company to create their own style guide. Our leadership can be a good basis for creating your own leadership. I'm curious: what code style guide is used in your company and do you like it? And is it at all?