It’s always great to be able to study the source code for successful, released, real games.
I will note that this is actually the source code of the C++ rewrite! The original game was written in Flash, and this version was apparently written by Simon Roth.
My impressions: The source code is a bit of a tangle here. There are magic numbers (https://wiki.c2.com/?MagicNumber) all over the place, plenty of god classes (https://wiki.c2.com/?GodClass), and in general you will have to do a bit too much archaeology to figure out what a particular class or member function is supposed to do (the function and class names don’t give you enough, and there are no comments to help out). There are a few WTFs in here, like the "int i, j, j;" in Script.h.
THAT SAID, This is not really out of the ordinary for successful, released, real games. It’s not really different in style from the Celeste Player code.
But if you’re writing a game of this size or complexity, I recommend doing some refactoring work along the way. In my experience, this will reduce the amount of insanity in the project towards the second half of the development cycle.
But I am always more impressed by ugly code behind a good game than I am by clean code behind an unreleased, unfinished, or unfun game.
> The source code is a bit of a tangle here. There are magic numbers (https://wiki.c2.com/?MagicNumber) all over the place, plenty of god classes (https://wiki.c2.com/?GodClass), and in general you will have to do a bit too much archaeology to figure out what a particular class or member function is supposed to do (the function and class names don’t give you enough, and there are no comments to help out). There are a few WTFs in here, like the "int i, j, j;" in Script.h.\
> This is not really out of the ordinary for successful, released, real games.
Maybe these supposed anti-patterns don't really matter as much as we think they do for successful software? Given that plenty of successful software has them.
The key insight is that most games are released largely once, and then only minor work is done after that. If it's really successful it may get additional ports or content expansions. It does not make sense to pour so much time into making code reusable and clean if it is not going to be built on continuously for long periods of time. Additionally, games do not have to deal with security issues as much; most code is running on trusted data sans networking features. Bad code is 100% counter to security - it's those edge cases where assumptions fail and you lose big time.
As a counterpoint for example, code running on servers should totally be built for the long haul: it's gotta evolve continuously with requirements, it has to cope with edge conditions and especially it has to be secure. You can't get away with the same type of spaghetti in your ACL checking that you can in the code that handles player physics in a platformer.
That does not mean video game devs are bad coders. They are just optimizing for different scenarios.
Where this style applies the most is with hardcoded assets: initializations and runtime behaviors that require an unknown degree of flexibility.
When someone is trained in OOP, the requirement "flexible behavior" triggers a search for answers through polymorphism, but polymorphism is much more structured than a copy-paste-modify code path. It creates limits to scope and requires more explicit names for things. That is the point, of course. You have write more code and do more complex things to get the same level of flexibility, and it'll be harder to debug since the call stack will have to jump around throughout the layers of abstraction. In a "time from idea to production code" latency analysis, OOP structures lose to copy-paste.
And that's why, across the bulk of game codebases I've seen, there tends to be a big jump from hardcoded assets straight into data-driven ECS-style approaches. Small games start with the former, and if they get big enough switch to the latter. The ECS approach still exacts a debugging penalty because more bugs will exist in data, where they're harder to analyze with IDE tooling, but more behaviors are encoded as explicit patterns with limited scope, which is good in a team environment, and it's possible to go data-driven in an incremental rewrite: simply find all the truly redundant stuff, replace it with a parameterized pattern, expose the pattern as data. For the stuff that isn't easily parameterized, consider encoding a small state machine interpreter so that an imperative program definition may occur in data. In the entity system, add some notion of dynamic compositions of state and behavior at initialization. Between those three you can cover just about everything, and you never have to do it 100% to ship: it's there to assist the things that benefit from additional structure, which probably isn't all of the game. (rather, in the AAA environment the data-driven approach can tend to get out of hand - it's a move that allows the designers to avoid needing explicit code support for longer and longer periods of time, with the predictable result of enterprise anti-patterns that abuse scripting in a fashion that is much harder to debug than the hardcoded equivalent.)
This distinction is also basically why you don't see Jon Blow, for example, really get excited about discussing the runtime architecture of his game projects: If the bulk of the game is assets all the way down, there's nothing to talk about, and the only part that concievably could be exciting is some of the core algorithms that drive the interactivity.
It's also why so many gamedevs are apologetic about their code: half of them try to escape this reality by finding a magic mix of abstractions(which eventually blows up and causes rewrites once sufficient complex behavior hits the codebase), the other half run with it and stay in the local optimum of inlined, hardcoded, primitive-heavy functions.
My experience 100% mirrors the code vs data trade off. The games I work on are all data driven from the start, but are also never could have been made hardcoded in the firs tplace.
Game development mostly relies on getting creative stuff implemented fast, not good. And if it works then it works, why spend any more time on it if you can implement even more cool ideas (or release it).
And Minecraft is one of the best examples for a game that is still paying for such a development style.
Game code is usually specifically built to be useful for a very short amount of time, and is frequently the definition of "one and done". Examples against are long lived games like world of warcraft, engines like infinity, unreal, naughty dog etc, which must support extension over time.
A game like VVVVVV is, once a class is created, unlikely to be changed a thousand times. It wouldn't scale if you had 200 people working on it with new people coming in and leaving all the time. But at the end it doesn't really matter much in this case, and isn't the right thing to focus on while coding up a game.
> Maybe these supposed anti-patterns don't really matter as much as we think they do for successful software
They matter a lot for some software more than others. All software is a set of tradeoffs in some ways or another - in this case its attention to things like perfect class naming.
They may not matter as much for this particular scenario. Most of the conclusions I would draw wouldn't extend past independent projects with a small close knit team, or an individual project.
Right now, I'm working on a piece of personal productivity software and trying ideas and patterns that I couldn't get away with in building software with others and to the requirements of other people. I can get away with things that are traditionally anti-patterns because I'm the only one it has to pattern to.
I have to imagine indie game dev is a similar sort of getaway.
I've read that before but on this re-reading it really hit me that he was describing 'go fast and break things' more or less with the so called New Jersey approach.
"It is better to get half of the right thing available so that it spreads like a virus. Once people are hooked on it, take the time to improve it to 90% of the right thing."
Is owner of the server playing some tricks, when I tried that URL on Firefox Focus on my phone, it is just the text and I believe what parent wanted to submit, but indeed it looks fishy, I got 403 when tried to see what the redirect was using curl.
This is one reason I love having changed network.http.referer.XOriginPolicy[1] in Firefox. I never send the referer to third party domains, and I've only ever seen one or two sites broken because of this. It's a vast improvement for privacy as well.
> Maybe these supposed anti-patterns don't really matter as much as we think they do for successful software? Given that plenty of successful software has them.
There's more than one variable involved. You can write successful software full of anti-patterns, but you'll probably pay for it in some other way (like increased QA effort or a low bus-factor [1]).
The motivations around those kinds of factors are different for games, though.
e.g., bus factor has to be weighted a lot more highly on software that needs to be maintained indefinitely than it does on software that is going to be shipped on a fast-approaching deadline and then (probably) never touched again.
Also, bus factor's importance depends on the size of the team. In the limit case - a 1-developer project like VVVVVV - it's meaningless.
It’s not meaningless when you’re spending exponentially more time debugging random bugs due to lack of architecture (which usually implies you can’t properly test anything, too).
Doesn't 'success' encompass all these variables at the same time?
If you can be successful with a very high QA effort then fine. If QA costs were too high then you wouldn't be making money and you wouldn't be successful.
If you can stay successful with a very low bus-factor then good for you.
This is a game that has gone through a few revisions by different authors. And whilst I doubt any of them were happy with the hand they were dealt it still shipped after their work. So maybe it had increased QA effort over the baseline version that doesn’t exist but it certainly had no bus factor.
its the game that is successful not the software. This game could be written many different ways. Players dont care. Now if there is a particular pain point or glitch in the game and it can be shown that its the style of code that allowed for this to happen. That better organized code would have mitigated this bug then that is important to investigate.
But trying to figure out what a successful game is by its source code is like trying to find what makes a painting good analyzing the chemicals used to make the pigments.
> This is not really out of the ordinary for successful, released, real games
Well said. A lot of people will nitpick and talk big about how they would use proper abstraction and write very clean code, but probably none of those people have ever had a successful indie game out.
To be fair, one of the most vocal advocates of higher code quality is Carmack, who has put out a few games.
What I’ve seen is that it’s hard for someone who’s not released a game before and supported it post-release to know where and how you can sacrifice code quality for a faster development time.
Right, but at the same time what Carmack considers quality code may well be considered low quality by non-game devs because it's too complex, too magical, not documented enough or whatever. Context matters; some code is one game developers beauty; while it's some other developers nightmare.
I'm not a game developer, but I've worked with enough game developers to realize they have a completely different mindset and point of view from mine. It doesn't make them wrong, and it doesn't make me wrong either, it's just that – different points of view, different contexts, different constraints. I have immense respect for game developers.
I'll play Battlefield 1 or Horizon Zero Dawn and marvel that they can get a stable 60 or 30 FPS despite seemingly infinite complexity, all the while I'm struggling to filter a grid with 250 rows in a web browser...
You're absolutely right – no one knows better where to sacrifice quality (whatever that means) for dev time or performance, than game devs.
Carmack is also generally writing foundations and engines that are reused in multiple games. If it's not readable, it's useless.
Most indie devs are trying to crank out a game fast because they have a vision and/or they just want to sell it while the market's hot. Most have no plans on touching the game beyond maybe a couple tiny absolutely essential bugfixes shortly after release. Writing cleaner code is ideal, but that takes planning, foresight, and refactoring. That's loads of time for solo devs or tiny teams to devote which could be otherwise spent making new content entirely.
Yeah but carmack style quality isn't as SOLID as people want to. Game devs have always also factor in performance, that's what other dev communities have lost. Apart from embedded devs.
I would caution people against taking SOLID as a measure of code quality. That’s a trap. SOLID is just a set of object-oriented design principles, not laws. If you take the guidelines too seriously, you will end up with a damaged code base which is difficult to read or understand.
Having spent quite some time in id Software code bases, I will defend them as being high quality, but whether or not they are SOLID is not something I really care about. The SOLID guidelines also regularly conflict with YAGNI.
> To be fair, one of the most vocal advocates of higher code quality is Carmack, who has put out a few games.
I'm working on a port of Doom right now, and have worked with the Quake 2 source code before. I also took a quick look at the Wolfenstein source code yesterday.
It's very clear that Carmack and his colleagues have developed their craft over time. Wolfenstein doesn't seem to have much structure to it at all. Doom is still a mess of global variables and ugly code. But you're starting to see some nice abstractions, like the WAD loading (which made it modder friendly) and the sound module system.
It's been a while since I worked with the Quake 2 code, but I remember it being quite elegant. From what I've heard the Doom 3 source code is excellent.
I think the point is, for your first projects, for small indie-style games, you really shouldn't worry too much. In my experience, it's really hard to learn to use good abstractions effectively unless you've tried to do things the hacky way first, and actually have a solid feeling for what the abstractions try to solve. I still notice this, that I really have trouble getting some projects off the ground if I try to do it the elegant way from the start. Sometimes you spend too much time with an abstraction that turns out to be a bad fit. If I just hack it together, and then refactor it later, it's much easier to get things started.
It's also important to remember that the more people you're collaborating with, the more important it is to have a clean structure.
I think the code bases prove it in their own right...
When I look at how many games Raven was able to churn out with the Quake 3 engine (and most of them were pretty good! Elite Force and Soldier of Fortune are both games that I remember quite fondly).
Carmack saw the value of clean code in providing a quality experience. I'd also wager that was a big part in their ability to iterate on the engine so quickly.
> Wolfenstein doesn't seem to have much structure to it at all. Doom is still a mess of global variables and ugly code.
Part of me asks how much of that was by necessity, and by that I mean whether some of those things were intended as ways to get every last bit of performance out of the code (especially things like globals, and for something as old as wolf3d one starts to also ask about things like segments shudder)
That's a weighed average with, presumably, hand-chosen coefficients to make a nice visual effect. I fail to see how these numbers are "magic." How would you produce this effect without numbers?
Ok, sure. Even so, this is about the least compelling example of "magic number" that I've seen. They're used once, the intent is clear, and you don't have to go hunting for the definition.
From a pragmatic point of view I would actually expect it to be pretty common for games to sacrifice code quality. You build a game with a set end date, and do a small amount of maintenance on it moving forward. As opposed to an engine where it's a product you're building on and you want to grow over a much larger timeframe, and those unfamiliar with a piece of code are likely to maintain it down the road.
> The original game was written in Flash, and this version was apparently written by Simon Roth
Thanks, I did not know that at all - I know VVVVVV (Linux version) and Roth rang a bell - it's the same guy that wrote (writes? the game has a very long development story) Maia - a space colony simulation/game.
> I will note that this is actually the source code of the C++ rewrite!
"The repo contains two versions – the desktop version, ported to C++ by Simon Roth in 2011, and later updated and maintained by Ethan Lee – and the mobile version, written in Actionscript for Adobe AIR, based on the original v1.0 flash version of the game."
I figured OP was referring specifically to defining j twice, which would definitely have been a WTF, but it seems in the actual source it's i,j,k. So, still weird, but better.
I will note that this is actually the source code of the C++ rewrite! The original game was written in Flash, and this version was apparently written by Simon Roth.
My impressions: The source code is a bit of a tangle here. There are magic numbers (https://wiki.c2.com/?MagicNumber) all over the place, plenty of god classes (https://wiki.c2.com/?GodClass), and in general you will have to do a bit too much archaeology to figure out what a particular class or member function is supposed to do (the function and class names don’t give you enough, and there are no comments to help out). There are a few WTFs in here, like the "int i, j, j;" in Script.h.
THAT SAID, This is not really out of the ordinary for successful, released, real games. It’s not really different in style from the Celeste Player code.
https://github.com/NoelFB/Celeste/blob/master/Source/Player/...
But if you’re writing a game of this size or complexity, I recommend doing some refactoring work along the way. In my experience, this will reduce the amount of insanity in the project towards the second half of the development cycle.
But I am always more impressed by ugly code behind a good game than I am by clean code behind an unreleased, unfinished, or unfun game.