SGDK. Creating a platformer for Sega Genesis.

Preface.

Download the finished project file with MEGA.

In this tutorial, we will create a platformer on SGDK. Yes, so quickly, and immediately into fire, let’s get started.

What will be required for the platformer.

And you will need the following:

  • Place the player’s sprite in the center of the screen.
  • Scroll the world map depending on the player’s coordinates.
  • Stop the map if the player on the tile grid (add a collision).
  • Give to player gravity (just add +1 to y, each frame).
  • Allow the player to jump (if on the ground, and button pressed, then take 10 from y).

Don’t be intimidated by the extensive list of items, only significant point is:

  • Stop the map if the player on the tile grid (add a collision).

The rest, we either studied, or they are not difficult to guess. And now the question, how do we know which tile on the map is solid?

We understand the array of collisions.

An array of collisions comes to the rescue.

I created this picture for the SGDK article. Move around the map. But it didn’t fit to that lesson, so I reworked it.

An array of collisions is a 2-dimensional array that stores information about the solid tiles. For example:

  • 0solid tile(can’t go through)
  • 1transparent tile(can go through)

Using the coordinates of the player, you can find out the tile on which the player stands. This is how it is done.

u16 x_tile = x_pos / 8; // so
u16 x_tile = x_pos >> 3; // or so

The 1st and second lines are identical, but the 2nd line is preferable because it works faster. The player’s position is divided by 8, because the tile is 8×8 px.

Knowing the coordinates of the tile, you can substitute them in the array of collisions (level), and find out solid tile, where player stands, or not. This is done according to this template.

level[y_tile][x_tile]

x_tile, y_tile – the tile on which the player stands.

That is, if the level[y_tile][x_tile] returns 0, then you can’t pass, if 1, then you can pass.

Next, we will need a Python script that will generate an array of collisions from image. I wrote the script by myself, for this task.

Generating an array of collisions.

First, download script from MEGA.

And drag the picture with the map to the col_generator.exe, the script will generate necessary files.

An alternative way. Run the col_generator.exe through console, with the following arguments.

col_generator.exe image [background_color_hex]
  • image – the name of the image, with the extension.
  • background color_hex – background color in hex format. You can not specify it, then the first color in the palette will be used. If the tile contains nothing but background color, then the script understands that the tile is transparent. Otherwise, solid.

Place the script in the image folder (outside the project), then run the script with the following arguments.

col_generator.exe bg.png #005500

The image bg.png is located in the res folder of the project.

After launching, script will generate 2 images and a text file.

In the picture bg_WithCollision.png, solid tiles are marked in red.

The bg_array.txt file stores an array of collisions, and the size of the map in pixels, which, in the future, will be needed to constrain camera.

Array of collisions received, now let’s analyze platformer code.

Platformer resources.

In the res folder, there is a player’s sprite, and a map of the world.

I made the player sprite like a rectangle, which would make it easier to see the collision area.

  • resources.res – we discussed in previous articles.
  • resources.h – created automatically, at compile time.

Platformer code.

Open main.c

We are greeted by the huge array of collisions generated earlier.

Next, there are the structures with player’s variables.

typedef struct {
	f32 x;
	f32 y;
} Point;

typedef struct {
	bool moving;
	bool is_on_floor;
	Point pos;
	Point spd;
} Player;

Player structure consist of:

  • Is the player moving?
  • Player on the ground?
  • Player position
  • His speed

The structures were versed in SGDK. Moving a bunch of sprites.

In the main function, fill in the player variables with.

plr.moving = FALSE;
plr.is_on_floor = FALSE;
plr.pos.x = FIX32(160);
plr.pos.y = FIX32(950);
plr.spd.x = 0;
plr.spd.y = 0;

Next

SPR_init();
VDP_setPalette(PAL3, spr_cup.palette->data);
setCameraPosition(fix32ToInt(plr.pos.x),fix32ToInt(plr.pos.y));
setCameraPosition(fix32ToInt(plr.pos.x)-1,fix32ToInt(plr.pos.y)-1);

cup_obj = SPR_addSprite(&spr_cup, fix32ToInt(plr.pos.x), fix32ToInt(plr.pos.y), TILE_ATTR(PAL3, 0, FALSE, FALSE));

SPR_update();
  • Inalitize the sprite engine SPR_init
  • Call setCameraPosition twice to get the picture in the first frame.
  • Add the sprite to the screen SPR_addSprite.
  • Make it visible SPR_update

Setting sprite position and scroll map are in setCameraPosition.

void setCameraPosition(s16 x, s16 y)
{
    if ((x-160 != camPosX) || (y-120 != camPosY))
    {
        camPosX = x-160;
        camPosY = y-120;
		
if (camPosX < 0){
		camPosX = 0;
	} else if (camPosX > MAX_X-320) {
		camPosX = MAX_X-320;
	}		
	if (camPosY < 0){
		camPosY = 0;
	} else if (camPosY > MAX_Y-120) {
		camPosY = MAX_Y-120;
	}	
	SPR_setPosition(cup_obj, x-camPosX, y-camPosY);
        MAP_scrollTo(bga, camPosX, camPosY);
    }
}

Let’s analyze this function

camPosX = x-160;
camPosY = y-120;

The position of the camera is shifted to minus relative to the coordinates of the player, so player in the center.

if (camPosX < 0){
	camPosX = 0;
} else if (camPosX > MAX_X-320) {
	camPosX = MAX_X-320;
}		
if (camPosY < 0){
	camPosY = 0;
} else if (camPosY > MAX_Y-120) {
	camPosY = MAX_Y-120;
}	

Limit the camera within the map.

SPR_setPosition(cup_obj, x-camPosX, y-camPosY);
MAP_scrollTo(bga, camPosX, camPosY);

Set the position of the sprite relative to the coordinates of the camera. This design allows you to move the sprite from the central position, when approaching the boundaries of the map.

Let’s move on to the collision check.

Let’s analyze the collision code.

bool checkCollision(s16 x, s16 y)
{
	plr.is_on_floor = FALSE;
	
s16 y_tile = y >> 3;
	s16 x_tile = x >> 3;
	
u8 player_width = 4;
	u8 player_height = 4;
	
s16 leftTile = x_tile;
	s16 rightTile = x_tile+player_width;
	s16 topTile = y_tile;
	s16 bottomTile = y_tile+player_height;
	
for(s16 i=leftTile; i <=rightTile; i++)
	{
		for(s16 j=topTile; j <=bottomTile; j++)
		{
			if(level [j][i] == 0) {
				if(j == bottomTile){
					plr.is_on_floor = TRUE;
				}
				return FALSE;
			}
		}
	}
	return TRUE;
}
  • Here, we correlate the tile on which the player stands (y_tile, x_tile) and because the player takes not even 1 tile, but 4.
  • We check every tile that the player occupies in the loop, and if the tile is solid (level [y][x] == 0),then we return FALSE (you can’t pass).
  • If you on tile (level [j][i] == 0 ) and tile under the player ( j == bottomTile), then player is on ground ( plr.is_on_floor= TRUE).
  • In other cases, the player can pass.

Now, let’s analyze moveEntity, it is this function that is responsible for the movement of the player.

If player moved with a speed multiple of the size of the tile (1, 2, 4, 8) and he strictly on the coordinates of the tile, then, you could shorten the moveEntity code to:

void moveEntity(){
	s16 posX = fix32ToInt(plr.pos.x);
	s16 posY = fix32ToInt(plr.pos.y);

s16 spdX = fix32ToInt(plr.spd.x);
	s16 spdY = fix32ToInt(plr.spd.y)+2; // Previously, there was no +2, and the player could get stuck on the ledge, oddly enough, the crutch picked up at random, saved the situation.

	if(checkCollision(posX+spdX, posY+spdY)) {
		posX += spdX;
		posY += spdY;
	}
	plr.pos.x = FIX32(posX);
	plr.pos.y = FIX32(posY);
}

Here is, if there is no conflict in the future position of the player (posX+ spdX), then, we move to the future position, otherwise we stand still.

But, this implementation is not suitable for platformers.

A heapsome block else comes to the rescue. Which mainly consists of copy-pasta.

Here, I will use a terribly un-optimized algorithm, because if I work on the correct version, the release of the article will be delayed until the second coming of Christ. So, let’s get started.

Check player’s path to the collision.

Let’s analyze the else code.

if (spdX) {
  testPosX = posX;
  if (spdX > 0) {
    for(u8 i=1;i<spdX;i++){
      testPosX++;
      if(checkCollision(testPosX, posY)) {
      posX = testPosX;
          } else {
            break;
          }  
        }
      } else {
        for(u8 i=spdX;i>0;i++){
          testPosX--;
          if(checkCollision(testPosX, posY)) {
            posX = testPosX;
          } else {
            break;
          }  
        }
      }
    }

If the player moves left or right (spdX), then check the position of the player, in range from posX to spdX.

for(u8 i=1;i<spdX;i++){
      testPosX++;
      if(checkCollision(testPosX, posY)) {
      posX = testPosX;
          } else {
            break;
          }  
        }

Exactly until the moment when the player’s coordinate does not encounter a collision. The final coordinate of the player, stored in testPosX, testPosY. It equates to the position of the player.

plr.pos.x = FIX32(posX);
plr.pos.y = FIX32(posY);

In the end, change the coordinates of the player.

Let’s analyze input code.

Control is set in handleInput:

void handleInput()
{
    u16 value = JOY_readJoypad(JOY_1);
    if (!paused && !plr.moving)
    {
		
plr.spd.x = 0;
		
if(!plr.is_on_floor){
			plr.spd.y += FIX32(1);
		} else {
			plr.spd.y = 0;
		}
		
if (value & BUTTON_RIGHT)
        {
			plr.spd.x = FIX32(5);
		}
        else if (value & BUTTON_LEFT)
        {
			plr.spd.x = FIX32(-5);
		}
        if (value & BUTTON_UP)
        {
			if(plr.is_on_floor){
				plr.spd.y = FIX32(-10);
			}
        }
    }
}
  • If the player is not on the floor, then apply gravity, otherwise – not apply.
  • Change speed of the player, when you push the buttons to the right or left
  • Jump if the button is pressed up and the player is on ground.

The last thing left is to cause all these lines in the main cycle.

while(1)
{
	SPR_update();
	handleInput();
	moveEntity();
		
VDP_showFPS(FALSE);
	setCameraPosition(fix32ToInt(plr.pos.x),fix32ToInt(plr.pos.y));
        
SYS_doVBlankProcess();
}

Everything here, you already know.

Conclusion.

So we created a basic platformer, without monsters and scenery, with one single level and without sound. In the next articles we will fix it.

(Bonus) Correct collision code.

Instead of checking all the way from posX to posX+spdX. It is much easier to find out the position of the tile (in pixels) with which you encounter, and subtract the coordinate of the player from it.

pos_x += (coll_tile_x*8) - posX; //not so good solution
pos_x += (coll_tile_x<<3) - posX; //very good solution

So far, I haven’t been lucky eroded to implement such implementation. If I understand, I’ll update the article.

Special thanks.

Thanks to:

Final result.