As a tutorial series, I think using plain C is a cool approach and will help illuminate what's going on under the hood and what parts of the engine are really essential.
But if you yourself want to write your own game with just you or a small team of like-minded people, I would highly encourage using C++. As long as you don't have too many cooks arguing about which C++ features to throw into the pot, you can pick a subset of C++ that isn't much more complex than C (which is already more complex than most realize) and you'll get a much cleaner, safer language. C++ has saner rules for implicit type conversions, namespaces, overloading, and a cleaner notation for dynamic allocation. Those alone make it a sufficiently "better C" to be worth using in my book.
Going farther, even if you don't like "object-oriented programming", I think classes offer modularity and encapsulation features that make them worth using, even if you never once write the keyword "virtual" or use subclassing. (Fun fact: the first version of C++ did not have virtual methods!)
I like C and enjoy programming in it, which I've done for over 20 years. I've written a successful open source project in it and am writing a book that uses C as one of the implementation languages. Even so, I generally only use straight C if I'm writing a library that I want C users to be able to consume. Otherwise, I think C++ contains any number of "better C"'s within it, and it's mostly a matter of choosing which better C you want.
”...it's mostly a matter of choosing which better C you want.”
For a beginner that can make things more difficult: in addition to the actual thing you want to learn, you need to first become a capable curator of language features from the past 35 years.
JavaScript today has the same problem: you can’t just start writing a web app because two lines into a tutorial you’ll be barraged with “So this is actually an ES2017b.71 feature that we’re enabling using babel-ts-flooginator, therefore you also need TypeScript and that means you need to...”
C feels clunky today, but writing more code in exchange for not having to curate the language can be helpful for learning.
It makes me wonder if this can be solved technically by published curated language subsets. Racket does this by supporting a bunch of different dialects, specifically to make it easier to teach [0].
One good do something similar for other languages. Step one is probably just writing a doc that says "Here's a standard subset of C++ we call Blah. These are the features it uses and these are the ones it doesn't: ..."
Then you could add tooling so that it will warn you if you use a prohibited feature.
Of course, this just pushes the problem up a level: now a new user has to know which curated sublanguage to use. But that's arguably simpler than doing it on a per-feature basis. At least they can just order a combo instead of having to pick a la carte.
I generally stick to it in all projects I write. The only exception is that in a few cases I'll use features that are disallowed in Google C++ primarily for historical reasons, most notably exceptions: https://google.github.io/styleguide/cppguide.html#Exceptions
The problem with exception-less C++ is that new, delete, ctors, and dtors become timebombs when they fail in a way that would have generated an exception.
I'm not sure why you were downvoted. I almost exclusively write exception-less C++ (compiling everything with "-fno-exceptions"), but correctly handling errors in constructors and destructors is absolutely an important concern. It's not hard to do, obviously, but it does require forethought and heavily encourages delegating complex logic to other parts of the code.
dtors that throw come with their own share of problems: If they do so while another exception is in flight (dtors are still called for objects when the stack is being unwound(?)), C++ crashes the process.
For our code base we avoid exceptions, except when due to awkard workarounds, or kludges mostly due to some API that's written in a way that you need to communicate with it with exceptions.
That to be said, even if you don't use exceptions in C++ - you must write exception safe code - e.g. as much as possible no manual "begin"/"end" but wrap things behind RAII, such that "end" is still called, in case of exception. Hence allocation through unique_ptr, shared_ptr, etc. is preferred over new/delete.
Because you never know the function you are calling whether deep down it won't throw an exception...
And some people choose to use more comprehensive documentation, like Google's C++ style guide, at the cost of imposing more restrictions that are largely in place for corporation-specific reasons that smaller personal codebases don't necessarily need to regard.
Choosing a curated language subset is exactly the "how do I curate the language" problem being referred to. The solution is not to subset a language, but to refuse to superset it. Stop building DSLs and frameworks and domain-specific abstractions and just stick to a flexible core.
>JavaScript today has the same problem: you can’t just start writing a web app because two lines into a tutorial you’ll be barraged with “So this is actually an ES2017b.71 feature that we’re enabling using babel-ts-flooginator, therefore you also need TypeScript and that means you need to...”
That's not a problem with javascript, it's a problem with the development culture, its schizoid relationship with the language and its obsessive need to be "cutting edge." You can, absolutely, just start writing a webapp because vanilla JS and even jQuery still work perfectly well. What you can't as easily do is find tutorials that don't assume Javascript has to be compiled from another language and pushed through a complex tech stack before you can even approach it.
I was mentoring these kids writing some code in C++ and I saw they were using all this syntax and functionality that was familiar from other languages, but that I had no idea was in C++. You see I used to program on games in C++, but like twenty years ago. I have learned a lot of languages since then, and don't really have a reason to go back to C++ although I liked it fine as a language. But it is possible to write C++ now that is really alien to me.
yep, I choose C89 for this project because there's no Object/Prototype/Class/overloaded bullshit, you get what you wrote and no more, i think it's easier to understand than any other high level language where you have to use obscure things by design.
Actually, this tutorial is not just about "building a game engine in C89", it's more about learning the concepts behind game engines, the language and the code is just illustrative examples.
I like C, but you definitely don’t “get what you wrote” in any meaningful way. You absolutely have to understand all sorts of nuance about undefined behavior, the size of primitives on various architectures, etc. I’m not advocating for C++, but pointing out that C is far more error prone and surprising than many HLLs. And this doesn’t touch on the regular error prone things about C, like memory management, array indexing, etc.
yep, i said "get what you wrote" and it's true, if you want your iterations to not get out of range you have to do it yourself, if you want to make a dynamic array you have to do it yourself and if something is not working as expected means that you wrote something wrong, that's far from the opaque flexible behaivour on javascript, f.e.
I meant that i think is easier to understand C than other languages cause you see all the flow without weird library stuff, maybe i'm wrong, dunno :(
Like I said, I wasn't commenting about out-of-range or those other things, I was commenting about the surprising undefined behaviors. :) JavaScript and many other HLLs have their criticisms, but they are generally far less surprising than C. In particular, there are languages like Rust and (to a lesser extent) Go which lack many of the dynamic runtime features of scripting/VM languages and behave very similarly to C, but with far fewer footguns (e.g., no architectures with 9-bit chars!).
Although this was mostly a decompiled project, I really appreciate the architecture and code style of https://github.com/OpenTTD/OpenTTD which seems to use OOP minimally and more c oriented code.
Absolutely, i stick to C89 in this serie for clarity as you said, knowing what a game engine does under the hood is the only point here and C is a self explanatory language by design (even if it can be "tricky" sometimes), Im also doing tricky things with it (not that tricky anyways) like the method-like function pointers in the structures that can be "obscure" for those who only work with high level languages but i'm trying to explain everything with detail.
As i said previously, i'd never do a profesional game proyect (or even a hobby one to be sold) with C89, it takes lots of boilerplate and it feels like reinventing the wheel over and over. I'll have to code a hash map approach, a growing array (already did), a "garbage collector" in C89, things that you either don't need or have in the C++ Stdlib, so yep, you are absolutely right.
Also, i enjoy C :D
Maybe after this i could do the same with C++ macroprogramming
I like your approach, mostly because it's bottom-up, and because this is an excellent opportunity to illustrate common pitfalls. Those pitfalls can be identified using sanitizers (-fsanitize=undefined,leak,address,thread etc). With the correct CI setup you'll have a safety net, where sanitizers provide precise motivations why one approach works, and another also works, but at risk of undefined behaviour.
I have no opinion on use of C89 instead of C++, since that's just an implementation detail. Also, for your purposes, it works to your advantage, since C is basically the white-box of C++.
> C++ has saner rules for implicit type conversions
I say this partly in jest, but once I have wrapped all my primitives in structs C has a perfectly reasonable rule for implicit type conversions: "don't".
As an embedded firmware developer, I really wish there was a way to outright disallow implicit type conversions in C source code. You'd have to exclude libraries, but I'm creating "typedef enum" to show what I'm doing AND help make sure I don't somehow screw it up. If all of those typedefs are interchangable with each other (and ints and chars), I lose out on part of the functionality.
To get nominal typing in C, wrap things in a struct. There shouldn't be any performance overhead (the generated code should often, if not always, be unchanged), and the boilerplate can be manageable (and you can unpack things locally when it starts to get too messy).
Any library that's not on board needs to be wrapped, for sure. Fortunately you can do it in just the prototypes if you keep things ABI compatible. It fits well with the practice of decorating functions with empty structs, which is always ABI compatible (... in C, with mainstream compilers. In C++ it is explicitly not ABI compatible, sadly).
It's extra important, in writing this kind of C, to pick a granularity of types such that they help you make the distinctions you need without drowning you in casts. And I'm not at all sure such a granularity always exists. It worked out very well on the (greenfield) project I built this way, though.
I'm a (primarily) C developer who would love to use C++ this way. I have some C++ experience but not enough to know which elements I should include or exclude. Or even how to begin effectively deciding that.
Can you point to any resources I can use to learn?
Speaking from my own very limited experience, I think C++ can be pretty nice if you basically write straight C but borrow from the C++ STL. Smart pointers (std::unique_ptr, std::shared_ptr, etc), collections (std::vector, std::unordered_map, std::set, std::stack, std::priority_queue), and stuff like std::tuple and std::optional really make C feel less clunky without getting too gross.
But if you yourself want to write your own game with just you or a small team of like-minded people, I would highly encourage using C++. As long as you don't have too many cooks arguing about which C++ features to throw into the pot, you can pick a subset of C++ that isn't much more complex than C (which is already more complex than most realize) and you'll get a much cleaner, safer language. C++ has saner rules for implicit type conversions, namespaces, overloading, and a cleaner notation for dynamic allocation. Those alone make it a sufficiently "better C" to be worth using in my book.
Going farther, even if you don't like "object-oriented programming", I think classes offer modularity and encapsulation features that make them worth using, even if you never once write the keyword "virtual" or use subclassing. (Fun fact: the first version of C++ did not have virtual methods!)
I like C and enjoy programming in it, which I've done for over 20 years. I've written a successful open source project in it and am writing a book that uses C as one of the implementation languages. Even so, I generally only use straight C if I'm writing a library that I want C users to be able to consume. Otherwise, I think C++ contains any number of "better C"'s within it, and it's mostly a matter of choosing which better C you want.