Forty-Six Bullets Dragged onto a Form
The oldest game code I still have is a top-down shooter written in Visual Basic 6, last touched in August 2004, when I was seventeen and living in a town of about a hundred people in rural South Australia. Thirteen numbered betas, a feature list, and six friends credited as testers by name and handle.
What's in there is the first, crude version of most of the things I'd spend the next twenty years doing properly.
The Bullet Pool Is Forty-Six Shapes
Opening the play form shows what the game is made of:
46 × VB.Shape Bulletbox
46 × VB.Shape eBulletBox
6 × VB.Shape RockBox
4 × VB.Shape eBoundingBox
4 × VB.Line (screen borders)
Every bullet, asteroid, and enemy is a Visual Basic form control. Not a struct in an array with a sprite drawn at its position, an actual control placed on a form in the designer.
Which means the pool size is however many shapes I dragged onto the form. Forty-six player bullets exist because I drew forty-six rectangles, one at a time, in the IDE. Wanting forty-seven would have meant drawing another one.
The thing is, this is object pooling. Nothing is created or destroyed while the game runs. There's a fixed set of objects, each either in use or parked, and firing a bullet means finding an unused one and moving it. That's the same idea as the ring buffer in my particle system eight years later, arrived at without knowing it had a name, and expressed by dragging shapes with a mouse.
It's also why the number is forty-six rather than a round number. Forty-six is how many fitted before I got bored.
GDI, Declared by Hand
The shared module is a wall of Win32 declarations:
Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal X As Long, ...)
Declare Function CreateCompatibleDC Lib "gdi32" (ByVal hdc As Long) As Long
Declare Function CreateCompatibleBitmap Lib "gdi32" (ByVal hdc As Long, ...)
Declare Function SelectObject Lib "gdi32" (ByVal hdc As Long, ByVal hObject As Long)
Declare Sub Sleep Lib "Kernel32" (ByVal dwMilliseconds As Long)Visual Basic had drawing methods of its own. These are the underlying Windows graphics calls, declared by hand so they can be called from a language that wasn't built for it.
Doing that at seventeen meant reading Win32 documentation written for C programmers and translating each signature into VB's declaration syntax, where getting a parameter's type wrong doesn't produce a compile error, it corrupts memory. There's no type checking across that boundary at all.
The whole DT_ enum for text formatting flags is transcribed in there too, all twenty-odd constants with their hex values, most of which the game never uses. That's the mark of someone copying a reference table rather than the two entries they need, which is exactly what I'd expect and is how a person ends up knowing what the other eighteen do.
The Changelog Records the Moment
The feature list is a real development diary, and one entry stands out:
Beta10 Switched graphic engines to facilitate double buffering to fix flicker Streamlined program flow
That's the moment. Ten betas of a flickering game, then finding out that the fix is to draw into an off-screen surface and copy the finished frame in one operation.
Four years later I wrote a C++ engine and called it Double Buff. I'd have said that name was a joke about a technique everybody knows. Reading this, it's a name commemorating the first hard thing I solved.
Other entries are just as revealing.
Beta12 Added an animation for the explosions so that they dissapate and are offscreen quicker, therefore requireing less sprites to be onscreen
That's a content change made for a performance reason, and the reasoning is correct. Shorter explosions means fewer live objects means less work per frame. Seventeen-year-old me couldn't grow the pool past what was on the form, so he shortened the animation instead. Working within a fixed budget by changing the content rather than the code is a thing I'd now call production, and it's here, at the very beginning, forced by a constraint I'd created by drawing rectangles.
Beta8 Made an array for enemies. Allows for 4 simultaneous enemies onscreen. Removed the graphics loaded into the executable making it 10 times smaller
That entry holds two steps. The first is discovering arrays as a way to have more than one of something, which is the step from "the enemy" to "an enemy."
The second reads like tidiness and isn't. At 14 kilobits, a build that's ten times smaller is the difference between a thirteen-minute download and a ninety-second one, for every tester, every release. Moving the art out of the executable was the first asset pipeline decision I ever made, and it was made for bandwidth. Every engine I've built since has made the same decision again with more ceremony and less reason.
Beta7 Enemy now shoots. 20% chance every renderloop.
A twenty percent chance per frame. That ties fire rate to frame rate, so the enemy shoots faster on a faster machine. It's the classic mistake and it's completely reasonable when nobody has explained that frames aren't a unit of time. The fix is the same one the whole industry learned: multiply by delta.
Beta6 The enemys movement is determined by a random parabolic expression allowing it to move in smooth curves
Procedural movement from a randomly parameterized curve. That's a real technique, chosen because hand-authoring paths for an enemy that appears repeatedly is tedious, which is the correct reason to reach for it.
Beta11 Added an option screen where you can choose whether you want sound, buffering fullscreen or either or.
Double buffering is a user-facing option, because on a slow enough machine the extra copy cost more than the flicker. That's a real tradeoff exposed to the player, and it dates the code precisely.
What Was Borrowed
The collision module isn't mine. It carries its original header:
'This module was created by Alejandro Albino
'Thursday, October 10th, 2002
'You may do whatever you want with this module, but
'if you do good with it, please make sure to give me creditI kept the attribution, which turns out to be a habit rather than an accident. The engine after this started from NeHe's OpenGL basecode with the header intact. The one after that vendored Valve's profiling helpers with their copyright. The current one uses a third-party expression parser rather than a hand-written one.
Four codebases across twenty years, and in every one the borrowed parts are still labeled. That's the only thing in this archive I'd claim I got right first time.
Thirteen Betas and Six Testers
The process around it is as developed as the code.
Thirteen numbered releases. A written changelog per release, in plain language, describing what changed and sometimes why. A features list separating what works from what's planned. Six friends listed as testers, with their handles, in the file that shipped with the game.
The archive still holds what shipping one of those cost. Beta13.zip is 728 kilobytes, which at 14 kilobits is around seven minutes of upload at the theoretical floor and longer in reality, then the same again for each person downloading it. Beside it sits Beta13_nosound.zip at 256 kilobytes, a build with the music stripped out so it's a third the size.
That second file is the detail I'd missed on the first pass. It isn't a feature, it's a distribution format, made because some of the six couldn't afford the full download. Shipping to six people over that connection was enough work to justify maintaining a second build.
Nobody made me do any of that. There was no team, no deadline, and no audience beyond people who'd play it because they knew me. I did it because releasing something to named people and telling them what changed felt like what making a game meant.
That's the same instinct that shows up much later as release notes, as postmortems, as the documents I now write for a living. It arrived before any of the engineering did.
What There Was to Learn From
This was written in rural South Australia, in a town of about a hundred people. That number does most of the explaining for everything above.
A town of a hundred has no bookshop, no technical section in a library, no computer club, and nobody else within driving distance building games. There was no one to ask in person, and no one to show it to who would recognize what was hard about it. The nearest person who knew what a device context was lived somewhere I'd never been.
The connection was 14 kilobit dial-up. Not 56k, which was the standard by then. Fourteen, and the reason for that number is an interesting story in itself.
When the town was wired, the phone lines were run through a frequency-slicing multiplexer, so several houses shared the frequency range of one physical pair of copper wires. For voice that's a fair trade. A phone call needs a few kilohertz, the copper carries far more than that, and slicing it lets one run of cable serve several premises instead of one. Fewer kilometres of copper to a town of a hundred people, for no loss anybody would hear.
The loss lands somewhere nobody was listening. A 56k modem needs the whole usable spectrum of the line to reach its rated speed, and a sliced line doesn't have a whole spectrum to give. So the ceiling was fourteen.
ADSL was worse than slow, it was impossible. ADSL works by using the frequencies above the voice band, and a multiplexer that only forwards the voice band has nothing to carry them on. It isn't a matter of distance or line quality or being far down the priority list. The infrastructure physically could not pass the signal, and fixing it meant replacing the infrastructure. That took until nearly a decade after most of the country had broadband.
Convert that number. Fourteen kilobits is about 1.7 kilobytes a second at the theoretical ceiling, and less in practice. A documentation page with images on it is a minute. A code sample worth downloading is several. Nothing is browsed at that speed, it's fetched, deliberately, one thing at a time, having decided in advance it justified the wait.
I've written elsewhere in this series about infrastructure that becomes a single point of failure through an accumulation of locally sensible decisions, each correct in context, with a collective failure mode nobody modelled until the load changed. This is that, at the scale of a town. Optimising copper for voice subscribers was right for the load that existed. The load that arrived was data, and the same decision that saved cable foreclosed it for ten years.
So the sources were these. MSDN, on the disc that came with the compiler, which mattered enormously because the disc was the only copy that could be read at the speed of reading. It's excellent reference material written for professionals who already know what they're looking for. It states precisely what CreateCompatibleDC returns. It does not mention that the reason a game flickers has a name.
Books existed and cost more than a teenager in a town of a hundred had, and couldn't be browsed before buying.
Then forums and code-sharing sites, where people uploaded a zip of something that worked, with a readme, and other people downloaded it and pasted bits into their own projects. The collision module in this game came from that world, which is why it arrives with a personal email address and a request for credit in the header rather than a licence file. Somebody wrote something useful, put it somewhere public, and asked to be acknowledged. That was the whole system.
And Usenet and web forums for questions, where a good answer might arrive in two days and a bad one in ten minutes, and where asking badly got a person told off in a way that taught them to write a careful question before they learned to write careful code.
That last one carried more weight than it sounds. For somebody with no local access to anyone who knew anything, a forum wasn't a supplement to a community. It was the entire community, and a slow pipe to it was still the only pipe there was.
What that adds up to is a specific bottleneck. The hard part was almost never understanding a technique. It was finding out the technique had a name. Ten betas of a flickering game is not ten betas of failing to implement double buffering. It's ten betas of not knowing that "double buffering" is a phrase that can be typed into a search box.
Sneakernet
The way around all of this was to carry it.
School had a connection that home did not, so browsing happened there and reading happened at home. I'd go through forums and articles at school, save the text and the code to floppy disks, and carry boxes of them back and forth. A 3.5 inch disk holds 1.44 megabytes, which is nothing for anything but is a great deal of plain text. The game's own release archive is half a disk.
What that arrangement does to how a person learns is the interesting part, and I didn't notice it at the time because it was just how things were.
Reading became batch mode. Everything had to be decided in advance: what might be useful, what was worth the space, what to grab in case. Then it came home and got read offline, with no way to follow a link, check a reference, or search for the term in the article that turned out to be the important one. A question raised at the kitchen table on Saturday waited until Monday.
Which means the cost of a wrong guess about what to save was a day, sometimes a week. Not a second. Guessing well about what would matter before knowing what it said was the actual skill, and it's a strange one to have practised.
It also explains why some sources beat others by so much. A code-sharing site where somebody had zipped up a working thing was worth ten forum threads, because it was one fetch that could be evaluated later at leisure. MSDN on a disc beat MSDN on the web by an enormous margin, not because the content differed but because the disc could be searched at the speed of thinking. Anything with random access won, and anything requiring a round trip per question lost.
There's a version of this I'd meet again years later, writing a game that streamed assets out of a compressed archive where seeking backwards meant decompressing the whole file again. The fix there was to work out the order things would be needed and pack them that way, so a session became one forward pass. I'd been doing that by hand with a box of floppy disks, deciding what to fetch and in what order because the cost of getting it wrong was measured in days.
What Arrived Afterwards
The years right after this changed that bottleneck more than anything since.
Video arrived in 2005, and watching somebody build something is a different kind of teaching from reading about it, particularly for anything visual.
Then 2008 brought two things in the same year. A question-and-answer site meant a specific problem now had a specific answer that stayed answered and was findable by the next person with the same problem. And a code host meant a real, working, large codebase could be read for free, which had previously required knowing somebody willing to send one.
Free professional engines followed at the end of that decade and through the next, so building a game stopped requiring somebody to build the thing that runs the game first.
Each removed a different obstacle: not knowing what to search for, not being able to watch the work being done, having nobody to ask, having no real code to read, and having to write an engine before writing a game.
What Somebody Starting Now Has
All of the above, plus something genuinely new.
A person learning today can describe a symptom in plain language and get a straight answer. "My game flickers when things move" produces the words double buffering, an explanation of why it happens, and example code, in one exchange. The bottleneck that cost me ten betas has essentially been removed. That is a wonderful thing and not a loss. Nobody learned anything valuable from those ten betas that they couldn't have learned in the eleventh.
A learner today can also read the source of the engines they use, run code in a browser without installing anything, publish to an audience the same afternoon, and get feedback from people making the same things. Every one of those took me years to arrange or never happened at all.
And the loop closed. Reading stopped being batch mode. A question that occurs while reading gets answered while reading, instead of being written down and carried back to school on a disk. Guessing correctly about what would be useful before knowing what it said is a skill nobody needs any more, which is exactly as it should be, because it was never the interesting part.
The change that matters most isn't any single tool, though. It's that where somebody lives stopped determining what they can learn. A teenager in that same town today has the same access as one in a capital city, which was emphatically not true at 14 kilobits, and is the largest thing that has improved about this in twenty years.
That should stop anyone who teaches or hires. The talent was never concentrated in cities. The copper was, and a decision about how to share it decided what a hundred people could learn for the next ten years.
The one thing that doesn't come with any of it is the reason to finish. Thirteen numbered releases, a changelog written for six named friends, and a features list separating done from planned were not requirements of the tooling. They were the part I invented because releasing to people I knew felt like what making a game meant.
That instinct is the only thing in this archive I'd tell somebody to copy deliberately, because it's the one thing the modern setup makes easier to skip. When distribution is free and instant, shipping stops being an event, and it was the event that made me write down what changed and why. Somebody starting now can build in a week what took me a year, which means the interesting question stops being whether they can and becomes whether they'll bother to finish and tell anyone.
What the Archive Is Actually For
Every technique in this file has a better version somewhere later. The pool became a ring buffer. The hand-declared GDI became a render backend behind an interface. The per-frame probability became delta-scaled. The changelog became a release process.
What the old version shows is which problems I found on my own, and the answer is most of them. Pooling, buffering, externalizing assets, cutting content to fit a budget, and giving the player a performance option are all here, all reasoned out, all expressed with whatever was to hand.
The tools were terrible. Dragging forty-six rectangles onto a form is not a technique. But a seventeen-year-old in a town of a hundred people, with a copy of Visual Basic, an MSDN disc, and no idea what any of it was called, got to the right shape of nearly every answer. That's a more encouraging thing to find in an archive than good code would have been, and it's the part I'd want somebody starting today to take from it: the answers are reachable in an afternoon now, and reaching for them is still the whole job.