Skip to main content

Physics and scenery interactions

WORK IN PROGRESS!

This page is not quite ready for visitors yet. Please check again another time.

tip

đź’ˇ Remember to run docker pull retcon85/toolchain-sms from any Terminal window to check for the latest toolchain updates!

Also, if you haven’t already, download at least v1.3 of Aseprite.

Corrections​

Before we get started, let’s make a few changes to the previous exercises as I would like to rework them a little bit, most notably I want the “goal” to be a sprite instead of a background tile.

  1. Firstly, in Aseprite, we want to move the “goal” image to your sprites.asesprite file instead of background.aseprite. This is, in theory, a simple matter of copy and paste, although you might find that between your goal and your player you end up with more than 16 colours, which might mean you need to rework either one or the other to use fewer colours.

    Once you’ve got Aseprite v1.3, the best way to do this is to:

  2. Open your sprites.aseprite file and load the full palette of 64 Master System colours using the preset button:

    Untitled

  3. Remember to press “Remap Palette” after you do this.

  4. Copy your “goal” sprite from background.aseprite and paste it into sprites.aseprite in an empty 8x8 slot. You may need to first expand the canvas size by going to the Sprite → Canvas Size option, or just pressing the C key. When resizing the canvas be careful to position the existing image top left so none of the tile positions are altered. By default Aseprite will centre the existing image, so you will need to press the ⬅️ and the ⬆️ buttons when you change the canvas size.

  5. After you have your “goal” sprite in position, click the palette options (burger) menu and Select Used colours. This should highlight all the colours you’ve used, and you can then grab the edge of any one of them and move them to the top of the palette.

  6. Press “Remap Palette”

  7. Now you have a 64 colour palette, but with your used colours at the top, so you should easily be able to see if there are more than 16. If so, you will need to redesign your sprites to use fewer colours. Repeat steps d → f as necessary to get 16 colours.

  8. Once you have your 16 colours, make sure your transparency is in position 0 and your chosen border colour is where you need it to be (see day 2).

  9. If you want to, you can also now make your sprites fill the whole 8x8 — they only needed to be 7x7 when we were building the maze game. I suggest you at least make it so that your player is sitting on the baseline (so the empty row of pixels is at the top rather than the bottom of the 8x8 grid)

  10. Re-export the sprites.aseprite file to a .bmp.

  11. In the main.c file we’ll remove the lines which previous said

    // display a token tile near the bottom of the screen
    SMS_setTileatXY(15, 18, 1);
  12. Near the bottom of the file, after the call to SMS_addSprite we’ll add another line:

    // draw the goal sprite
    SMS_addSprite(120, 120, 1);

    This is assuming your goal sprite is the second sprite (i.e. sprite “1”) in your sprites.aseprite file. If not, replace the 1 with the index of the sprite, remembering that it starts from 0 for the first sprite in the file.

Physics​

So far we have been moving our sprite by directly modifying its position whenever a button is held down on the gamepad. In real life, motion is more complicated. There are three quantities which are important in describing motion:

  • Position which is the x and y we have already used to position the sprite in 2 dimensional space
  • Velocity which is the rate of change of position, i.e. how many pixels a sprite moves per frame
  • Acceleration which is the rate of change of velocity, i.e. is the movement of the sprite slowing down or speeding up

So far we've been modelling both horizontal motion and vertical motion as either zero (i.e. nothing moving) or a constant velocity (i.e. a steady speed, neither accelerating or decelerating). This isn't too strange for horizontal motion, but it's not at all intuitive for vertical movement where we expect gravity to pull an object down towards the ground.

Gravity acts like a constant acceleration. On Earth, a falling object's speed increases by around 10 metres per second, every second. This acceleration is trying to happen all the time, although if there is a solid object, like the ground, in the way you can't accelerate and you will stop moving.

In order to model gravity-like acceleration in our game, we'll need to track not just the position, but also the velocity of our player object. We'll also need to track velocity in two dimensions since they are completely separate components of an object's motion.

Since we need to hold more and more information about our player, instead of relying on simple variables (player_x and player_y), we’re going to start being a little more structured and use a struct to group our player data together.

Okay, ready to rock and roll?

Refactor to use a struct​

  1. We’re going to replace the two variables, player_x and player_y, so let’s delete the two lines which declare them, near the top of the main.c file.

  2. Where they were, let’s define a struct to hold some object information:

    struct Object
    {
    uint8_t x;
    uint8_t y;
    int8_t vx;
    int8_t vy;
    }

    Our Object struct declares four fields, x, y, vx and vy; corresponding to horizontal position, vertical position, horizontal velocity and vertical velocity respectively.

    Note that x and y are uint8_t types, which means they are unsigned 8 bit integers. This makes sense because screen positions can only be positive and the width of the screen is 256 pixels so 8 bits is just enough for us.

    On the other hand, vx and vy are int8_t types, and the lack of u means that they are by implication signed integers. This is important because velocity could be acting to the left or the right, so we need both positive and negative numbers. Because the fields are still 8 bits, that means we can express velocities from -128 to +127 with these numbers. That’s way more than we need — a value of 127 would move our sprite the width of 30 screens per second!

  3. Defining a struct by itself doesn’t cause anything to happen. We now need to use it by declaring some memory which uses that structure. We’ll do that immediately underneath the struct definition:

    struct Object player1 = {
    .x = 8,
    .y = 0,
    .vx = 0,
    .vy = 0
    };

    Writing struct Object player1 declares a variable called player1 which is of type struct Object — i.e. the struct we defined above. That means player1 has all the four fields inside it we defined. We also initialise our new variable with some initial field values. They would have all defaulted to 0 anyway, but it’s nice to be explicit, plus we give x the initial value 8 which corresponds to the initial value of player_x previously. Note carefully the syntax for field initialisation which is to prefix the field name with a dot . character.

    We are initialising our player at position (8, 0) which is where she was before, and we are also not giving her any velocity in either the horizontal or vertical direction, so she should start off at rest.

  4. Now we want to complete the refactor. Find all instances of player_x in your source code and replace with player1.x and similarly replace player_y with player1.y. You can use VS Code’s find and replace function (Cmd-Opt-F) or select all occurrences (Cmd-Shift-L) to save time.

    Instead of accessing the old variable player_x, we are now accessing the field x from inside the player1 struct, by using the . operator. player1.x means “the x field inside the player1 struct”.

  5. Rebuild and test that everything works the same as it did before. We are halfway through our refactor.

  6. Now we are going to modify our game logic so that it uses velocity instead of constant movement:

    First let’s make a definition at the top of the file to hold our horizontal speed. This will make our code more readable and make it easier to change later.

    Best place is a couple of lines below the line that says `uint8_t paused = 0;` near the top of the file:

    uint8_t paused = 0;

    **#define RUN_SPEED 1**

    Be very careful here — you do NOT want a semi-colon at the end of this line, and you also don’t want to use any = operator here. A #define directive is something a little different from regular C code, and we’ll go into more detail later, but for now just make sure you write it just like I have.

  7. Now we’ll rewrite the game logic. Since quite a lot will change, for convenience I’ve included the entire contents of the if (!paused) block:

    if (!paused)
    {
    keys = SMS_getKeysHeld();

    // handle horizontal movement
    if (keys & PORT_A_KEY_LEFT)
    {
    player1.vx = -RUN_SPEED;
    }
    else if (keys & PORT_A_KEY_RIGHT)
    {
    player1.vx = RUN_SPEED;
    }
    else
    {
    player1.vx = 0;
    }

    // handle vertical movement - TODO

    player1.x += player1.vx;
    player1.y += player1.vy;

    // draw the sprites
    SMS_initSprites();
    SMS_addSprite(player1.x, 184 - player1.y, 0);
    SMS_addSprite(120, 120, 1);
    }

    Check carefully for what has changed and make the corresponding changes to your file line by line. We’ll walk through the changes now:

  • If the left key is held down, instead of adding 1 to the x position of the player as we did before, we now set the horizontal velocity to -RUN_SPEED which will be -1 at the moment, according to our #define RUN_SPEED 1 statement earlier.
  • Similarly, if the right key is held down, we set the horizontal velocity to a positive number: RUN_SPEED.
  • If neither the left nor the right D-pad key is held, we say that the player is not moving horizontally, so in other words we set their vx to be 0.
  • Now that we have set the horizontal velocity, we can use it to move the player. That’s where the following two lines come in:
    player1.x += player1.vx;
    player1.y += player1.vy;
    Note we are also changing the vertical position here, although right now this doesn’t do anything because vy is still 0 from the start of our program.
  • Lastly, we’ve made a change to our coordinate system, so that (0, 0) now corresponds to the bottom left corner of the screen instead of the top left. We’ve done this to make it a little less complicated to reason about. We’ve achieved this by setting the sprite position to 184 - player.y instead of plain old player_y. It’s 184 because the screen is 192 pixels high, and your sprite is 8 pixels high, so you want to start drawing your sprite at position 192 - 8 = 184 if you want it to sit right at the bottom of the screen when y is 0.
  1. Rebuild and see if it works!

Refactor to use fewer magic numbers with #defines​

We introduced the #define directive earlier. In C, when something starts with a # it’s what’s called a “pre-processor directive”. This sounds fancy, but it just means that the effects of that directive take effect before normal compilation of the source code.

For example, the #define pre-processor directive will substitute one piece of text for another before the compilation starts. That’s how we could write RUN_SPEED instead of 1 earlier. The pre-processor goes through and every time it sees RUN_SPEED it replaces it with the text 1 before compiling. This is a very efficient way to define constant values in our code without resorting to defining variables, which do come with some overhead every time we use them.

It’s very idiomatic in C to use #define for constants, so that’s what we’ll do. It’s also conventional to make them ALL_CAPS so we’ll do that too!

We have a few more “magic” numbers we can tidy up and we will introduce some more. For now we have:

  • All the tile indexes, e.g. 0 for a blank tile, 0 for the player sprite and 1 for the goal sprite.
  • The magic 184 we use to map world Y coordinates to pixel Y coordinates, explained above.
  • The running speed, which we’ve already dealt with.

Let’s fix the first two things now:

  1. Make some new definitions, perhaps just above #define RUN_SPEED 1:

    #define TILE_BLANK 0
    #define SPRITE_PLAYER 0
    #define SPRITE_GOAL 1

    Now use those defines wherever you need to in the rest of the code, e.g.:

    for (int i = 0; i < 768; i++)
    {
    SMS_setTile(TILE_BLANK);
    }

    and

    SMS_addSprite(player1.x, 185 - player1.y, SPRITE_PLAYER);
    SMS_addSprite(120, 120, SPRITE_GOAL);

    Now if you ever change the order of your sprites in your bmp file you can easily update the code.

  2. Next let’s deal with the magic 184. Back at the top, let’s define both the screen height and the height of the player sprite:

    #define SCREEN_HEIGHT 192
    #define PLAYER_HEIGHT 8

    Now we can use that on the second last line:

    SMS_addSprite(player1.x, SCREEN_HEIGHT - PLAYER_HEIGHT - player1.y, SPRITE_PLAYER);
  3. This is an improvement, although it’s a little verbose and we might need to make this calculation a lot, so we can go one further.

    As well as replacing simple values, we can use #define to replace whole chunks of code in just the same way. We can also parameterise the define so that the replacement can be adapted to different situations. In this case we tend to call the definition a “macro”. Let’s write one:

    #define MAP_Y(world_y) (SCREEN_HEIGHT - (world_y))

    The syntax here is that arguments are specified in parentheses after the macro name (MAP_Y). Here we have one argument called world_y and whatever value that argument has will replace the text world_y in the macro body.

    A macro feels a little like a function, but again it’s important to note this is simply a way to literally repeat some code before compilation — it’s a little like an automated copy and paste and no function structure is actually created in the compiled program as with a regular function.

    So for example, we can use this macro to again change the SMS_addSprite line which renders the player sprite:

    SMS_addSprite(player1.x, MAP_Y(player1.y + PLAYER_HEIGHT), SPRITE_PLAYER);

    So here, when we write MAP_Y(player1.y + PLAYER_HEIGHT), the pre-processor will first be expanded to: (SCREEN_HEIGHT - (player1.y + PLAYER_HEIGHT)), and then to: (192 - (player1.y + 8)).

    Do note that the parentheses () are very important within macro definitions. This is because we could be instantiating the macros from anywhere in our code and without the parentheses the replacement might end up not making sense in its new context. When in doubt, it’s safest to put parentheses around the entire macro body and also around the usage of each argument within the macro body.

    Now that we’ve written the MAP_Y macro, we can use it any time we need to convert from world coordinates to screen coordinates. We still need to allow for the height of the player, but it makes more sense to add the height to the y coordinate than to subtract it. e.g. in MAP_Y(player1.y + PLAYER_HEIGHT) , when the player is at y position 0, that means they are touching the ground, so we need to add their height to that to find the position where the top left corner of their sprite tile need to be, then finally we map that to screen coordinates with the macro.

Jump physics​

Had enough refactoring yet? Let’s build some more game functionality!

  1. Find the comment which says // handle vertical movement - TODO and replace it with this:

    // handle vertical movement
    if (player1.y > 0)
    player1.vy -= GRAVITY;
    else if (SMS_getKeysPressed() & (PORT_A_KEY_1 | PORT_A_KEY_2))
    player1.vy = JUMP_STRENGTH;

    You might notice we’ve also used two new defines here: JUMP_STRENGTH, and GRAVITY, so let’s create them with our other defines at the top of the file:

    #define JUMP_STRENGTH 5
    #define GRAVITY 1

    Let’s review — this new code is saying that if the player is above the ground (player1.y > 0), it must be falling, so every frame we subtract GRAVITY from the vertical velocity. This simulates the acceleration due to gravity we were talking about in the introduction. Remember acceleration is the rate of change of velocity, so the acceleration due to gravity here is 1 pixel per frame, and because we are subtracting it from player1.vy that means it’s acting in a negative, or downward direction relative to our world coordinates.

    If player1.y is not > 0, it must be exactly equal to 0 because player1.y is an unsigned quantity and can’t be negative. Then the else if clause applies, and we check the control pad to see if either the A or B action buttons are pressed:

    SMS_getKeysPressed() & (PORT_A_KEY_1 | PORT_A_KEY_2)

    Essentially we are making sure here that the player can’t jump off of thin air — they must be on the ground in order to jump.

    Using the SMS_getKeysPressed function ensures that if we are holding the buttons down we don’t zoom upwards forever — a single press will cause a single jump. We’re using bitwise AND and OR operations in combination to check for both buttons at the same time, rather than having to check for each individually. & means bitwise AND and | means bitwise OR. PORT_A_KEY_1 | PORT_A_KEY_2 combines the bit mask patterns for those two buttons into a single bit mask, which can then be checked against the keys pressed using &.

    If either of the buttons are pressed, we then set the player’s vy to be JUMP_STRENGTH, in this case 5. This means that every frame the player will be moving 5 pixels in the positive direction — i.e. upwards. By itself, that means that the player should shoot off into the sky forever, but of course after the jump has started and player.y is greater than 0, then next frame gravity will take effect instead, and the player will start slowing down. First vy will become 4, then 3, then 2, then 1, then 0 at which point the player will be travelling neither up nor down — they will be at very the top (the zenith or apogee) of the jump, floating in mid air for a split second — and then vy will start to go negative, becoming -1, -2, -3, etc. accelerating faster and faster towards the ground.

  2. Let’s rebuild and try this out…

    You should find that you can press one of the action buttons and see your sprite jump in the air. You might have to look up which keys on your keyboard the action buttons are mapped to in Emulicious.

    However, you might see something terrifying happening after you have jumped and started falling again. Your character will accelerate down through the Earth’s crust, faster and faster, wrapping around the screen. Every so often you’ll see her slow down again, which happens when her velocity becomes less than -127 and we run out of bits in our int8_t so it wraps back around to zero!

    We need some more code which stops our player falling through the ground.

  3. We’ll do the check after we’ve moved the player, so immediately after the line that says player1.y += player1.vy;:

    player1.y += player1.vy;

    **// don't let player fall below the ground
    if (player1.y > SCREEN_HEIGHT) {
    player1.y = 0;
    player1.vy = 0;
    }**
  4. Rebuild and test.

    That’s better!

    But why did we write our check for being below ground as if (player1.y > SCREEN_HEIGHT) rather than if (player.y < 0)?

    This is because we are using an unsigned uint8_t to store player1.yand so by definition it can never be less than zero. If we have an unsigned 8 bit value of 0 and subtract 1 from it, we will actually end up with 255. However, since we know our screen is only 192 pixels high we can use this to our advantage and simply check whether or not the new player y position is greater than that. That means we should be able to make our player climb all the way to the very top of the screen, but no higher, because any pixel positions greater than 192 will be determined to be below the ground rather than in the air.

    Note that if our screen was 255 pixels high we wouldn’t have been able to pull this trick because there would be no buffer zone between the top of the screen and the bottom of the screen — we might have had to use a 16 bit variable instead of an 8 bit variable in that case.

    Also if our player is moving so fast downwards that it skips over the zone from 192 to 255 we won’t detect it has fallen through the floor and it will mysteriously fall from the sky again! The difference between 192 and 255 is only 63 pixels, so actually if gravity were strong enough and we dropped our player from sufficiently high we could cause this to happen, but with gravity of 1 this shouldn’t be a problem.

    You should now find that you can jump around the screen to your heart’s content. Try changing the different constant defines to jump higher or run faster, or change gravity to see what happens. You could even try making gravity zero or negative and see how the world would be if you made a mockery of the laws of physics.

Scenery interactions​

We’re now going to draw some background scenery and have our character interact with it. The goal is to create a little earthy hill that our player can climb to the top of.

Drawing a scenery map​

  1. First let’s create a background tile. In background.aseprite draw a tile that you can use for your hill. Here’s what my background.aseprite looks like with the blank tile and the scenery tile next to it:

    Untitled

  1. Export the aseprite file to a bmp again.

  2. We’ll need a new define for our scenery tile. Since it’s index 1 in the bmp we’ll do this, just underneath the define for TILE_BLANK:

    #define TILE_EARTH 1
  3. We’ll need some kind of a map to say what our hill looks like and where it is on the screen. There are lots of ways to do this, but for now let’s take a simplistic approach. We’ll map the height of the landscape for each of the 32 columns on screen, so we’ll have an array of 32 numbers, where each number is the height in tiles of each column of earth. Let’s create a new variable called columns underneath the declaration and initialisation of the player1 variable, and just before the main function begins:

    // height in tiles of each column of the screen
    **uint8_t columns[32] = {
    0, 0, 0, 0, 0, 0, 1, 2,
    3, 4, 5, 6, 7, 8, 8, 8,
    8, 8, 7, 6, 5, 4, 3, 2,
    1, 0, 0, 0, 0, 0, 0, 0
    };**

    void main(void)
    {
    ...

    I’ve broken it up into 4 groups of 8 numbers just so it’s more readable here, but you don’t need to do that, you could put the whole thing on one line if you wanted.

  4. Now that we’ve described our landscape, we can rewrite our screen clearing loop to draw it for us. Find the loop, which currently counts from 0 to 767 using for (int i = 0; i < 768; i++) and replace the whole for loop with this code instead:

    ```c
    for (int row = 23; row >= 0; row--)
    {
    for (int col = 0; col < 32; col++)
    {
    if (columns[col] > row)
    SMS_setTile(TILE_EARTH);
    else
    SMS_setTile(TILE_BLANK);
    }
    }
    ```
  5. Rebuild the project and test it out, you should see something like this:

    Untitled

    How does this work?

    Instead of just one loop that goes round 768 times, we now have two nested loops. The inner loop goes round 32 times for every outer loop; and the outer loop goes round 24 times. Note that 32 x 24 = 768 so we are still covering the same number of tiles, which is the total number of tiles on the screen.

    Because the inner loop is going round 32 times, and because the way SMS_setTile works is writing tiles from left to right across the screen, row by row, this means that each iteration of the inner loop draws one tile on a single row, and each iteration of the outer loop draw a whole row of 32 tiles to the screen.

    The inner loop counts the col variable from 0 to 31 and every time it iterates, it checks the columns array for a corresponding entry in that column number by considering columns[col]. Remember that the entries in columns represent the height of the ground in tiles, so a value of 0 means ground level, a value of 1 means 1 tile high, etc. Knowing this, we can compare the value in columns[col] with whatever row we are currently drawing. To make this a bit easier, and since we need to invert our y coordinate so that 0 is at the bottom, rather than counting up as we usually do, we’re counting down from 23 to 0.

    for (int row = 23; row >= 0; row--)

    Note carefully that we start with row as 23, and decrement its value every iteration with row-- instead of row++. Since we start with row as 23, and we want to count 24 times, our final iteration needs to be when row is 0 and so we need to check for >= 0 instead of just plain >.

    Now that we’re counting backwards, we can say that the first row is row 23, the second row is row 22, etc. all the way down to the bottom row which will be row 0.

    This makes our comparison with the column heights easy peasy! If the column height for our column is 1, we want to draw the earth tile on row 0 (the bottom row) only. If it’s height 2, we want to draw the earth tile on rows 0 and 1, etc. In other words, if the column height is greater than the row number, we draw the earth tile; otherwise we can draw a blank tile.

    if  (columns[col] > row)
    SMS_setTile(TILE_EARTH);
    else
    SMS_setTile(TILE_BLANK);

    Well, that wasn’t too hard!

    Feel free to play around with the columns variable and try encoding different landscapes. When you’re ready, we’ll start working on making our player interact with the landscape instead of walking in front of it.

Interactions​

Interacting with scenery is a little more complex than just drawing it.

The first thing we’ll handle is having our player interact with the top edge of scenery tiles. Essentially what it means to be standing on some scenery is that the ground level has changed from being at y coordinate 0 to being some higher number. In our world coordinate system, which measures pixels from the bottom of the screen, the ground level goes up by 8 pixels for every scenery tile in the stack for that column. For example, if we are standing on one tile, we are 8 pixels in the air and if we are standing on two tiles, we are 16 pixels in the air, etc.

That means we can calculate the ground level like this:

ground_level **=** columns[player1.x **/** 8] ***** 8

How does this work? Well, remember our columns variable is an array of 32 values, one for each column on the screen, and that each value represents the height in tiles of the scenery for that column.

Since we know exactly how far across the screen our player is, with player1.x, we just need to work out which column she is currently standing in, and then we can work out the height of the scenery for that column.

Figuring out the column means dividing the x coordinate, which is measured in pixels by 8 to get the column index, measured in tiles, because each tile is 8 pixels wide. There are 256 pixels across the screen, but only 32 tiles.

In C when we divide an integer like player1.x we always end up with another integer, even if the division doesn’t go exactly, and the result is always rounded down towards negative infinity. For example, if player1.x is 5, then player1.x / 8 will be 0 because 5/8 is 0.625 which rounds down to 0. Similarly, if player1.x is 100, player1.x / 8 will be 12 (rounded down from 12.5).

Similarly, if player1.x is 0 then player1.x / 8 will be 0 and if player1.x is 255 (the last pixel on the screen) then player1.x / 8 will be 31, so we can see that simply dividing by 8 will convert pixels to columns.

The only caveat here is that our reference point is the left hand edge of our sprite, so only when that point walks into a column will the ground level change. We could choose another point (e.g. the middle) of our sprite to be the reference point, but then we’d need to do adjust our calculation slightly. More on this later.

Now that we know which column we are standing in, we can look up the height of the ground in that column by doing columns[player1.x **/** 8].

Of course, this returns the height of the ground in tiles rather than pixels, but we can easily convert back again by multiplying by 8.

Let’s start coding some of this:

  1. Declare a new variable called ground_level after your declaration for the keys variable:

    uint8_t keys;
    **uint8_t ground_level = 0;**
  2. Now let’s calculate the ground level just in front of the code we wrote before which stopped the player falling through the floor, and we’ll also change that code a little:

    player1.y += player1.vy;

    **ground_level = columns[player1.x / 8] * 8;**

    // don't let player fall below the ground
    if (**(player1.y < ground_level) || (**player1.y > SCREEN_HEIGHT**)**) {
    player1.y = **ground_level**;
    player1.vy = 0;
    }

    The new bits are in bold.

    So after moving the player, we calculate the ground level at their new position. Then, as well as checking whether or not they are below the bottom of the screen (player1.y > SCREEN_HEIGHT from before), we also check whether or not they are sitting below the ground level we have calculated for that particular column with player1.y < ground_level. If either of these conditions are met (we use the logical OR || operator to check), we run the code which prevents the player from falling down. We still set vy to 0, which stops any vertical falling motion, and we also correct the player1.y position so that if the new position of the player would have had them sunk into the ground by a few pixels, it moves them back out again so that they are neatly positioned right on top of the scenery, with player1.y = ground_level. Pretty clever, huh?

  3. Rebuild and test.

    You should find that we’re getting somewhere. The player can jump up from step to step, and when she walk down the slope she drops satisfyingly down to the lower levels. Also, rather pleasingly, she “automatically” walks up the steps without needing to jump, although that wasn’t our intention and we will change that soon. She’ll do this in both directions, although if you look carefully you’ll notice that she won’t move up the steps in the left-to-right direction until she’s right over it:

    Untitled

    Untitled

    This is to do with the reference point being the left-hand edge of the sprite, as I was explaining above. Only when the sprite’s left-hand edge enters the column does the floor height change. We’ll fix that very soon.

    Although it looks cool that our player automatically climbs steps just by walking at them, it’s not that sensible. You can see why not if you change the columns map to create a giant cliff, for example like this:

    Untitled

    If you try walking into the cliff, you’ll find that you’re magically transported to the top, but only after somehow walking 7 pixels into solid rock!

    Untitled

    What we really want to do is, if we find ourselves having moved inside solid rock, to bounce the player back out so that she is standing just the right side of it, thereby preventing her from walking through it.

  4. We need to check whether or not we’ve walked into rock before we do the ground level check, because the ground level check is what is automatically transporting us to the top of the column. So let’s write this next bit of code after we update the player’s x position but before we update their y position, i.e. in between these lines:

    player1.x += player1.vx;
    **// HERE**
    player1.y += player1.vy;

    First let’s write something to check whether or not the left hand edge of the player sprite is inside the scenery. The check is simply whether or not the left hand edge of the player is underground. We don’t want this to be possible, so we will push them back into the next column. If the left hand edge of the player is underground, we should push them away to the right, since that’s probably where they came from. New code in bold:

    player1.x += player1.vx;

    **// don't allow player to move through solid rock
    // check left hand side of player
    if (player1.y < (columns[player1.x / 8] * 8))
    {
    // push player one column to the right
    player1.x = ((player1.x / 8) + 1) * 8;
    }**

    player1.y += player1.vy;

    ground_level = columns[player1.x / 8] * 8;

    You should recognise the expression columns[player1.x / 8] * 8 as the calculation for ground level at the point player1.x. So we calculate ground level and compare it with player1.y. If the player is underground (i.e. if player1.y is less than the calculated ground level), then we need to reset player1.x to position the player in the next column to the right. We can calculate that with ((player1.x / 8) + 1) * 8

    As before, player1.x / 8 gives us the current column number of the player’s left-hand edge. We then add 1 to it, to find the next column to the right, then we multiply by 8 to translate that back from columns to pixels.

  5. You should be able to rebuild and test this change. You’ll find that you can still walk up the stairs from left to right, but now if you try to walk into them in the right-to-left direction you’ll just hit a hard stop.

    Now to fix it for the left-to-right direction. New code in bold:

    player1.x += player1.vx;

    // don't allow player to move through solid rock
    // check left hand side of player
    if (player1.y < (columns[player1.x / 8] * 8))
    {
    // push player one column to the right
    player1.x = ((player1.x / 8) + 1) * 8;
    }
    **// check right hand side of player
    else if (player1.y < (columns[(player1.x + 7) / 8] * 8))
    {
    // push player one column to the left
    player1.x = (player1.x / 8) * 8;
    }**

    player1.y += player1.vy;

    ground_level = columns[player1.x / 8] * 8;

    This is essentially doing the same as before, except we need to check the right-hand edge of the sprite instead of the left-hand edge, so we calculate the ground level at a point 7 pixels to the right:

    columns[(player1.x **+ 7**) / 8] * 8

    Otherwise the comparison works in the same way.

    The other difference is that it would make more sense to push the player one column to the left this time, again because that’s probably the direction she came from:

    player1.x = (player1.x / 8) * 8

    This time we don’t need to make any column adjustments, we just round back down to the start of the current column, because if the right-hand edge of the sprite overlapped with the scenery a little, we just want to jog the player back onto alignment with the column, which will nudge the right-hand edge back again.

    You might be wondering, why didn’t we move our calculation of ground_level up, so that we did it before our horizontal collision checks? Isn’t it the same code repeated at least twice?

    The problem is that the collision check could change player1.x, and so in turn the ground level could change after we’ve calculated it. We will always need to calculate a “final” value for ground_level after the horizontal collision checking has finished. In any case, although it looks like we are calculating the ground level multiple times, remember we are calculating it for at least two different reference points — the left-hand edge of the sprite and the right-hand edge, and those calculations are a bit different.

    We might create a macro to reduce the repetition, but let’s not get caught up in prettifying our code right now for this example game!

Caveats​

We’ve made quite a few assumptions here, notably that our sprites are 8 pixels wide and also that velocity is limited to less than 8 pixels at a time. If these things change we might need to make some more advanced checks and rules about how scenery interactions work.

Further refinements​

There are a few bells and whistles we can add here.

There’s a little bit of a strange behaviour when walking down the hill — the fall speed is different when walking downhill from right-to-left vs. left-to-right.

Again, this is because our ground level reference point is the left-hand edge of the player sprite. When we walk downhill from right-to-left we will fall down almost immediately: as soon as the first pixel overlaps the edge of the tile. To fix this we can just move the reference point to be close to the centre of the sprite:

ground_level = columns[(player1.x **+ 4**) / 8] * 8;

Now for vertical scenery interaction the ground level is taken from a point 4 pixels into the sprite, rather than the very left-hand edge. This means that the player will fall down a step only when the fourth pixel overlaps the tile edge.

Note that this still isn’t quite symmetrical, because there are 8 pixels in the sprite, which is an even number, so the left-to-right reference point will still be slightly different from the right-to-left reference point, but this shouldn’t be too noticeable.

Another nice touch we can make is to render some grass at the top of each column:

  1. Create a new grassy tile in background.aseprite, e.g.

    Untitled

  2. Export to bmp

  3. Create a new define for the tile e.g. #define TILE_GRASS 2

  4. Modify the background rendering code. New code in bold:

    for (int row = 24; row >= 1; row--)
    {
    for (int col = 0; col < 32; col++)
    {
    **if (columns[col] == row)
    SMS_setTile(TILE_GRASS);
    else if (columns[col] > )**
    SMS_setTile(TILE_EARTH);
    else
    SMS_setTile(TILE_BLANK); }
    }

    This code checks whether we are dealing with the very top of the column by subtracting the height of the column from the current row number. If the difference is exactly 1, we must be at the top of the column, so we render the grassy tile. If the difference is greater than 1 we must be further into the column, so that’s plain earth. If the difference is 0 or less we are above the column and render a blank tile.

    Untitled

    Looks pretty sweet!

As a last refinement, let’s have the player change direction so that she’s not walking backwards when she moves from right-to-left.

  1. Create a new sprite design for your backwards facing player. You could just horizontally flip the existing tile in Aseprite, or design one from scratch. The Master System is a little unusual in that it doesn’t provide hardware flipping of sprite tiles as some other systems did. Although it means you have to create flipped sprites yourself, it also means you get to design them to look slightly different if you like, which is kind of cool.

  2. Export to bmp and take note of the position of the sprite in the file.

  3. Create a new define and rename the existing one, so that you have two defines, one for each direction:

    #define SPRITE_PLAYER_R 0
    #define SPRITE_PLAYER_L 2

    Here my left-to-right facing sprite is at position 0 in my bmp file and my right-to-left facing sprite is at position 2.

  4. Modify your Object struct so that you can keep track of which way it is facing, and initialise it to point whichever way you like. New code in bold:

    struct Object {
    uint8_t x;
    uint8_t y;
    int8_t vx;
    int8_t vy;
    **bool facing_left;**
    };

    struct Object player1 = {
    .x = 8,
    .y = 0,
    .vx = 0,
    .vy = 0,
    **.facing_left = false**
    };

    (note that you may need to add #include <stdbool.h> at the top of your file in order to get access to bool and false)

  5. Make the changes so that your character switches direction when you use the D-pad. New code in bold:

    // handle horizontal movement
    if (keys & PORT_A_KEY_LEFT)
    {
    player1.vx = -RUN_SPEED;
    **player1.facing_left = true;**
    }
    else if (keys & PORT_A_KEY_RIGHT)
    {
    player1.vx = RUN_SPEED;
    **player1.facing_left = false;**
    }
    else
    {
    player1.vx = 0;
    }
  6. Finally, modify the sprite rendering code so that it renders either the left-facing or right-facing sprite depending on the value of the facing-left field. New code in bold:

    // draw the sprites
    SMS_initSprites();
    SMS_addSprite(
    player1.x,
    MAP_Y(player1.y + PLAYER_HEIGHT)**,
    player1.facing_left ? SPRITE_PLAYER_L : SPRITE_PLAYER_R**
    );
    SMS_addSprite(120, 120, SPRITE_GOAL);

    I’ve broken this over several lines to make it easier to read, but you could write this on a single line if you wanted.

    Here we are using a ternary expression rather than an if statement. The ternary is of the form condition ? result_if_true : result_if_false

    so player1.facing_left ? SPRITE_PLAYER_L : SPRITE_PLAYER_R will result in a value of SPRITE_PLAYER_L if player1.facing_left is true, or SPRITE_PLAYER_R if it’s false.

    We could also have written this using an if/else statement instead:

    if (player1.facing_left)
    {
    SMS_addSprite(
    player1.x,
    MAP_Y(player1.y + PLAYER_HEIGHT),
    SPRITE_PLAYER_L
    );
    }
    else
    {
    SMS_addSprite(
    player1.x,
    MAP_Y(player1.y + PLAYER_HEIGHT),
    SPRITE_PLAYER_R
    );
    }

    But the ternary makes it less repetitious.