Preface
More than 20 years ago my teenage self had a dream. To be more exact, my teenage self shared the same boring dream with most of my friends. That dream was to create a video game for the Commodore 64. It sounds silly today, but that’s what every teenager wanted to do back in the 80s. You wanted to be an astronaut, a football player or a game developer. Not surprisingly, I did not make a career in space travel or any kind of professional sports and, although I went on to publish several games for modern platforms many years later, I didn’t make it my main occupation. Which is not something I regret now. What I really regretted was failing to write my own Commodore 64 game. I’ll spare you the details of what made me try to rectify that situation, although I guess it could simply be summarised by a friend of mine telling me that it is never too late (yes, it’s that simple), but anyway, fast forward a couple of decades and P0 Snake, my entry in the RGCD 16Kb game development competition, came first and will be published later this year. Better late than never, you silly daydreaming teenager!
In this and the next series of posts I’ll try to cover a few aspects of the development process that I think are worth sharing, especially those that may be of general programming interest. In fact, I’ll try not to make these notes inaccessible to someone who has never seen a Commodore 64 in his life, although I might be writing a couple of lines of assembler here and there (which I’ll try to explain in detail). Hopefully I’ll give some answers to those of you who are wondering how someone can even think of implementing something for a 33-year-old machine. And who knows? Maybe I’ll drag some of you into this madness. That would be great!
An appreciation of the limitations of the target platform
It’s no mystery that the Commodore 64 comes with 64KB of RAM. This might sound like a silly amount of silicon to achieve anything sensible nowadays, but it was not back in the 80s. In 1982, when the machine came out, 64KB of RAM was “A LOT” of memory. In fact, it was the best selling point of the machine. This is a very important fact to consider, because we are targeting this specific machine, not a modern PC. An assembler instruction on the Commodore 64 takes 1 to 3 bytes. 64KB could host a good 30,000 “lines” of code if we were to use all of that space for this purpose. Furthermore, old machines in general, and the Commodore 64 in particular, were designed to make video game development (well... certain aspects of it) “easy”… Programming a ball that bounces around the screen, for example, is something that can be achieved very easily, and it really takes few bytes to be implemented. If you want to play a sound effect that resembles an explosion, that’s easy too. This is because the Commodore 64’s strongest asset were its custom chips that allowed programmers to achieve certain effects without much effort. The most famous and one of the most useful aces up the C64’s sleeve are its mighty hardware sprites. Let’s go back to the bouncing ball example. This is what we want to achieve:
Now let’s write pseudocode to achieve this effect on a machine of that era with no Hardware sprites:
x = 0
y = 0
xdir = 1 //going right
ydir = 1 //going down
ball = {(x0,y0), (x1,y1), .. (xn,yn)} //a sequence of pixels making up the shape of the ball.
while(true)
{
foreach (pixel in ball)
{
screen[x + pixel.x, y + pixel.y] =
backbuffer[x + pixel.x, y + pixel.y]
}
x = x + xdir
y = y + ydir
if (x == 0 or x == rightborder)
{
xdir = -xdir //bounce horizontally
}
if (y == 0 or y == bottomborder)
{
ydir = -ydir //bounce vertically
}
foreach (pixel in ball)
{
screen[x+pixel.x, y+pixel.y] = ballcolour
}
somedelay()...
}
The first few lines set the ball position and direction. It will be initially positioned on the top-left corner of the screen. Each iteration of the loop will update the x and y position according to the direction. The problem here is what “update” means:
There are three main steps here:
- erase the pixels at position (x,y) + offset for each pixel offset in ball
- update the ball position (x,y)
- set all the pixels at position (x,y) + offset for each pixel offset in ball
Surprisingly, this is not very different from what a modern PC does. The fact that machines and graphic cards are so fast now, makes this process totally invisible to the user, but those of you who witnessed the birth of the first GUIs, will definitely remember that moving windows around the screen triggered that weird erase/redraw effect. That’s exactly what went on in PCs in the early nineties: the user moving a window would result in all the pixels making up the window at the old position being erased (background is drawn) and then all the pixels being redrawn in the new position. It’s blazing fast nowadays, not so much 20 years ago. And indeed, things would get much more complicated when you had multiple objects that moved on the screen, which is always the case in games, unless you are dealing with something as simple as a bouncing ball.
How would you achieve the same effect on the Commodore 64?
The commodore 64 comes with 8 Hardware sprites. A sprite is a shape that can be defined by the user and positioned anywhere on the screen. What’s great about sprites is that they exist on a different layer than the background. This means that upon moving them, you don’t have to worry about the background. Let’s see what the bouncing ball pseudocode looks like on the C64.
xdir = 1 //going right
ydir = 1 //going down
ball = {(x0,y0), (x1,y1), .. (xn,yn)} //a sequence of pixels making up the shape of the ball.
sprite0.pointer = ball
sprite0.x = 0
sprite0.y = 0
sprite0.visible = true
while(true)
{
sprite0.x = sprite0.x + xdir
sprite0.y = sprite0.y + ydir
if (sprite0.x == 0 or sprite0.x == rightborder)
{
xdir = -xdir //bounce horizontally
}
if (sprite0.y == 0 or sprite0.y == bottomborder)
{
ydir = -ydir //bounce vertically
}
somedelay()
}
And that’s it. We basically set up the sprite number 0 to represent the ball, then in the main loop we just update the position of sprite0 and take care of the bouncing, and good old commodore 64 takes care of everything else.
We spoke about efficiency, which is clearly the most obvious advantage of this approach, but there’s so much more going on under the hood. The erase-redraw approach that other computers are forced to resort to implies that you have to store the background in memory twice. Once for the clean, unaltered background that you need to copy the pixels FROM when you are erasing, and once for the actual displayed version of it, with the ball and all the other objects overlayed. The concept of double buffering is an integral part of game development today (and even the Commodore 64 provides an elegant way to implement it), but you really don’t want to resort to it, unless you really need it, to limit the footprint of your graphics into memory.
In our specific example, besides the memory that goes to waste for something that simple, a lot of cpu power is wasted in erasing and drawing back the same thing over and over. There are a lot of tricks that programmers of other platforms came up with in the 80s to make this process as quick as possible, but the basic approach stays the same: you have to erase and redraw your objects.
This video shows the bouncing ball implemented on a C64. As you can see the background is completely unaffected by the ball movement.
This long preamble was needed to put things into perspective: The 16Kb limitation of the competition is indeed a tight constraint, but things are not so bad after all. There is a good hardware support for some common tasks that allows you to achieve interesting results with limited memory. The pure horsepower of the machine is ridiculous compared to your mobile (let alone your computer) but the sheer amount of “things” that go on on the display is very limited, and even when things get very busy, the number of pixels that the machine needs to shift at a certain moment in time is one order of magnitude smaller than those that your mobile has to move around, say, when you unlock the screen. Yes, you can’t play the latest tv show on your stock Commodore 64 (well, some people will disagree with this, but let’s not make these posts too hardcore, shall we?), but you can move objects on the display and play some sound without having to implement the basics of that.
That’s why I’m not going to cover the aspects of how to squeeze a game in 16 kb on the Commodore 64, because in general that’s not really something to tell home about: there are literally thousands of videogames developed by pioneer coders in the 80s that are smaller than 16kb. Those unsung heroes deserve your respect because THEIR 16Kb creatures laid the foundation of the industry that brought you Halo, Fifa 2015 or whatever you guys are playing today. Nor I’m giving you an introduction to C64 coding, because there are dozen of tutorials on game programming for this machine (you’d be surprised!). Finally, I won’t introduce you to Commodore 64 programming at all, because this video does it better than I could dream of and is well worth your “I’m not slacking, my code is compiling”-time today. Check it out!
What the next posts will cover are those aspects of the development of P0 Snake that challenged and intrigued me the most. The hurdles I had to come to term with and the makeshifts that allowed me to overcome them. It’ll be about how we can conjugate modern tools and knowledge with vintage technology to craft something beautifully old. It’s not going to be a tutorial (I would never be that arrogant), nor a lesson in data compression (again). It’s just the way I did it, which is not necessarily the best way of doing it. But, heck, it worked!
Stay tuned.
canon support code 5b00
ReplyDelete