Let's write a video game from scratch like it's 1987
gaultier.github.io
gaultier.github.io
And even with this impressive reduction in resource usage, it's actually huge for 1987! A PC of that age probably had 1 or 2 MB of RAM. The Super NES from 1990 only had 128Kb of RAM. Super Mario Word is only 512KB.
A PlayStation from 1994 had only 2MB of system RAM. And you had games like Metal Gear Solid or Silent Hill.
If I recall correctly, home computers tended to ship with between 4 MB and 8 MB of RAM just before the release of Windows 95. There were also plenty of people scrambling to upgrade their old PCs to meet the requirements of the new operating system, which was a minimum of 4 MB RAM.
I distinctly remember the 386sx-16 I got late 1989 came with 1 megabyte and a 40mb hard drive for just under $4k from Price Club (now Costco), which was an unusually good price for something like that at the time.
You needed something way more expensive to run X11 before 1990.
Since we are talking about software written today, not just software available in 1987, X386 (which came out with X11R5 in 1991) was more than capable of running on a 386-class machine from 1987. Granted, a 386 class machine with 1MB of ram and a hard-disk would have been pushing $10k in 1987 (~$27k in 2024 dollars), so it wasn't a cheap machine.
https://lunduke.substack.com/p/desqviewx-the-forgotten-mid-1...
When arcade games started to be written in C, it was still using mainframe and micros, with downlink cables to the development boards.
I really like frame locked, often using sprite system, type games. Atari VCS comes to mind, as do many other systems that have sprites and some means to sync with the display.
But the feel of those is crisp, sharp edged, fast, sometimes too perfect if you can maybe guess what I mean.
Bitmap games are different! The drawing, masking, and every other thing happens on the CPU. It feels a bit wild, and the feel varies. Number of enemies, missiles, etc... do often change the speed a little. And that's nice, kind of wild! The game feels alive in a way. Not living but not stale either.
The author packed a lot of shooter into a 256 x (whatever) screen. And the art is really good! Takes a bit of talent to make multiple baddies stand out amidst quite a bit of background detail.
And the sounds! Little bits of this and that packed into the game loops. That's a feel I like too. Often sounds are just a click or beep. But sometimes there is a little more, and this game does that.
Great work. Just my kind of game.
Fun bit: the explosions and possibly other sounds were just us pushing graphics bytes to the audio port (a 1-bit piece of metal which you had to flip in software to make it vibrate) as we rendered them. A poor man's source of noise.
On my 2600 game Ooze! I would drop item Y values into the audio registers as a poor man's way of making some cool sounds of enemy advancing.
Same for the player missile. As it travels, some variation of its Y value becomes the sound.
Tossing bytes at ports does insure the user associates the noise with the right on screen action.
Anyhow, great art! You did a fine job making things stand out and that is not easy.
There's always Elite, a whole space sim game in 1984
Was it developed on an actual ZX Spectrum? I think I read that the development environment for the Spectrum port of R-Type was running on a 80286 PC, curious if this was common back in the day.
We used HiSoft's GENS assembler. My brother reverse-engineered the microdrive code in GENS and replaced it with functions to access the Timex drive. That was a huge timesaver for us and other Spanish developers at the time. The source code and the GENS program had to fit in the 48K.
For our next Spectrum game, we used a hardware system to connect an Atari ST and develop on it. It was certainly faster and more comfortable, but the system was buggy as hell and crashed/corrupted the source almost daily.
But yeah, "actual" in that it's a real commercial game (30k copies or so) developed and released in 1987. If memory serves right, we started in late autumn'86 and it was published around November'87.
This is my thought as well. You can even avoid some of the grotty details of this article if you use Xlib as your interface instead of going in raw over a socket. Basic Xlib is surprisingly nice to work with, albeit with the caveat that you're managing every single pixel on the screen. For something like a game where you're not using system widgets it is all you need.
Where people ran into trouble is when they try to add the X Toolkit, which is far more opinionated and intrusive.
http://www.art.net/~hopkins/Don/unix-haters/x-windows/disast... exaggerates slightly but is basically in keeping with my experience. win32 gdi is dramatically less painful. it's true that the x toolkit is far worse than xlib
if you do want to write a game where you manage every single pixel, sdl is a pretty good choice. i also wrote my own much smaller alternative called yeso: https://gitlab.com/kragen/bubbleos/blob/master/yeso/README.m...
tetris with yeso is about five pages of c: https://gitlab.com/kragen/bubbleos/blob/master/yeso/tetris.c
the stripped linux executable of tetris is 31.5k rather than 300k. it does need 10 megs of virtual memory though, but that's just because it's linked with glibc
i should do a minesweeper, eh?
Go for it. I just finished a lazy port to C and SDL. Not counting SDL and the spritesheet it's 42Kb. It's a fun weekend hack.
If you've ever actually tried using that configuration, you might notice that every part of every application suffers from this same problem. Almost all slowness of remote X11 used to be caused by stacking up round trip delays. Probably still is, though there's another cause now, which is transferring all the pixel data because apps treat it as a dumb pixel pipe.
This isn't a niche problem and it doesn't only affect application startup.
I did it in Turbo Pascal with BGI graphics. I remember having problems with recursively uncovering empty tiles in large levels where mines were sparse. Recursive algorithms turned out to be rather tricky in general when the stack segment is 64k.
I added a starting disk of various diameters which let you pick a starting location without risk of explosion, which I think was appreciated.
This is probably a better description (from a code point of view) on what you had to do as a programmer to write a game in the late 80s / early 90s:
And that dates to 1983.
For me the big thing was all the latencies stacked together. Slow hard drives, slow floppies, god help you if you swapped, etc. The era was mostly waiting around for the beige machine to finally do its thing.
Back then it was just "new" beige and "yellow-ish old worn" beige.
It was a great language and IDE for a young self-taught programmer.
> One interesting thing: in Odin, similarly to Zig, allocators are passed to functions wishing to allocate memory. Contrary to Zig though, Odin has a mechanism to make that less tedious (and more implicit as a result) by essentially passing the allocator as the last function argument which is optional.
Zig does have support for empowering library users to push coded settings in to libraries, and you could conceivably use this for writing this type of code. Although it’s probably not worthwhile.
Or you can just straight up default your library to using a specific allocator, fuck the calling code.
Anyway. Zig has patterns to do stuff like this, but it’s probably unwieldy in large projects and you’re better off just making it a parameter.
You can also look in to the std ArrayList, which provides managed and unmanaged variants for yet another way that you might write code that empowers users to set an allocator once, or set it every time.
An allocation library cannot serve two masters. Maximizing single thread performance is anathema to multithreaded performance.
I'd go further. If allocators don't matter, why are you using a systems programming language in the first place?
(when i've done similar things, it's been to pass an arena for inlined pointer-bumping allocation, a strategy widely used by runtimes that want to make allocation fast. normally this requires two registers, though chicken gets by with just one)
However, again, if you don't care about your allocators, why are you using a systems programming language? If you're willing to give up control over allocation, you are way, way better off in a managed memory (garbage collected or reference counted) language.
Systems programming is pain for gain. You give up a lot of convenience in order to have strict control over things--control of latency, control of memory, control of threading, etc. The price for that control is programming with a lot fewer abstractions and far less help from the language/compiler/interpreter to keep you from shooting yourself in the foot.
If you aren't using that "control" then you're just making life painful for yourself for no good reason.
Obviously, people can use whatever language they perfectly well want for whatever reason they want. Given how much I talk about and use C, Rust and Zig, people are always surprised that if they ask if they should use any of the systems languages (C/C++/Rust/Zig/etc.) my first response is always "Oh, hell, no."
but i disagree pretty comprehensively with your comment. i don't agree that systems programming is pain, i don't agree with your definition of systems programming as programming with tight low-level control, i don't agree that tight low-level control is pain, i don't agree that abstraction is a necessary or useful thing to give up either for systems programming or to get tight low-level control (though it certainly is expedient), i don't agree that garbage collection (including reference counting) is the only way to simplify memory management, and i don't agree that either systems programming or tight low-level control requires accident-prone languages (and i'm especially surprised to see that assertion coming from an apparent rustacean)
that is, i recognize that these tradeoffs are possible. i just disagree that they're necessary
That's a nice theoretical position; however, the current programming languages as they exist disagree with you.
I would also point out the one of the problems in "systems programming" is that it encompass both "can run full blown Linux" and "slightly more CPU than a potato and has no RAM". Consequently, there are VERY different lenses looking at "systems programming".
> i don't agree that either systems programming or tight low-level control requires accident-prone languages (and i'm especially surprised to see that assertion coming from an apparent rustacean)
Rust is particularly poor when you can't define ownership at compile time. If you have something that you init once and then make read-only, you will be writing unsafe. If memory ownership passes between Rust and something else (say: memory between CPU and graphics card), you will be writing lots of unsafe. RPC via shared memory with a non-Rust process--prepare for pain.
Writing "unsafe Rust" is super difficult--moreso that straight C/C++. If you are writing enough of it, why are you in Rust?
You have to architect your solution around Rust to make the most of it and lots of things (especially stuff at runtime) are off limits. See: Cliff Biffle from Oxide and all the things he needed to do to make their RTOS completely defined at compile time because anything at runtime just gave Rust fits.
This is not the main reason that hubris does things up front. It does that because it makes for a significantly more robust system design. From the docs:
> We have chosen fixed system-level resource allocation rather than dynamic, because doing dynamic properly in a real-time system is hard. Yes, we are aware of work done in capability-based memory accounting, space banks, and the like.
as i pointed out in another comment, c++ is a good example of having tight low-level control without programming with a lot fewer abstractions, and c++ is hardly a little-known language we can dismiss as irrelevant
so the current programming languages as they exist do not disagree with me
c++ is a good example of having tight low-level control without programming with a lot fewer abstractions. indeed, the abundance of abstraction is what makes c++ usually so painful
i think it's reasonable to describe game engine programming as systems programming. i mean some of it, like writing interpreters, persistence frameworks, drivers for particular devices, and netcode, is pretty obviously right in the core of systems programming, but plausibly all of it is in that wheelhouse
It's also the most important reason why the C++ standard library is so unpopular in the space. All standard C++ containers allocate implicitly because often enough that's OK even in systems programming. What matters is that you can control the allocation more finely when you want.
Seems like that could be further optimized especially for simple 2D games that don't use many features. I was impressed overall though. I hadn't looked at Godot in a long time.
And, like electron, the size is almost entirely unnecessary. Even in the browser you can make quake in 13kb:
I did a 1-1 replica of the Windows 95 version of Minesweeper.
You can find it at https://github.com/danielricci/minesweeper
I didn’t do anything fancy in terms of reducing the size of the output file, but it was fun to try and replicate that game as best as I could.
There is definitely something to be said of bloat but probably not in this case. You could even keep supporting Linux versions as old as this promises by using legacy 1.x SDL.
In addition, there are solutions for cache locality of the pixbuf for SDL that you can code around, if you specifically need higher performance X11 code on Linux.
You feel it's a flaw because you only ever run applications locally. But more constraints are a side effect of more possibilities, because you have to program the lowest common denominator of all possible scenarios, so your program works in all of them.
That's how APIs work. They all have this tradeoff.
I'm not just talking about a Perl script running on Apache like it's 1999. I'm talking about globally distributed cloud apps. It might be theoretically possible with X11 but it doesn't actually exist. Maybe we could envision a world where x11s://docs.google.com transparently opens an SSH connection, sets up X forwarding back to my local X11 server, and allows me to work with "Google Docs" the X11 client app just like how a browser tab works today. But nobody's written that, and they haven't written any of the myriad pieces of infrastructure to allow it to scale to a global user base either. That's not even getting into the security and interoperability concerns between apps running on different hosts which the browser has (not always in the best way) already addressed.
> how is Wayland's architecture much different from X11's again?
Honestly, I don't know that Wayland is really that much better. Though I think abandoning network transparency was the right move, I can't say I feel the same about any other decision (mostly due to ignorance). It seems to have stabilized enough after 15+ years that the major desktop environments and distributions have (mostly) adopted it, but honestly I've never seen any tangible benefit as an end user. I too have asked "why not X12?" but I don't know that it was any more feasible. The competitors (Windows, macOS/iOS, Android) were all able to deliver more coherent experiences (and in a lot less time) because one entity owns the whole window system stack. Wayland set out to maintain a lot of the same agnosticism X11 had about particular details; while perhaps inevitable given the goals, the end result is nowhere near as simple and cohesive as the competition.
X11 did network transparency wrong. Nobody in that ecosystem stepped up to do it right. The web does it better, even with its warts. Citrix is going to find its way into the same graveyard before too long. This isn't a problem Wayland can solve given the way desktop and mobile apps already work.
Delivering a working desktop experience is more important than trying to achieve some kind of purity about what seemed like the right thing to do in the 1980s. Even RISC CPUs have adopted SIMD and task-specific instructions like cryptographic acceleration. VNC and RDP are good enough. Wayland has taken so long to reach maturity, I think, because it tried to do too much more than what was actually necessary.
But most of all, the biggest strength and weakness of open source is that it allows anyone and requires someone to put in the work. If you think X12 is the right solution, then all you need to do is make it happen. For all my gripes about Wayland, it exists in large part because nobody was really trying to keep X alive and working well on modern computers.
It's not a flaw if you want to run a thin-client from a central machine or otherwise offer a networked interfacing system, no.
One of those use cases is far more common today than the other.
The joke was on her, of course, because I didn't have any friends. :-(
Later I wrote a basic ray tracer for my TI-89. I even made it do 4x anti-aliasing by rendering the scene 4 times with the camera angle slightly moved and had a program that would rapidly move between the 4 rendered pics so that pixels that were dark for only some of the pictures would appear grey because of the screen's insanely slow response time. A basic "reflective sphere over a checkered plane" in that super low TI-89 resolution still took like 90 minutes and drained half the battery.
It tells the story of how TI got into the calculator market and the domination it achieved in the US classrooms (+ other interesting tidbits).
It is legitimate but by 1987 …
The 1980-2000 era was just a raw font of imagination for video games. Since then things have become more iterative and tend to focus on maximizing profits, though thankfully there is a lot of creativity in indie studios/solo devs being empowered by the increasing ease of modern game engines, and even some bigger studios here and there, like FromSoftware.
Unsurprisingly though they also spent a lot of time developing a lot of time-saving tools, like macro assemblers and higher level languages like C.
Probably the games on the Amiga and Atari ST was the last heyday of these kinds of development.
x y z z y S-Return
would cause the top left pixel of the screen to change color depending on whether the cursor was over a safe square or a bomb. Since you could plant flags before the timer started, it was possible to get rather unrealistic times.WTF? This is a showcase of everything wrong with the company today.
https://github.com/id-Software/DOOM/blob/master/linuxdoom-1....
Nicely and consistently formatted, relevant comments, sane structure, along with decent fn, and var names.
What is your definition of "good, readable code?"
P.s. I've emailed the mods, but a currently dead child comment from a new account states essentially the same thing (my vouch was insufficient to rez): https://news.ycombinator.com/item?id=40742493
If the code is perfectly legible to you, can you explain how R_DrawPlanes draws the ceiling and the floor, step by step, as a practical illustration? How long did it take? It took me maybe 5 minutes to understand how it works.
I think just about every function I read every day is easier to comprehend. And I review a lot of game engine code. I make no claims I’m a fantastic programmer, of course.
the last two look like this
if (fixedcolormap)
ds_colormap = fixedcolormap;
else
{
index = distance >> LIGHTZSHIFT;
if (index >= MAXLIGHTZ )
index = MAXLIGHTZ-1;
ds_colormap = planezlight[index];
}
that's indentationmaybe you're running some kind of browser extension that screwed up the formatting of the code in your browser. or maybe the problem is that the indentation uses 8-space tabs and your browser is rendering the tabs in some other way. (when i initially pasted the code in here, hn converted them to spaces.) what browser are you running? the code looks fine in linux firefox
the only exception to this perfect syntactic consistency in compound statements is that in two cases (lines 137 and 237) there are unnecessary braces around a single statement, turning it into block that could have been just a simple statement. i don't know if this is an artifact of the code's edit history or what
They didn't have no gofmt back then. We are spoiled today with the extreme consistency :) the skill of reading code includes reading even when it deviates from one's own preferred formatting (within reason, maliciously formatted code can be challenging, to say the least).
I must respectfully disagree about this being an issue worthy of anyone's attention, especially yours and mine.
Didn't need gofmt as plenty of IDEs and editors had automatic indentation and syntax colouring already implemented.
1993 wasn't the dark ages you know.
If this programmer leaves the team and their large codebase like this is left behind, it will likely become a calcified legacy monolith of code that is expensive to maintain.
Today, such code would not pass code review and possibly some automated pre-submit testing (Halstead’s metrics, Microsoft’s maintainability index, etc) in the AAA companies I worked for that cared about code maintainability.
It’s not specifically about formatting or syntax. It’s more about whether a different programmer can look and understand exactly what it does, and what cases it handles, and which it doesn’t, at first glance. And the other programmer can’t be Carmack-experienced or grey beards — it has to be the usual mid-level dude.
The code could even be written in a self-documenting code paradigm with parts extracted into small appropriately named functions. And it doesn’t need to be done to some perfectionist extreme, just a little bit more than what Carmack did. It just can’t be what we jokingly refer to in the industry as academic code — made for one author to understand and for every other reader to be impressed but not work with.
1993 was a different time, more code was academic, most of it was poorly documented. And there were good reasons not to have function call overheads. We even used to do loop unrolling because that saves a jump instruction and a few variable sets (you only increment the program counter as you go). So some of the reasons why this code is the way it is are good. But in readability, we have evolved a lot in the games industry.
So much so that Doom’s code is pretty hard to read for most programmers. I asked around at work, in a large AAA company, and the consensus is that it’s archaic. But you know, it’s still good code, it did what it had to do, I’m not bashing it.
What do you find unreadable about it? What would you do differently now that there have been 30 years of software development between then and now?
I would at least add more comments for context. Some teams have other ideas, like self-documenting paradigms with small functions and descriptive names.
If a typical programmer doesn’t at a glance understand what each line of each function does on screen, what all the variables mean, what changing any line would do, what is the usual data flowing through the function, and what are the limitations of the function, then it either can’t be maintained quickly and cheaply, or maintaining it will introduce unforeseen bugs.
But the key point is to not have a codebase that only a small core team can maintain.
If you want to see examples of the difference between this and modern in C-like code, see Unreal Engine’s source code. It will generally, at least in areas of frequent change, be much easier to read. I would expect a mid-level programmer to understand 90% of UE’s functions in 10 seconds each. And more experienced programmers usually understand most functions they’ve not seen in UE in a couple of seconds.
That’s not the case with Doom code. It took me up to 5 minutes to understand some of them. That means significantly worse readability. And I work in C++, C, C#, and other programming languages at a quite senior level in AAA games. So I don’t think this is a skill issue. It could be, of course.
I always saw the TI99/4A as one of the "zillions" of "microcomputers" in the 70s/80s but from your comment I now learnt that it was not so simple regarding Texas Instruments, one of the leaders (if I remember well) of the semiconductor industry at that time. Also the ColecoVision [2] used a variant of that chip. The ColecoVision had the best version of Donkey Kong [3] available in game consoles.
Reading about the TMS9918A[1] now. Thank you very much.
[1] https://en.wikipedia.org/wiki/TMS9918
[2] https://en.wikipedia.org/wiki/ColecoVision
[3] https://archive.org/details/donkey-kong-game-manual-coleco-v...
This is more like 1992.
Michael Abrash's book was published in 1990.
So I just threw out the graphics library and wrote directly to the screen memory. Lots of games did that
"We will implement this in the Odin programming language"
checks Odin homepage...
"The project started one evening in late July 2016"
What? How big are your hostnames?
What?