jnrdev - the 2d jump'n'run tutorial series
jnrdev #2 - hills & slopes - Florian Hufsky

1. Introduction

welcome to the second issue of jnrdev, in this tutorial we'll add a new type of tiles to to the game from jnrdev #1: hills and slopes.

note: through the whole tutorial slope will be used for slope or hill (hills and slopes are the same, it's just a point of view).

2. Theory

to check for slopes we'll use a technique i developed for this tutorial:

"slope slope axis axis" collision detection (made-up-name(TM))

i often write it down as:
s
  s
a
a
because the second slope testing is only used for "moving from slope to slope" cases.
the slope collision detection also works without the second testing, but with it some jittering is avoided.

it's quite simple:

if( we entered a slope ){
 move player and set new y position
 end collision checking
}

else if( we left a slope ){
  set player y to the height of the slope on the side the player left it

  //this is the second slope testing
  if( we are now on a slope ){	//we're moving from slope to slope
   move player and set new y position
   end collision checking
  }
}

do normal seperate axis testing now (only if we're not on a slope)


we'll use this point to check if we are on a slope

there are three cases that can occur:

the player enters a slope:


after slope collision detection, no seperate axis collision detection is done, because we are now on a slope.


the player leaves a slope:


we aren't on a slope in this frame -> use seperate axis collision detection.


the player moves from slope to slope:


no seperate axis collision detection afterwards.

so we entered a slope, but how do we now the new y coordinate to make the player stand on the slope, not in it?

there are three approaches to this:

a) calculate the position
   easy to implement, but you can only have slopes basing on a (mathematical) function.

b) use a array to save the height of the slope at every pixel (-> int slopeheights[TILESIZE])
   nice, you can have different shaped slopes this way.
   but aligning y horizontal to the slope is difficult this way (but you won't need that anyway)

c) use pixel perfect collision detection
   cool, because the slope information is already stored in the image.
   but you have to lock the surface or copy the information to a seperate bitmask
   if you want to use this, use a seperate mask - surface locking is awful (slow) (especially when using openGL for 2d blitting)

i think c) and b) are the better methods, because you can have different shaped slopes.
but i'll use a), because it's easier to implement.

... ok, you got me - b) isn't harder to code, but i've already coded a) and am lazy - i'll improve the slope system in another tutorial.


this is how a) works:

i assume every slope is a 45° line (it simplifies things sooooo much)

3. Implementation

3.1 changes to jnrdev #1

CTile / CMap

CTile now uses
enum TileType{t_nonsolid, t_solid, t_slopeleft, t_sloperight};
instead of bool solid.

and CMap::map(...) is used instead of CMap::colmapxy(...) - it returns the TileType.

CPlayer

i've changed all functions but think(), draw(), CPlayer to private.
and i've added some new functions:

3.2 collision detection with slopes

i don't know what i should explain first, how to react on hitting a slope or how the whole slope thing works.
i think the big picture first would be better, but there are arguments against it, so read as you wish - either 3.2.1 first, or 3.2.2

3.2.1 what to do if we hit a slope

first we'll take a look at the function bool CPlayer::collision_slope(int sx, int sy, int &tsx;, int &tsy;).

it's used to determine if we hit a slope at sx, sy.
if we hit a slope tsx and tsy contain the tile coordinate of the slope,
the y coordinate is set to align with the slope
and tsx, tsy contain the tile coordinates of the slope.

bool CPlayer::collision_slope(int sx, int sy, int &tsx;, int &tsy;){	
	tsx = sx / TILESIZE;	//map coordinates of the tile we check against
	tsy = sy / TILESIZE;

	TileType t = map.map(tsx, tsy);
	
	//if we found a slope we set align y to the slope.
	if(t == t_sloperight){
		//sloperight -> \  
		y =  (tsy+1)*TILESIZE - (TILESIZE - (sx%TILESIZE))  - h - 1;
		return true;;
	}
	else if(t == t_slopeleft){
		//slopeleft -> /
		y =  (tsy+1)*TILESIZE - sx%TILESIZE  - h - 1 ;
		return true;
	}
	
	return false;
}

the most complicate part here is the aligning of the y coordinate:
example: we hit a t_sloperight:

now take a look how y this is calculated when hitting a t_sloperight:

y =  (tsy+1)*TILESIZE - (TILESIZE - (sx%TILESIZE))  - h - 1;

y =
(tsy+1)*TILESIZE		//y pixel coordinate of the ground of the tile
- (TILESIZE - sx%TILESIZE)	//minus how far sx is inside the tile (16 pixels in the exapmle)
- h				//minus the height (sx is located at the bottom of the player, but y is at the top)
- 1 (one)			//we don't want to stick in a tile, this would cause complications in the next frame
				//(ok, it won't because of my slope technique, but it will matter when improving it)

t_slopeleft works the same, but we don't need to substract sx%TILESIZE from TILSIZE because we're counting from the other side.

3.2.2 the whole slope collision detection

the slope collision detection is done before the seperate axis checking described in jnrdev #1.
if a collision with a slope occurs, the seperate axis checking is skipped.

//check for slopes (only if moving down)
if(vely > 0){
	int tsx, tsy;			//slope tile coordinates
	int sx = x + (w>>1) + velx;	//slope chechpoint x coordinate


	if(collision_slope(sx, (y + h), tsx, tsy) ){	//we entered a slope (y is set by collision_slope)
		x += velx;	//move on
		//y has been set by collision_slope
		
		unlockjump();	//we hit the ground - the player may jump again
		vely = 1;		//test against the ground again in the next frame

		slope_prevtilex = tsx;	//save slope koordinate
		slope_prevtiley = tsy;

		return;
	}
	else{				//we're not on a slope this frame - check if we left a slope
		//-1 ... we didn't move from slope to slope
		//0 ... we left a slope after moving down
		//1 ... we left a slope after moving up
		int what = -1;

		if(map.map(slope_prevtilex, slope_prevtiley) == t_sloperight){
			if(velx > 0)		//sloperight + velx > 0  = we left a slope after moving up the slope
				what = 0;
			else
				what = 1;	//we left after moving down the slope
		}
		else if(map.map(slope_prevtilex, slope_prevtiley) == t_slopeleft){
			if(velx < 0)		//sloperight + velx > 0  = we left a slope after moving up the slope
				what = 0;
			else
				what = 1;	//we left after moving down the slope
		}

		if(what != -1){		//if we left a slope and now are on a slope
			int sy;

			if(what == 1){
				y =  tsy*TILESIZE - h -1;	//move y to top of the slope tile
				sy = y + h;
			}
			else{
				y =  (tsy+1)*TILESIZE - h -1;	//move y to the bottom of the slope tile
				sy = y + h + TILESIZE;		//test one tile lower than the bottom of the slope
								//(to test if we move down a long slope)
								//it's physically incorrect, but looks a lot smoother ingame
			}

			
			//check for slopes on new position
			if( collision_slope(sx, sy, tsx, tsy) ){	//slope on new pos (entered a slope after we left a slope)
									//-> we moved from slope to slope
				x += velx;
				//y has been set by collision_slope

				unlockjump();
				vely = 1;

				slope_prevtilex = tsx;
				slope_prevtiley = tsy;

				return;
			}
		}	
	}
}

slope_prevtilex, slope_prevtiley is saved for checking if we were on a slope in the previous frame

4. problems with this approach

i'm not completely satisfied with this solution, because it has some errors.

but these occure in very rare cases and the solution would need quite some code rewriting, so i'll leave it like it is.

instead just tell your level designer, which things he isn't allowed to put in the levels, these are situations no one would expect in a game anyway.

when adding time based calculations in a later tutorial we'll need to fix the errors anyway :)


most tile combinations only cause jittering / being stuck in a tile for one frame. but this causes nasty level bugs (wall climbing, ...)

and there's another bug in the slope collision detection:


this happens, because we allign the y coordinate immediatly to the slope without checking if we really collide with the slope.

this bug won't affect gameplay, but i just don't like bugs.

5. the example

the source example is here: jnrdev2example.zip.

for running the game you will also need SDL.dll, which you can get here: SDL-1.2.7-win32.zip.

for compiling you need the SDL headers, which can be found here: SDL-1.2.7.zip.
note: you have to add some include / library directories if you use VC++, take a look at VisualC.html, which comes with SDL how to do that.

if the direct links to the sdl stuff don't work here's the official sdl site: www.libsdl.org.

6. That's it

i this topic (and my explanations mixed with my bad english) made this tutorial a bit confusing, but i think it worked out quite well in the end.

i know that there are still some flaws in the checking, but they don't affect gameplay and only appear in very rare cases.
i want to improve this system later, but first we'll build a map editor and bigger maps.

questions / critics / comments? forum


http://jnrdev.72dpiarmy.com © 2003-2004 Florian Hufsky