I’m kinda disappointed at the sheer lack of platform games with slopes in them which aren’t Game Maker / Unity hackjobs. So, I’ll explain how they work in Cave Wanderer and to some extent This Is Totally About Evolution.

As I’ve coded both of these games in assembler (Z80 for CW, 68000 for TITAE), and in neither case have I had a floating point unit, I’ve been slightly cheap wrt slopes, simply not affecting the X coordinate.

Anyhow, the most important detail: tile types.

Each tile type has these two values:

- flags – Flags
- clampy – Maximum in-tile Y clamp value

And of course, flags is a bunch of boolean true/false values typically stored as a bit mask in some byte/doublebyte/quadbyte integer. (Not using the term “word” – that’s CPU-specific.)

So what we end up doing is a chain of functions doing something like this:

void tile_do_step_1(tiletype_t *tt, int *x, int *y) { if(tt->flags & TF_STEP1) { /* apply step 1 here - f(v) */ tile_do_step_2(tt, x, y); /* mathematically unapply step 1 here - g(v) */ } else { return tile_do_step_2(tt, x, y); } }

Note, x and y are the x,y coordinates relative to the top-left corner of the tile. Or whatever corner you choose for practicality. Anyhow, those are *pointers* to x and y, mostly because you want to screw with them all the way up and then all the way down again.

Heck, if you really wanted to, you could make them global variables “col_x” and “col_y” or whatever sane name you want to give them. Just be very careful if you decide to start multithreading stuff. Or you could just, y’know, pass the entity along instead.

Having a mathematical inverse is absolutely crucial here – in other words, for every vector v, g(f(v)) = v (or at the very least g(f(g(f(v)))) = g(f(v)) if you can’t ensure that (EDIT 2013-08-19: adjusted the latter constraint)). All inverses I’ve used in CW and TITAE are very straightforward. Let’s list them.

- TF_SWAPXY: f(x,y) = <y,x> = g(x,y)
- TF_INVX: f(x,y) = <tile_width-1-x,y> = g(x,y)
- TF_INVY: f(x,y) = <x,tile_width-1-y> = g(x,y)
- TF_SLOPE1: f(x,y) = <x,y+x>; g(x,y) = <x,y-x>
- TF_SLOPE2: f(x,y) = <x,y+x/2); g(x,y) = <x,y-x/2>

Of course, if you want a physically-accurate slope, you’d probably want to then divide the inverses there by some numbers I can’t quite recall the values of, but are basically the lengths of whatever the hell kind of vectors you want to shove in there.

I was experimenting with that in CW but the experiments proved to be a pain in the ass so I just went with what you see above (minus TF_SLOPE1, of course).

Anyhow, the final step is just this:

void tile_do_step_last(tiletype_t *tt, int *x, int *y) { if(*y >= tt->clampy) *y = tt->clampy; }

…that really wasn’t hard, was it?

Of course, you then need to know what to do with these values. Simple: you give an entity and some relative coordinate to a function that goes a little something… like this:

void apply_tile_collision(entity_t *e, int rx, int ry) { int bx = (e->x + rx) % tile_width; int by = (e->y + ry) % tile_height; int x = bx, y = by; tile_do_step_1(get_tile(x/tile_width, y/tile_height), &x, &y); e->x += x - bx; e->y += y - by; }

Call that a bunch of times for any point you like, and… that’s it really. Although you can then send across some special flags for each point, like “this is on the bottom therefore I should be counted as on the ground and able to jump”. TITAE uses 8 points for the player: bottom middle, top middle, left middle, right middle, all 4 corners.

Anyhow, the way the tiles are done in CW is something like this:

- There are “top” (TF_INVY) and “bottom” (no flags) tiles. clampy is about half way down the tile.
- There are “left” (TF_SWAPXY) and “right” (TF_SWAPXY,TF_INVY) tiles. clampy is 0.
- There are “clear” (clampy=tile_height*2) and “solid” (clampy=0) tiles.
- Slopes are denoted with TF_SLOPE2, have selected clampy values, and tend to have TF_INVX in one direction and not in the other.

And that’s Slopes 101. Any questions?

Tags: tutorial

Thanks for this post. It’s a little heavy without some explaining pictures I guess. Will have to read it again and slowly to understand everything I guess. But it’s wonderful to have someone explain this!