Physics and scenery interactions
This page is not quite ready for visitors yet. Please check again another time.
đź’ˇ 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.
Firstly, in Aseprite, we want to move the “goal” image to your
sprites.asesprite
file instead ofbackground.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:
Open your
sprites.aseprite
file and load the full palette of 64 Master System colours using the preset button:Remember to press “Remap Palette” after you do this.
Copy your “goal” sprite from
background.aseprite
and paste it intosprites.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 theC
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.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.
Press “Remap Palette”
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.
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).
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)
Re-export the
sprites.aseprite
file to a.bmp
.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);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 the1
with the index of the sprite, remembering that it starts from0
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
andy
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​
We’re going to replace the two variables,
player_x
andplayer_y
, so let’s delete the two lines which declare them, near the top of themain.c
file.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
andvy
; corresponding to horizontal position, vertical position, horizontal velocity and vertical velocity respectively.Note that
x
andy
areuint8_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
andvy
areint8_t
types, and the lack ofu
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!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 thestruct
definition:struct Object player1 = {
.x = 8,
.y = 0,
.vx = 0,
.vy = 0
};Writing
struct Object player1
declares a variable calledplayer1
which is of typestruct Object
— i.e. thestruct
we defined above. That meansplayer1
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 to0
anyway, but it’s nice to be explicit, plus we givex
the initial value8
which corresponds to the initial value ofplayer_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.Now we want to complete the refactor. Find all instances of
player_x
in your source code and replace withplayer1.x
and similarly replaceplayer_y
withplayer1.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 fieldx
from inside theplayer1
struct, by using the.
operator.player1.x
means “thex
field inside theplayer1
struct”.Rebuild and test that everything works the same as it did before. We are halfway through our refactor.
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.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 be0
. - 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:Note we are also changing the vertical position here, although right now this doesn’t do anything because
player1.x += player1.vx;
player1.y += player1.vy;vy
is still0
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 to184 - player.y
instead of plain oldplayer_y
. It’s184
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 wheny
is0
.
- 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 and1
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:
Make some new definitions, perhaps just above
#define RUN_SPEED 1
:#define TILE_BLANK 0
#define SPRITE_PLAYER 0
#define SPRITE_GOAL 1Now 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.
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 8Now we can use that on the second last line:
SMS_addSprite(player1.x, SCREEN_HEIGHT - PLAYER_HEIGHT - player1.y, SPRITE_PLAYER);
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 calledworld_y
and whatever value that argument has will replace the textworld_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. inMAP_Y(player1.y + PLAYER_HEIGHT)
, when the player is at y position0
, 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!
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
, andGRAVITY
, so let’s create them with our other defines at the top of the file:#define JUMP_STRENGTH 5
#define GRAVITY 1Let’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 subtractGRAVITY
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 is1
pixel per frame, and because we are subtracting it fromplayer1.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 to0
becauseplayer1.y
is an unsigned quantity and can’t be negative. Then theelse 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 beJUMP_STRENGTH
, in this case5
. 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 andplayer.y
is greater than0
, then next frame gravity will take effect instead, and the player will start slowing down. Firstvy
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 thenvy
will start to go negative, becoming -1, -2, -3, etc. accelerating faster and faster towards the ground.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.
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;
}**Rebuild and test.
That’s better!
But why did we write our check for being below ground as
if (player1.y > SCREEN_HEIGHT)
rather thanif (player.y < 0)
?This is because we are using an unsigned
uint8_t
to storeplayer1.y
and so by definition it can never be less than zero. If we have an unsigned 8 bit value of0
and subtract1
from it, we will actually end up with255
. 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​
First let’s create a background tile. In
background.aseprite
draw a tile that you can use for your hill. Here’s what mybackground.aseprite
looks like with the blank tile and the scenery tile next to it:
Export the aseprite file to a bmp again.
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 forTILE_BLANK
:#define TILE_EARTH 1
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 theplayer1
variable, and just before themain
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.
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);
}
}
```Rebuild the project and test it out, you should see something like this:
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 from0
to31
and every time it iterates, it checks thecolumns
array for a corresponding entry in that column number by consideringcolumns[col]
. Remember that the entries incolumns
represent the height of the ground in tiles, so a value of0
means ground level, a value of1
means 1 tile high, etc. Knowing this, we can compare the value incolumns[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 from23
to0
.for (int row = 23; row >= 0; row--)
Note carefully that we start with
row
as23
, and decrement its value every iteration withrow--
instead ofrow++
. Since we start withrow
as23
, and we want to count 24 times, our final iteration needs to be when row is0
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:
Declare a new variable called
ground_level
after your declaration for thekeys
variable:uint8_t keys;
**uint8_t ground_level = 0;**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 withplayer1.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 setvy
to0
, which stops any vertical falling motion, and we also correct theplayer1.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, withplayer1.y = ground_level
. Pretty clever, huh?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:
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: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!
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.
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 pointplayer1.x
. So we calculate ground level and compare it withplayer1.y
. If the player is underground (i.e. ifplayer1.y
is less than the calculated ground level), then we need to resetplayer1.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 add1
to it, to find the next column to the right, then we multiply by8
to translate that back from columns to pixels.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 forground_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:
Create a new grassy tile in
background.aseprite
, e.g.Export to bmp
Create a new define for the tile e.g.
#define TILE_GRASS 2
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.
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.
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.
Export to bmp and take note of the position of the sprite in the file.
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 2Here my left-to-right facing sprite is at position
0
in my bmp file and my right-to-left facing sprite is at position2
.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 tobool
andfalse
)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;
}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 formcondition ? result_if_true : result_if_false
so
player1.facing_left ? SPRITE_PLAYER_L : SPRITE_PLAYER_R
will result in a value ofSPRITE_PLAYER_L
ifplayer1.facing_left
is true, orSPRITE_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.