The new year for game developers began with a wave of criticism that struck the C ++ Standardization Committee after the publication of Aras Prankevichus “Complaints about modern C ++” . There was a serious question: did the standards committee really lose touch with reality, or is it the other way around, and these game developers have split off from the rest of the C ++ community?
We offer you a translation of Ben Dean's popular post , a veteran of the gaming industry who has worked for a long time at Blizzard, Electronic Arts and Bullfrog as a C ++ developer and team leader, in which he responds to criticism from the perspective of his own experience.TL; DR: The C ++ Standardization Committee does not have a hidden goal to ignore the needs of game developers, and “modern” C ++ is not going to become an “non-debugable” language.
Throughout last week
, Twitter was
actively debated , during which many programmers - especially those who work in the field of game development - said that the current vector of development of "modern C ++"
does not meet their needs . In particular, from the standpoint of a regular game developer, it looks as if the debugging performance in the language is ignored, and code optimization becomes expected and necessary.
Due to the fact that in 2019 I had been working in the gaming industry for more than 23 years, I have my own opinion based on observations on this topic in relation to game development, which I would like to share. Is "debugging" important for game developers and why? What are the issues related to it?
')
For a start - a small excursion into history.
Many game developers who write in C ++ work in Microsoft Visual C ++. Historically, around the Microsoft platforms, a huge market for games has formed, and this is reflected in the typical experience of the average game programmer. In the 90s and 2000s, most games were written in the light of these circumstances. Even with the advent of consoles from other manufacturers and the increasing popularity of mobile gaming, the wealth of many AAA studios and numerous game programmers today are tools made by Microsoft.
Visual Studio is arguably the best debugger for C ++ in the world. And most of all, Visual Studio really stands out in terms of debugging programs — more than with its front-end, back-end, STL implementation, or anything else. In the past five years, Microsoft has made great strides in developing C ++ development tools, but even before these achievements, the debugger in Visual Studio has always been very cool. So when you are developing on a Windows PC, you have a world-class debugger at hand.
Considering the above, let's consider the process of obtaining a code in which there will be no bugs; the possibilities we have from the point of view of a programmer who does not play games; and the limitations that game developers face. If we rephrase the main argument in favor of the “vector of development of modern C ++”, then it will be reduced to types, tools and tests. Following this thought, the debugger should be the
last line of defense . Before we reach it, we have the following options.
Option # 1: Types
We can use as much typing as needed to eliminate whole classes of bugs during compilation. Strong typing is, without a doubt, an opportunity that the recent evolution of C ++ has given us; for example, starting with C ++ 11, we managed to get:
- significant expansion of
type traits
; - innovations such as
nullptr
and scoped enum
to combat the legacy of C - weak typing; - GSL and auxiliary tools;
- concepts in C ++ 20.
Some of you may not like template metaprogramming; others may not like the way they write code, in which
auto
used almost everywhere. Regardless of these preferences, the main motive for using the listed styles in C ++ is clearly visible here - this is the desire to help the compiler so that he, in turn, can help us, using what he knows best: a type system.
If we talk about game programming, strong typing here is a wide field for research, and it is actively used by game programmers I know who are interested in improving their C ++ skills in practice. There are two important things of concern here: the effect on compile time, and the effect on code readability.
Frankly, you can easily ignore the compilation time - but only if you are a programmer in a very large company that does not play games and has a well-established internal infrastructure and endless computing power to compile any code you can write . Such huge companies are concerned about the cost of compilation - therefore they use modules - but, as a rule, this does not cause pain to individual developers. At the same time, for most game programmers, this is not at all the case. Indie developers don't have farms to build builds; AAA games developers often use something like
Incredibuild , but given the fact that they can easily work with a code base that has turned 10 years or more, the build process can still take 15-20 minutes.
We can argue about the relative cost of adding “hardware” versus the time cost of a programmer, and I agree with the position that hardware costs less, however:
- The hardware is the real one-time expenses that will be borne by the budget of the current quarter, as opposed to not so tangible expenses in time / hiring / and the like, which will be distributed over a longer period of time. People do not cope well with the decision in favor of such a compromise, and companies are specially built to optimize short-term profit.
- Infrastructure requires support, and almost no one goes into the gaming industry in order to become a release engineer. Compared to other areas where C ++ is used, the salary of game developers is not so high - and non-game engineers are paid even less.
You can also speculate on the fact that the compile time should never have reached such a state; and again I agree with you. The price of this is in constant vigilance - outgoing, again, from the release engineer - and, ideally, some automated tool that allows you to track changes over time required to build the build. Fortunately, due to the emergence of CI-systems, this goal today can be achieved much easier.
Option # 2: Tools
We must use the maximum of tools available to us — warnings, static analysis, sanitizers, dynamic analysis tools, profilers, and others.
My experience says that game developers use these tools where it is possible, but here the industry as a whole has several problems:
- These tools tend to work better on non-Microsoft platforms — and, as mentioned earlier, this is not a typical scenario in game development.
- Most of these tools are designed to work with “standard” C ++. Out-of-the-box, they support
std::vector
, but not my self- CStaticVector
class CStaticVector
from a hypothetical engine. Of course, blaming the tools for this is useless, but it is still one of the barriers to their use that developers have to overcome. - Creating and maintaining a CI chain that will launch all of these tools requires the presence of release engineers — and, as mentioned earlier, hiring people for engineering jobs that are not directly related to games is a systemic problem for the gaming industry.
So, since these tools work so well with standard C ++, then why don't game developers use STL?
How to start the answer to this question? Perhaps, from the next excursion into the history of game development:
- Until the early 90s, we did not trust the C compilers, so we wrote games in assembly language.
- From the beginning to the mid-90s, we began to trust the C compilers, but we still did not trust the C ++ compilers. Our code was C, which used C ++ style comments, and we no longer needed to write typedefs for our structures all the time.
- Around 2000, the C ++ revolution took place in the game development world. It was the era of design patterns and large class hierarchies . At that time, support for STL on consoles left much to be desired, and the world was then ruled by consoles. On PS2, we are forever stuck with GCC 2.95.
- Around 2010, two more revolutions are being undertaken. The pain of using large class hierarchies has stimulated the development of a component code approach. This change continues its evolution today in the form of Entity-Component-System architectures. Hand in hand with this was the second revolution - an attempt to take advantage of multiprocessor architectures.
In the course of these paradigm shifts, the gaming development platforms themselves were constantly changing, and moreover they changed seriously. Segmented memory has given way to a flat address space. Platforms have become multiprocessor, symmetric and not very. Game developers, accustomed to working with Intel architectures, had to get used to MIPS (Playstation), then to a special “hardware” with heterogeneous CPU (PS2), then to PowerPC (XBox 360), then to even greater heterogeneity (PS3) ... Each New platform came new performance for processors, memory and disks. If you wanted to achieve optimal performance, then you had to rewrite your old code, a lot and often. I’m not even going to mention how much the games were influenced by the emergence and growth of the Internet, as well as the limitations that platform holders put on developers.
Historically, STL implementations on gaming platforms have been unsatisfactory. It is not a secret that STL-containers are poorly suited for gaming. If you press the game developer to the wall, then perhaps he admits that
std::string
is quite OK, and
std::vector
is a reasonable default option. But all the containers contained in the STL have a problem of controlling allocation and initialization. In many games, you have to worry about the limitation of memory for various tasks - and for those objects, the memory for which you will most likely have to allocate dynamically during gameplay,
slab or
arena allocators are often used.
Amortized constant time is not a good enough result, since allocation is potentially one of the most “expensive” things that can happen during program execution, and I don’t want to miss a frame just because it happened when I didn’t expect it . I, as a game developer, have to manage my memory requirements in advance.
A similar story is obtained for other dependencies in general. Game developers want to know what takes each processor cycle, where and when and for what each byte of memory is responsible, as well as where and when each execution thread is controlled. Until recently, Microsoft's compilers changed ABI with every update - so if you had a lot of dependencies, then rebuilding all of them could be a painful process. Game developers usually prefer small dependencies that are easily integrated, do just one thing and do it well — preferably with a C-style API — and are used in many companies, are in the public domain or have a free license that is not requires indication of the author.
SQLite and
zlib are good examples of what game developers prefer to use.
In addition, the C ++ games industry has a rich history of patients with the “Not invented here” syndrome. This is to be expected from an industry that began with lonely enthusiasts who were making something of their own on completely new equipment and had no other options. The gaming industry, among other things, is the only one where programmers appear in captions in no particular order.
Writing a variety of things is fun, and it helps your career! It is much better to build something of your own than to buy ready-made! And since we are so worried about performance, we can tailor our solution so that it is appropriate for our project - instead of taking a generalized solution that wastes resources available. Dislike Boost is a prime example of how such thinking manifests itself in game development. I worked on projects that went the following way:
- To begin with, to solve this or that problem, we connect the library from Boost to the project.
- Everything works very well. When you need to upgrade, there is a little pain, but no more than when updating any other dependencies.
- Another game wants to use our code, but the stumbling block is that we use Boost - despite the fact that our experience with using Boost was completely normal.
- We remove the code using Boost, but now we are faced with a new problem: we have to solve a problem that was previously solved instead of our library from Boost.
- We essentially copy the parts of the Boost code we need into our own namespaces.
- Later, we inevitably and again and again encounter the fact that we need additional functionality, which would already be in the original code, if we had not thrown it out. But now we ourselves are the owners of this code, so we have to continue supporting it.
We don’t like something huge trying to do too many things at the same time or that can affect the compile time — and that’s quite reasonable. What people make mistakes over and over again is that they are opposed to accepting the supposed pain today - while because of this decision they are faced with a very real and much greater pain, supported by something the budget they will have to experience over the next three years. Alas, the existence of evidence in the form of games that successfully use a dish from STL and Boost, in no way can affect the psychology of the person and persuade game developers.
For all these reasons, many gaming companies have created their own libraries that cover what STL does — and more — and still support game-specific use cases. Some large gaming companies were even able to master the development of their own, fully-fledged, almost completely compatible with the
STL replacement API, which later entailed the huge costs of supporting this project.
It is reasonable to find an improved alternative to std::map
, or apply
small buffer optimization to
std::vector
. It is much less acceptable to be doomed to support your own implementation of
algorithms
or
type traits
, which will bring almost no benefit. As for me, it is regrettable that STL for most developers is only containers. Since learning STL at the start is taught by them, speaking of STL most implies
std::vector
- although in fact they should think about
std::find_if
.
Option number 3: Tests
It is argued that extensive testing should be carried out, TDD and / or BDD should cover all the code that can be covered, and bugs should be fought by writing new tests.
So let's discuss the topic of testing.
Judging by my experience, automated testing in the gaming industry is almost never used. Why?
1. Because correctness is not so important, and there is no real specification.
As a young programmer in the gaming industry, I quickly got rid of the thought that I should strive to model something realistically. Games are the
smoke and mirrors and the search for short confuses. No one cares how realistic your simulation is; The main thing is that it is fascinating. When you have no other specification than “the game should be felt right,” there is no test item itself. Thanks to the bugs, the gameplay can even get better. Quite often, the bug gets into the release, and even wins the love of users (
remember the same Gandy from Civilization ). Games are different from other areas in which C ++ is used; here the lack of correctness does not lead to the fact that someone as a result loses their savings.
2. Because it's hard
Of course, you would like to do automated tests wherever you can. This can be implemented for some subsystems for which there are clearly defined outcomes. Unit testing in the gaming industry is, of course, present, but is usually limited to low-level code — the previously mentioned STL analogs, string conversion procedures, methods of the physics engine, etc. Those cases where the executable part of the code has predictable results are usually tested by unit tests, although TDD is not used here - because game programmers prefer to simplify their lives, and not vice versa. But how do you test the gameplay code (see point one)? As soon as you go beyond unit testing, you immediately encounter another reason why testing games is so difficult.
3. Because content is involved in it.
Testing of non-trivial systems may include the provision of content, with the participation of which it will be carried out. Most engineers are not very good at making this content on their own, so you need to involve someone with the right content creation skills to get a meaningful test. After that, you will encounter the problem of measuring what you get at the output - after all, it is no longer a string or number, but an image on the screen or sound that changes over time.
4. Because we do not practice it.
Unit testing is a function for which I know the possible inputs and outputs. However, gameplay is an unpredictable, dynamically developing behavior, and I don’t know how such a phenomenon could be properly tested. What I can test is - if, of course, I get permission from my manager to devote enough time to it - this is, for example, performance, or high-level matchmaking capabilities, which I can analyze. Such infrastructure work can be fascinating for some game programmers, but most of them are simply uninteresting - and, in addition, require the approval and support of the wallet owner. In the role of a game programmer, I never have the opportunity to practice writing high-level tests.
5. Because [the company] sees no need for automated testing.
Our main goal is to release the game. We live in a time of an industry that moves forward with hits that earn most of their money in the first month of sales, when the marketing costs of these hits are at their maximum. The life cycle of consoles taught us that the code will not live in such a long time either. If we are working on an online game, then most likely we will get additional time to test matchmaking or server load. Because for the release of the game, we need its performance to be in order, we should at least do performance testing, but we should not automate this process. For management in the gaming industry, automated testing is nothing more than a waste of time and money. For it, you have to hire experienced engineers who will produce work, the result of which will be almost imperceptible. The same time could be spent on developing new features. In the short term, it is much more profitable to use QA personnel to test the game, which brings us to the next point.
6. Because in general, testing refers to second-rate activities in games.
I adore good QA specialists. For me they are worth their weight in gold. They know how to make your game better, breaking it in a way that would never occur to you. They are profile experts in your gameplay in the sense that you do not understand, and hardly ever will understand. They are better than a team of super-capable compilers that help you do everything right. I am glad that I had the chance to work with several wonderful QA specialists over the years of my work.
I almost always had to fight only to keep them on my team.
AAA-, , QA — , . , . , .
, , , . «» , , QA , , , .
. , . QA-, , API , « ». , , .
. , , , — .
. . , , « .» « Y.». QA- — , , .
, , ; , — , , — QA , , , , QA .
, , , QA-, . . . , , .
— , API , , ( ) — , .
, , C++.
, . , , , , , . , - , , .
, , , , . , , — , . , (
data breakpoints ) , , — , , ? , , , , , , , , , (
soak testing )?
. , . , ; ; ; ; ; , ; , , .
, «», . , , , — . , — , . - , .
« ++» , . ; , ; , . « C++» , — , , STL _ _, STL . , STL « », ; , ,
, .
, , « C++» — , .
— , .
, C++ , . , . , .
copy elision ( ) , . . , , NRVO , , . , C++
.
: ++
, C++, .
1.
, C++, , . , . , , — .
, . C++98, , - , .
, , , . , C++-, «» C++. — , C C++98. , , , – , . ?
2.
, GDC,
CppCon , , . ;
;
. , , — , .
C++ . , SG14, SG7, SG15 — , —
isocpp.org . — , , 200 ? «» «» .
, , , , Twitter Reddit. , — .