This is a great piece of advice and one that has worked for me in the past when refactoring code like the one referenced.
Nicely put. :) Thanks for the link.
I was quite surprised at how often copy-paste-modify
operations resulted in subtle bugs that weren't
immediately obvious.
I now strongly encourage explicit loops for everything,
and hope the compiler unrolls it properly.
I found this particularly interestingAnother good example of this is the Phong shading model. IIRC it was invented in the mid-70s, and some software rasterizers implemented it, but it didn't really make its way into graphics cards with fixed pipeline rendering until the mid-90s.
Graphics tech in the 90s and even early 2000s involved a lot of hardware and software catching up to theory from the 70s and 80s because we were finally getting enough compute power to make it worthwhile to use "approximate" rasterization-based algorithms in order to get real-time rendering.
> Did you think about doing a first-person shooter after their success with Wolfenstein 3D and Doom?
> TS: It was funny how we got to that point. When I saw Wolfenstein for the first time, that was truly shocking. I'd never envisioned that you could do 3D in a computer game; I don't know why.
> The research had indicated that you could do that for at least 15 years before that, but it was this 3D game with real-time texture mapping; you know, real-time bitmaps scaled up and displayed in 3D on the screen. It never occurred to me that you could actually write code to do that. It was just another lack of foresight there.
> But seeing that for the first time, I was like, "Wow, I'm totally not worthy. I need to get out of programming now, because I'm never going to be able to compete with this." So they just basically demoralized me into becoming a manager for a few years.
> Around 1994, James Schmalz had written this 3D texture-mapping code, and I was starting to think, "Hmm, maybe that's not so hard." So I started reading up on references there and experimenting with it, and it turns out that, yeah, it's just another piece of code that you can learn how to create.
http://www.gamasutra.com/view/feature/4035/from_the_past_to_...
The algorithms themselves (like 3D texture-mapping) are college third year problem set level difficulty to actually implement, but having the curiosity, courage, and ambition to be the first to do it real-time is huge!
The algorithms used in today's 3-d game engines are definitely more complex than something I'd expect a college student to do, but the state of the art has also progressed substantially since the 90's.
The man is a legend. I remember using his little set of DOS utilities (things like stuffit.com [2]) back in the early 1990s, which I got from a friend whose dad was a colleague of his at Hydro. They were all written in assembly language, and were superb little extensions to DOS.
[1] https://www.linkedin.com/in/terje-mathisen-b740181
[2] http://files.mpoli.fi/unpacked/software/dos/utils/keyboard/s...
Needlessly increased variable scopes - and thus increased risk of harder to follow cross-dependencies - is probably the biggest downside risk. But I've found code that executes linearly easier to read and understand when it's written linearly. When everything is factored into functions - particularly impure functions - you need to click all over the place to understand the whole.
If there are higher-level primitives that can be used - filters, maps, folds, etc. - then they can reduce the verbosity overall. Some monadic styles can flatten nested code by computing a pipeline / computation rather than having lots of nested loops. But depending on the language and the problem domain, performance may suffer.
I refactor as soon as my code gets past 3 levels, I don't know how one is supposed to readily understand this block of code.
The lack of comments isn't what troubles me. Comments get old and, when they do, they get completely misleading and downright dangerous. As soon as you feel the need to add a comment, it's often because that particular chunk of code should be extracted into its own function/method/whatever and given an intelligent name. Comments should be reserved for really important stuff, like when the code does something totally non-obvious.
generally agree on commenting, but it's also about discipline - method-level commands are rarely updated, but if there is some particular algorithm within method, I don't see a reason to describe it beforehand on a line or two.
General rule - if method is longer than my screen, it's time to refactor. very rarely this isn't a good idea.
http://hg.icculus.org/icculus/lugaru/file/97b303e79826/Sourc...
Question regarding index access: There are tons of for loops with player[k] occurrences and similar. Personally I would create a local variable and prevent further index-retrievals.
Is this worthwhile or would a compiler optimize the array-index access away? Or maybe subsequent uses of the same index are cached by the cpu? Does someone know?
One reason you might want to use a separate variable would be to prevent indexing mistakes, like using i instead of k by mistake in a context where i is a nested loop's counter. But apart from this and other "to make it easier on the human" reasons, there's no point in caching that in a variable. Now, if you were writing Python on the other hand...
Grabs the eye bleach
Consder this:
char* p1, p2;
You could erroneously think that char* is a type, and p1 and p2 are variables of that type. That's not correct however, since p1 is indeed char*, but p2 is char. Such declarations therefore should be strongly avoided for clarity.[1]: https://github.com/CRYTEK-CRYENGINE/CRYENGINE/blob/release/C...
So I always do char \p1. Yes, for a non-C programmer its not as intuitive, but C isn't intuitive a lot of the time (cough, char p1[] what?). But you avoid making these mistakes altogether.
Note in function declarations I use char, because you cannot do statement expansion with a comma there, and because in headers the variable name is optional anyway.
char *p;
?I prefer to use char* p; as well as std::string& s; and etc. rather than attaching the type syntax to the name, since in essence that's part of the type. But yes, it's a mess that this inherited C logic doesn't treat it that way clearly. To avoid such mistakes, it's better not to use comma declarations altogether, you can perfectly avoid them.
It's a good feeling when working with new languages like Rust which avoid such mess.
The point is, though, that while in a developers mental model you attach ref or ptr to the type, the language itself does not, and writing it out like it does can easily drive newer developers who do not understand the nuance to make hard to recognize mistakes as a result of it.
What can be the rationale behind writing this [1] ?
[1] https://github.com/CRYTEK-CRYENGINE/CRYENGINE/blob/release/C...
I suppose this is the result of deadlines and lax code reviews.
This is usually the result of years of changing/updating the code. Any living code base tends to grow and accumulates cruft over time. Just because it doesn't fit the ideal you learned in university doesn't mean it is bad code, this is real life code.
There is no excuse for not writing well commented code - or even code with well named variables and functions that reduce the need for as much commenting.
If you suffer from code like this in your workplace, perhaps actively comment it as you reach an understanding of what it does.
"I've worked in the Games industry with age old engines, I currently work in embedded development with a code base over 10 years old - I'm fully aware of what "real life" code is like. There is no excuse for not writing well commented code - or even code with well named variables and functions that reduce the need for as much commenting. If you suffer from code like this in your workplace, perhaps actively comment it as you reach an understanding of what it does."
Relatively experienced 6 year C++ dev - I mainly mentioned university because things where much stricter - but having an environment with decent code reviews and maintaining a little self discipline goes a long way with the sanity of your co-workers.
6 years is significant, but it's not a long time. There's much to learn, particularly about the cultural aspects of various segments of the industry.
This is excellent code that was produced on a tight timeline.
*Edit - Yes though - by no means do I think I'm some big shot know it all. But I do have strong feelings about readability after having had to deal with code which is awful to grok.
To clarify, my objection was that we're criticizing this code for superfluous reasons when it's currently a delicate political climate in most game companies to even release code like this in the first place. It's probably best not to imply their reputation should be harmed by their lack of comments, since this can discourage other game companies from releasing their code in a similar manner. I've seen the argument "it might harm our reputation" be used to block endeavors like this.
I really hope that companies don't view criticism as a negative thing, it's cause for discussion and change which are most certainly positive - I hope some bugs are found and they start to feel the benefit of the community a little.
Using raw pointers in C++ is rarely really needed.
Shared pointers (as in ref counting) is just an example of that, so I understood the above comment as implying that game development somehow encourages manual pointers management. May be I just understood the intention wrong, and it didn't mean to exclude other types.
More info on performance loss due to cache thrashing:
https://lmax-exchange.github.io/disruptor/
https://lmax-exchange.github.io/disruptor/files/Disruptor-1....
http://martinfowler.com/articles/lmax.html
http://mechanical-sympathy.blogspot.com/2011/08/disruptor-20...
And http://mechanical-sympathy.blogspot.com/ is great in general.
The reason that raw pointer management works in gamedev is because gamedev is closer to crafting than traditional programming. No one will die if a game crashes, and the iteration loop is a tight feedback cycle of code-compile-run code-compile-run.
Due to the nature of the entertainment industry, the codebase also loses much of its value within a year of releasing the game, as opposed to traditional software that typically gains value with time, meaning it's more important to get code out the door than to get it right. History is littered with the skeletons of game companies that disregarded this unfortunate truth.
The reason this works is because of discipline. Generally, there is a FooManager class which owns Foos. The FooManager is responsible for both allocating and deallocating Foos, regardless of where they're used. And in a game, "When should something be deallocated?" usually has a clear answer: When the level loads, for example, or when you move from one part of the continuous world to another part.
Then there are Subsystems (singletons) for each division of the engine: GraphicsSubsystem, InputSubsystem, etc.
Between those two patterns, there aren't a lot of ways to lose track of a pointer.
So limiting lifetime of the object by some scope should work pretty well for it, and if you can pass raw pointers to transfer ownership, you can as well pass managed ones. At least it's safer.
Anyway, by using something like Rust a lot of such problems are solved by the language itself.
I don't know where Rust came from, since the discussion was about C++. But feel free to write an engine in Rust. It seems like a promising approach.
I think another important factor is the pseudo-realtime update loop that synchronization is tied to.
Unless there is some garbage collection between game state changes (unloading unused resources during a level, etc...), its rare that references are invalidated unexpectedly, or accessed in parallel to the cleanup step between game state changes.
eg. a global state change from RUN to CLEANUP tells the subsystems to stop using a resource, so during the CLEANUP state the subsystems can safely delete any resources they have ownership of.
The more time I've spent understanding and building game engines, the more I see it as an organised network of state-machines managing and working with collections of data.
More often than not, shared state has clear ownership and lifecycle management built into the relevant state-machines. By isolating creation and destruction of resources in the transitional states (load and start a level, open a menu, change to Game Over screen), most of the code can safely reference data from other subsystems without reference counting, under the assumption that references are only valid until a global, shared transition in state.
Imagine a player entity that stores a reference to a model, texture, sound effect, input state, etc... If that data is loaded at the start of the level, and destroyed when the level ends, is there really a need to inc/dec a reference count if an enemy entity shares a sound effect reference?
Long functions which have grown over time are also understandable, given the realities of changing requirements and such.
Thousands of lines of uncommented code, however, are not understandable... in the sympathetic sense, or literally.
Real-world work stresses getting stuff done. And in this case parts of the code can also take a life of their own.
Ideally both ideologies meet somewhere in between and you get code that works and is maintainable. But sometimes you can get bad code that is shipped... or an unhealthy obsession with polished code that never ships. One is infuriating, the other makes businesses slowly unfeasible.
Frequently changed parts of the code were a hideous mess of 90% comments to 10% code. And then occasionally we'd just rename an entire file by prepending 'old' to it and starting fresh with a new file.
That place was such a learning experience on the concept of technical debt. But at least I got a fun talking point for my resume. "x++, is that a typo?" "No, sir..."