DEV Community

JoeStrout
JoeStrout

Posted on

HWYDT: Donkey Kong

This is the first in a series of posts where I'll be addressing the question:

How would you do that?!

In today's post, we'll be looking at the classic arcade game Donkey Kong.

Screen shot of Donkey Kong

Like many video games of its day, Donkey Kong consists of sprites (Mario, Donkey Kong, the barrels, the princess, etc.) and tiles (the environment). Not coincidentally, this is just like Mini Micro! But hang on — most of the girders in Donkey Kong are sloped! How would you do that?

The secret of the tiles

If we zoom in on the Donkey Kong screenshot and add grid lines every 8 pixels, we begin to see the secret.

Zoomed-in Donkey Kong screen shot, with grid lines

Here you can see that the environment really is a tile display — there's a clear periodicity to it, every 8 pixels. But where the girders are sloped, we must have other tiles in our tile set, that are shifted versions of the basic girder tile. We would need, in fact, eight girder-tops, and eight girder-bottoms. We'll also need versions with ladders above and ladders below. With those, we could build everything we see here.

A bit of googling for "Donkey Kong arcade tiles" led me to a website called The Spriter's Resource, where someone had already extracted a Donkey Kong tile set. But it had a lot of wasted space, so I got out an image editor and boiled it down to this:

dk_tiles.png, a tile set for this demo

If you're following along at home, save the above image as dk_tiles.png. Then fire up Mini Micro and let's get coding!

Using the Tile Set

Whenever working with a tile display, the first step is to make sure you know what your tile indexes are! To save time, I've prepared a blown-up version of our tile set, with indexes overlaid:

tile indexes for dk_tiles.png

As you can see, the top-left tile is index 0; the one next to it is index 1, and so on. A basic girder is at index 16 and 32; the next seven after each of these step down by one pixel each. (The set starting at index 16 have ladders above them, while the set starting at 32 do not). Girder-bottoms are found at indexes 24 through 31.

I made this tile set 64x128 pixels, because 64x64 wasn't quite big enough, and computers like powers of 2. So there is plenty of unused space at the bottom for you to add your own tiles, if you decide to pursue this further.

Now that we have an orientation to the tile set, let's start coding! Launch Mini Micro, edit, and put in the following code.

clear
display(5).mode = displayMode.tile
td = display(5)
td.tileSet = file.loadImage("dk_tiles.png")
td.tileSetTileSize = 8
td.cellSize = 32
td.extent = [28, 32]
td.clear

makeFlat = function(x0, x1, y)
    for x in range(x0, x1)
        td.setCell x, y, 16
    end for
end function

makeRampUp = function(x0, x1, y0, startLevel)
    level = startLevel
    for x in range(x0, x1)
        td.setCell x, y0, 32 - level
        if level > 0 then td.setCell x, y0+1, 40 - level
        if abs(x-x0) % 2 == 0 then continue
        level += 1
        if level > 7 then
            y0 += 1
            level = 0
        end if
    end for
end function

makeFlat 0, 13, 0
makeRampUp 14, 27, 0, 1
makeRampUp 25, 0, 3, 4
makeRampUp 2, 27, 7, 6
makeRampUp 25, 0, 11, 7
td.setCell 10, 1, 8  // (ladder)
Enter fullscreen mode Exit fullscreen mode

The first eight lines or so just set up the TileDisplay. Note that tileSetTileSize is set to 8, because that's how big the tiles are in our tile set. But given Mini Micro's much higher-resolution display, we set the cellSize on screen to 32. This effectively scales up the tile display by a factor of 4 (32 divided by 8).

Then we define a couple of helper functions for building the level. makeFlat is simple; it just makes a straight row of tile 16 (a basic girder). The makeRampUp function is a little more complex; it keeps a "level" from 0-8 that each girder is shifted relative to the basic girder, and advances this every other column, so we get nice gradual ramps as in the original game.

Finally, the last half-dozen lines use these functions (plus an extra call to setCell to build the level. If you've done all this correctly, running your code should produce this:

Screen shot of basic level from above code

Note that rather than building the level in code, you could use a level editor like /sys/demo/levelEditor to build the level by hand. That's probably more like what the Donkey Kong developer did; each level is probably just stored in ROM and loaded onto the screen. But either approach is fine.

Adding a Player Sprite

A game level isn't much fun if you can't play in it! So let's add our favorite platformer character, Kip. We'll make use of the /sys/lib/spriteControllers library to do most of the heavy lifting for us. edit the code again, and add this line to the top of the file:

import "spriteControllers"
Enter fullscreen mode Exit fullscreen mode

Then scroll to the bottom, and add this code:

// Set up our player character, "Kip"
newAnim = @spriteControllers.newAnimation
kip = new spriteControllers.Platformer
display(4).sprites.push kip
kip.x = 400; kip.y = 80
kip.idleAnim = newAnim(file.loadImage("/sys/pics/KP/KP-stand.png"))
kip.runAnim = newAnim([
   file.loadImage("/sys/pics/KP/KP-run1.png"),
   file.loadImage("/sys/pics/KP/KP-run2.png")], 10)
kip.jumpUpAnim = newAnim(file.loadImage("/sys/pics/KP/KP-jump.png"))
kip.fallDownAnim = kip.jumpUpAnim
kip.climbAnim = newAnim([
   file.loadImage("/sys/pics/KP/KP-climb1.png"),
   file.loadImage("/sys/pics/KP/KP-climb2.png")], 10)
kip.curAnim = kip.idleAnim

// Main loop:
while true
    spriteControllers.updateSprites 1/60
    yield
end while
Enter fullscreen mode Exit fullscreen mode

Run again, and you should have Kip hanging out on the bottom of the screen. Using the arrow keys and spacebar, you can make Kip run around and jump — but he passes right through the girders, ignoring them entirely.

Interacting With the Girders

For our last, and perhaps most crucial, trick, we need to get Kip to walk on the girders — including the sloped ones!

If your last test is still running, press Control-C to break out of it. Then edit again, and insert the following code before the main loop:

// determine where the ground is below the given position
kip.groundBelow = function(x,y)
    col = floor(x / 32)
    row = floor(y / 32)
    while true
        if row < 0 then return 0
        tile = td.cell(col, row)
        if 16 <= tile <= 23 or 32 <= tile <= 39 then
            return (row+1) * 32 - (tile % 8) * 4
        end if
        row = row - 1
    end while
end function
Enter fullscreen mode Exit fullscreen mode

This defines how Kip finds the ground below him. The given x,y position is in pixel coordinates. We start by dividing those by 32 — the size of our TileDisplay cells on screen — to find the row and column. Then comes a little loop, reducing the row by 1 each time until we find a non-null tile in our display.

Then comes the real magic: if the tile we find is in the range 16-23 or 32-39 — which, you might recall, are grid tops in our tile set — then we can calculate the correct Y position of the ground with this math:

(row+1) * 32 - (tile % 8) * 4
Enter fullscreen mode Exit fullscreen mode

Here, (row+1) * 32 gives us the top of the tile row; but then we subtract off an additional amount, (tile % 8) * 4, to account for how far the girder is shifted down in this particular tile. % is the "mod" operator, and gives us a value from 0-7, based on how far our tile index is beyond 16 or 32 (or any multiple of 8 for that matter). And then we multiply this by 4, because of the 4-to-1 scaling of our TileDisplay.

So save and run that, and you should be able to walk and jump on the girders perfectly!

Animation of Kip running around on the sloped girders

Conclusion

So now you know the secret of Donkey Kong: those apparently sloped girders are just a bunch of shifted girders, carefully arranged side by side, with a bit of math to figure out how Mario should walk on them.

This was the first game to do something like this, and must have caused quite a stir; nobody had seen a tile engine used like this, creating a strong illusion of a non-rectangular environment. And now that you know how it's done, you can use the same trick in your own Mini Micro games!

Top comments (8)

Collapse
 
joestrout profile image
JoeStrout

Hey there! Did you find this interesting or helpful? Are there any other games that make you think "How would you do that?" Let me know, and I'll follow this up with more posts delving into whatever mysteries you find!

Collapse
 
sebnozzi profile image
Sebastian Nozzi

Yes! Something comes to mind ... might be easy, but maybe not obvious - and at least an interesting problem: climbing up ladders. How to detect when there is one, how to animate going up / down, and when (and how) to stop and be on the ground again. What do you think?

Collapse
 
joestrout profile image
JoeStrout

/sys/lib/spriteControllers actually makes this pretty easy, but I felt it was too much to cram into this post. Sounds like we need a follow-up post using the same tileset, but focused on the ladders! Thanks for the idea.

Thread Thread
 
sebnozzi profile image
Sebastian Nozzi

It could evolve into a series analyzing the mechanics of Donkey Kong and implementing them in Mini Micro ...

Collapse
 
treytomes profile image
Trey Tomes

I was just about to ask this very same question about the ladders. :-) I'd love to see a follow-up post here.

Collapse
 
sebnozzi profile image
Sebastian Nozzi

Great post! And "the secret of Donkey Kong" almost sounds like the title of a new game :-D

Collapse
 
sebnozzi profile image
Sebastian Nozzi

I'm curious: what did you use to write the tile-numbers (indexes) ...? Because I assume you used some sort of automation here ...

Collapse
 
joestrout profile image
JoeStrout • Edited

Haha, no, I confess to using a drawing program, and manually typing each of those 88 numbers!

For tile sets where the tiles are bigger, setting up your TileDisplay (td) and doing view td.tileset shows the image with all the numbers overlaid. But when the tiles are only 8x8 pixels, like these, they're too small — the numbers all overlap and are illegible!