HWYDT: Ladders

JoeStrout - Jan 17 - - Dev Community

Last week, we looked at how you would do sloped platforms like in Donkey Kong. People loved it, but they wanted to know:

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.

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

How Would You Do That: Ladders?

So it turns out that the Platformer class in /sys/lib/spriteControllers already handles most of the messy details of climbing. If you define a climb animation, then by default your character can climb anywhere. We defined just such an animation in our little demo:

kip.climbAnim = newAnim([
   file.loadImage("/sys/pics/KP/KP-climb1.png"),
   file.loadImage("/sys/pics/KP/KP-climb2.png")], 10)
Enter fullscreen mode Exit fullscreen mode

Try it! If you don't have last week's code handy, you can twist open this section and copy it into Mini Micro:

Last week's code (click the little triangle to see)
import "spriteControllers"

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

// Set up the level
makeFlat 0, 13, 0
makeRampUp 14, 27, 0, 1
makeRampUp 25, 0, 3, 4
makeRampUp 2, 27, 7, 5
makeRampUp 25, 0, 11, 6
td.setCell 10, 1, 8  // (ladder)

// Set 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

// determine whether (and where) kip has solid ground below him
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

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

run that code, and then just press the up-arrow key anywhere. You'll find Kip happily climbs up (and down) nothingness.

screen shot of Kip climbing nothing

Adding ladders

Last week we hacked in a single ladder tile, just for decoration. This week, we want more! Again we'll copy the original Donkey Kong layout.

edit the program, and scroll down to right below the makeRampUp function (right above // Set up the level). Insert this new function to create a ladder:

makeLadder = function(x, y0, y1=null)
    if y1 == null then y1 = y0
    for y in range(y0, y1)
        c = td.cell(x, y)
        if 24 <= c <= 39 then
            td.setCell x, y, c - 16
        else
            td.setCell x, y, 8
        end if
    end for
end function
Enter fullscreen mode Exit fullscreen mode

This function takes the X (horizontal) position of the ladder, and one or two Y (vertical) positions for the ladder extent. It iterates over that Y range, and sets a ladder. But what exactly is going on with that if statement?

To understand that, we need to refer back to our tile set:

tile indexes for dk_tiles.png

You can see that tiles 24 through 39 are the partial-girder tiles, places where we have just the top or bottom of a girder (in order to make sloped platforms). How do we modify those to have a ladder? As the tile set is arranged, the proper girder+ladder section is always 16 tile indexes before the girder-only tile. So, if we find the existing cell is in this range, we subtract 16. If the existing cell is anything else, then we just set it to index 8, which is a full ladder piece.

Now replace the next part of code, which sets up the level, with this:

// Set up the level
makeFlat 0, 13, 0
makeRampUp 14, 27, 0, 1
makeRampUp 25, 0, 3, 4
makeRampUp 2, 27, 7, 5
makeRampUp 25, 0, 11, 6
makeLadder 10, 1; makeLadder 10, 4
makeLadder 23, 1, 3
makeLadder 12, 5, 8
makeLadder 4, 5, 7
makeLadder 8, 9; makeLadder 8, 12
makeLadder 14, 9, 12
makeLadder 23, 9, 11
Enter fullscreen mode Exit fullscreen mode

When you run this, you should see a more complete level with ladders, as shown below.

Level with ladders

Climbing on ladders only

To limit where our character can climb, we need to override the canClimb method on our character class. To prove that this works, edit your code, scroll down below the kip.groundBelow function (right above the while true loop), and insert this code:

kip.canClimb = function(direction)
    return false
end function
Enter fullscreen mode Exit fullscreen mode

Now run the program again, and you should find that Kip can't climb at all (even though he still has a climb animation defined).

So the job of this method is to return true when kip can climb, given his current location and the intended climb direction (+1 to climb up, or -1 to climb down).

Now if we didn't have partial-girder, partial-ladder tiles, and if ladders extended through the girders, then implementing this function would be fairly trivial: we'd only need to check if the tile at our feet is the ladder tile. But neither of these things is the case; ladders in Donkey Kong stop below the girder they attach to, and because of slopes, we have to deal with funny Y coordinates depending on which particular tile we have. All that makes our canClimb method a bit longer. But here it is:

kip.canClimb = function(direction)
    col = floor(self.x / 32)
    row = floor((self.y - self.footOffset) / 32)
    c = td.cell(col, row)
    if (c == null or c < 8 or c > 23) and (
        direction < 0 or self.state == spriteControllers.CLIMBING) then
        // no ladder here, if either we're going down, or we're 
        // already climbing, then also check the tile right below it
        row -= 1
        c = td.cell(col, row)
    end if
    if c == null or c < 8 or c > 23 then return false
    // OK, there's a ladder in this cell; but account for slopes
    // to figure out exactly where it ends.
    climbStep = self.climbSpeed / 60
    if 16 <= c <= 23 and direction < 0 then  // ladder on top of a girder
        minY = self.groundBelow(self.x, self.y)
        if self.y - self.footOffset < minY + climbStep then return false
    else if 9 <= c <= 15 and direction > 0 then // ladder below a girder
        maxY = self.groundBelow(self.x, (row + 1) * 32)
        if self.y - self.footOffset + climbStep*2 > maxY then return false
    end if
    // center ourselves horizontally on the ladder
    self.x = (col + 0.5) * 32
    return true
end function
Enter fullscreen mode Exit fullscreen mode

Let's walk through how this works. First, we get the column and row of our feet, and check the cell at that position. If it's not a ladder cell (between 8 and 23), and if we're either going down or we're already climbing, then we check the tile below. That deals with the case at the top of a ladder, where we're actually on a girder tile but we still want to consider it climbable.

Then, we have two if blocks that make adjustments for slopes (partial girders). The first one, checking for a tile index from 16 to 23, deals with a ladder section on top of a girder (which occurs at the bottom of a ladder). We make use of the groundBelow function from last week to figure out where the actual girder part is, in pixel coordinates. If our climb down would exceed that, we report false ("can't climb").

The else if block, checking for a tile index from 9 to 15, handles the opposite: this is the girder on top of the ladder, and again we use groundBelow (starting up one row) to figure the maximum Y we should allow the player to climb.

The final part of this method just centers the player on the ladder. You could comment this out if you want to see the effect: without it, you can climb up the side of a ladder, which looks a little funny.

This method was developed iteratively, which is to say, I coded a little, tested a little, and repeated until I was satisfied. That's likely to be the case in your own game, too — getting the feel just right often takes a little experimentation.

That's all, folks!

And that's it! Once you have that canClimb method properly defined for your game, the library code handles the rest. You should now be able to run, jump, and climb around your level!

Animated GIF of Kip climbing around our Donkey Kong level

As you can see, making a run/jump/climb platformer in Mini Micro is much easier than you'd probably expect (even with sloped platforms!). Why not try making your own game?

Also, I'd like to continue to explore how classic video game effects were done, all on hardware considerably less powerful than Mini Micro. So keep your eye out for things that make you go "hmm," and let me know in the comments below if anything makes you say: How would you do that?!?

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .