Skip to main content

Sound effects, flip-screen, goal interaction and enemies

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!

Sound effects​

TODO – what happened to this section!?

Flip-screen​

Of the two methods of handling multiple screens: flip-screen and side-scrolling, flip-screen is the more simple to implement. The basic idea is that when you move to the edge of the screen, the game suspends while the screen completely redraws with a whole new screen — it “flips” from one screen to the next. This is relatively easy for us to do because we have already written some code which loads a whole screen — the code that draws the level in the first place. We just need to re-use that code every time we move from screen to screen. Here’s the general idea:

  • We’ll modify our columns data so that it can hold more than one screen’s worth of columns.
  • We’ll refactor the screen drawing code into a function that we can call any time we like.
  • We’ll detect when the character walks off the extreme left or extreme right of the screen.
  • If the character walks off the extreme left of the screen, we’ll redraw the screen, except we’ll do so with the set of 32 columns to the “left” of where we were before. We’ll also transport the character sprite to the far right of the new screen.
  • If the character walks off the extreme right of the screen, we’ll redraw the screen, except we’ll do so with the set of 32 columns to the “right” of where we were before. We’ll also transport the character sprite to the far left of the new screen.

Sound good? Okay, let’s do this thing.

Storing multiple screens of column data​

This is easy enough. Our columns variable so far is a fixed array of 32 values. We’re going to change it so that it’s a 2-dimensional array, that is an “array of arrays”. The first dimension of the array will be the number of the screen we are on, and the second dimension will be the column number on that particular screen, still from 0-31 as before. We’ll start off with 1 screen, and then increase to 3 as an example but you can add as many as you like.

  1. Add some new defines to hold the constant array sizes:

    #define SCREENS 1
    #define COLUMNS_PER_SCREEN 32
  2. Change the declaration of the columns variable from uint8_t columns[32] to uint8_t screens[SCREENS][COLUMNS_PER_SCREEN]

  3. Where we previously initialized columns with a literal array of 32 values between { and } signs, that will correspond to a single screen only, so we need to put another set of curly braces around it for the new dimension of the array, so the whole thing needs to look something like this:

    const uint8_t screens[SCREENS][COLUMNS_PER_SCREEN] = {
    {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}
    }

    We’ve now declared 1 screen worth of data, and there are still 32 column heights for that 1 screen.

    Where as before, if we wanted to access the height of a particular column, let’s say the 6th column, we would have written:

    uint8_t height = columns[5];

    Now we would write:

    uint8_t height = screens[0][5];

    The screens[0] part is the first dimension of our array, and says “give me the array of columns for the first screen”. The [5] part says “give me the 6th column”, just like before.

    If we had more than one screen, let’s say we had 3 screens, and we wanted to get the height of the sixth column for the third screen, we would write:

    uint8_t height = screens[2][5];

    Hopefully you get the idea.

    tip

    ℹ️ You might also notice that I added the const keyword in front of the variable declaration. We could have done this before, with the columns variable, so there’s no special reason we’re adding it now other than I onlt just noticed we hadn’t done it!

    The const keyword tells the compiler that we do not intend to modify the data inside screens at any point after it has been initialised. This is a very important hint to the compiler, because it will use that clue to make a number of optimisations to our program. It will also mean that if we ever try to change the data inside our array, the compiler will complain that we are not allowed to do it. This is quite useful to prevent us from accidentally changing things we didn’t intend to, so you should try and declare things as const whenever possible unless you definitely plan to use them as variables you can change. The technical term for changing data is mutation and we call the “regular”, non-const, variables [mutable variables](https://en.wikipedia.org/wiki/Constant_(computer_programming)).

    When compiling for embedded systems like SMS games, one major thing in particular that declaring const variables will likely do is cause the data to be accessed from ROM — i.e. on the cartridge itself — rather than copied from the cartridge ROM into RAM. This is because cartridge ROMs are Read Only, and so if the data is never going to change that’s a fine place to put it. Data which might change has to go in RAM because that’s the only place it can go if it needs to be changed. There is only 8KB of RAM in the SMS base system, so it’s a scarce resource, but there is potentially a very large amount of cartridge ROM — the biggest known cartridges back in the day were 4MB.

    To keep our program working, we need to go through our code and change all the references to columns so that they refer to screens[0] instead. Do that, with either find and replace (Cmd-Opt-F) or with select many (Cmd-Shift-L). You might want to review each replacement by eye though, to make sure only things you want to be changed are being changed.

    Rebuild and test. Does it still work? We’ve changed our code so that it’s always making decisions based on the columns in screen 0 and that’s what all the screens[0] are about. If we defined some more screens and instead of screens[0] we wrote screens[1] for example, we would be working with a different set of columns. Of course, rather than manually changing the screen number from 0 to 1 etc. we could use a variable.

  4. Let’s define two more screens of screen data. That means you will need to add two more sets of 32 column heights to the screens array. Each set of columns must be enclosed within inner pairs of curly braces {...} and also separated from the previous set of columns with a comma after the curly brace ...},

    You can add as many screens as you feel like, just remember to update your SCREENS define, wherever you put that, to be the same as the number of screens you’ve made.

    Here’s what it should look like:

    // remember to change this - it must match what's in your array
    #define SCREENS 3

    // ... other code ...

    const uint8_t screens[SCREENS][COLUMNS_PER_SCREEN] = {
    {1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 8, 7, 6, 5, 4, 3, 2, 1, 3, 5, 6, 7, 8, 9, 10},
    {9, 9, 10, 10, 8, 6, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7},
    {7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
    };

    Note that the third screen does NOT have a trailing comma after the closing curly brace because it’s the last element in the outer array.

    If you forget to update your SCREENS define, C will complain that you haven’t not the amount of initial data right for your array. Because it’s a 3x32 array, C expects you to give it exactly 96 numbers, and specifically in 3 groups of 32.

  5. Now let’s make a new variable to hold the screen we are currently on. We’ll make this variable global, so put it outside the main function, alongside player1.

    (new code in bold):

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

    **int current_screen = 0;**

    Let’s also do another find and replace, changing all instances of screen[0] to screen[current_screen].

  6. Rebuild and test — everything should still work as before. However, now we should be in a position to test our new screens. Try changing the value of current_screen from 0 to 1 or 2 and rebuilding. Hopefully you should see your second or third screens load up instead of the first one.

    The highest screen number you will want to initialise current_screen to is 1 less than the total number of screens you have defined. For example, if you have #define SCREENS 3 then the highest you can go is screen 2, so uint8_t current_screen = 2;

    You can try putting higher numbers in if you like, but you’ll get some crazy results, columns all over the shop. This is because C is allowing you to “fall off” the end of your array and is interpreting some other memory as column data. Who knows what is in that memory, it will be some other variables in your program probably. It shouldn’t cause anything to break — you’re only reading from that memory, not modifying it — but you might find your player is stuck between two giant columns or something weird like that. This is one of the ways that C isn’t as “safe” as other languages. We say that C is not “memory safe”.

Refactor our screen drawing function​

Ready for some more refactoring?

Because flip-screen is just redrawing a whole screen of data, we’ll re-use the code we’ve already written to do exactly that. We’re going to pull out the code we’ve already written and make it more general so we can call it any time. That means putting it into its own function. We call this type of refactoring extracting a function.

  1. The first thing we’ll do is write the bare skelington of our extracted function. We’re going to do this right above the main function declaration (the line that says void main(void)). The new code is in bold here:

    **void load_screen(void)
    {
    }**

    void main(void)
    {
    //... etc. ...
  2. The code we are going to extract is some of the very first stuff we wrote on day 1. We’re going to extract from the line that says SMS_setNextTileatLoc(0); and take everything including the nested for loops. Highlight all that code and cut it to the clipboard with Cmd-X. Now in its place type:

    load_screen();

    Paste the code you cut inside the empty load_screen function and correct any indentation issues you spot.

  3. At this point you should be able to rebuild and test. All we’ve done is move some code from A to B, but we’re still running it just the same as before. In fact, it’s quite possible that the compiled program hasn’t even changed at all — the compiler may have actually decided in its own head to leave the old code where it is rather than actually creating a function for us. That’s how clever the compiler is!

Dealing with the edges of the screen​

Currently the player can freely walk past both left and right edges of the screen, and will likely wrap around to the other side. If your player can’t do that, it’s probably because there is some “random” data sitting before or after the 32 length columns array (now screen[current_screen] array which looks like a tall column just off the screen).

Let’s first fix it so that the player can’t walk off the screen.

Because we are using a uint8_t, which can only store numbers from 0 to 255, to store the player x position; and because the screen is 256 pixels wide, that means we can’t simply compare x < 0 to detect if the player has walked off the edge of the screen, because x can never be zero as it is unsigned. To work around this for now, we are going to define the left hand edge of our screen as the first strip of 8 pixels between positions 0 and 7. If the character finds themselves in that zone they will be transported to the next screen to the left.

We’ll do the same thing on the right hand side, but for a completely different reason: because our player is 8 pixels wide and their reference point is at the top-left of their sprite, to detect when the player’s right-hand edge starts to go offscreen we’ll need to subtract 8 pixels from the edge, so if player1.x goes beyond position 247 (255 - 8).

  1. Find the point at which we first update the player’s x position. It should say something like player1.x **+=** player1.vx;

    First we’ll stop the player from going past either edges of the screen. New code in bold:

    player1.x += (player1.vx / 4);

    **// handle edges (screen-flip)
    if (player1.x < 8)
    {
    player1.x = 8;
    }
    else if (player1.x > 247)
    {
    player1.x = 247;
    }**
  2. Try this out. You should now find that your player is unable to move beyond either edge of the screen. Do you understand how it works?

    Depending on what your backdrop colour is, you may see that the player actually stops 8 pixels short of the left hand edge:

    Untitled

    Of course this is what we were discussing just before about the 8 pixel strip on the left hand side of the screen. If you have a black backdrop you won’t notice this, since the backdrop colour is the same as the black background.

    The SMS VDP actually has a feature we can use to our advantage here: there’s a setting which, when enabled, will blank off the first 8 pixels of the display with the same colour as the backdrop. Let’s turn it on!

  3. Put the following line (in bold below) anywhere in main before the call to load_screen():

    void main(void)
    {
    // ...anywhere from here...

    **SMS_VDPturnOnFeature(VDPFEATURE_LEFTCOLBLANK);**

    // ...before this line
    load_screen();

    SMS_displayOn();

    // main game loop
    while (1)
    {
    // ... etc. ...

    Now you should find that your backdrop colour extends 8 pixels further into the screen and so you can walk right up to it:

    Untitled

    Of course, now there are only 248 usable pixels on screen, rather than 256, and if you had defined any column data in column 0 it will now be obscured by the backdrop. Let’s choose not to care about that.

    tip

    ℹ️ The “left column blank feature” we have just used is primarily designed to allow sprites to move smoothly past the left-hand edge of the screen, although that isn’t actually relevant in our game, because we are not allowing the characters to moving past the edges of the screen anyway.

    Because the SMS VDP is 256 pixels wide, and because there is only a single 8-bit byte which holds the sprite x positions, it means that there is no way to instruct the VDP to start painting a sprite at negative positions. e.g. you can’t say to the VDP: “position this sprite at -2” if you wanted only the right-most 6 pixels to appear on the left-hand edge of the screen. To work around this restriction, you can blank off the left hand column, which allows part of the sprite to be visible and part of it to be obscured, giving the illusion that it’s really moved offscreen to the left.

    There isn’t the same problem with moving sprites past the right-hand edge because the VDP will allow sprites to start anywhere up to position 255 and in that case it simply doesn’t draw the parts of the sprite which would be offscreen.

Flip-screen to the left​

Right, we’re ready to start flippin’.

  1. We’re going to handle the left-hand edge first, so for convenience let’s make sure that we initialise our game with some non-zero screen to start with, say screen 1:

    int current_screen = 1;

    We wrote this line near the very top of your main.c file (if you ever struggle to find lines in your code, remember you can do Cmd-F to find any text in your file)

  2. Now let’s modify our edge-handling code to flip the screen. New code in bold:

    // handle screen flip
    if (player1.x < 8)
    {
    **if (current_screen > 0)
    {
    current_screen -= 1;
    load_screen();
    player1.x = 247;
    }
    else
    {**
    player1.x = 8;
    **}**
    }
    else if (player1.x > 247)
    {
    // ... etc. ...

    Let’s take a look at this line by line:

    if (current_screen) > 0)

    If we’re already on the left-most screen we can’t go any further to the left, so we do nothing — just set player1.x to be 8 and prevent the player from walking any further, just like before.

    current_screen -= 1;
    load_screen();

    These two lines are the actual screen-flipping. We decrement current_screen by 1, and then trigger the screen to repaint by calling load_screen(). Simples!

    player1.x = 247;

    Here we are transporting the player to the right hand edge of the screen to give the illusion like they just walked in from the right. We set the position to 247 because that’s the last allowable point on the screen. If we had set the position to, say 255, they would have been immediately sent back to this screen one frame later and maybe even ping-ponged between the two screens — what a headache!

    **else
    {**
    player1.x = 8;
    **}**

    Since we are now setting player1.x to be 247 we don’t want to overwrite it with the value 8, which now only applies in the case when we are already on screen 0.

  3. Test your code. You should find that you can walk from screen 1 to screen 0 off the left-hand edge of the screen! However you won’t be able to get back to screen 1 no way no how, because we haven’t written that code yet.

Flip screen to the right​

Do you want to have a go at writing it yourself??

If so, good on you!

If not, or if you gave up, expand this toggle for the answer!
  // ... etc. ...
}
else if (player1.x > 247)
{
**if (current_screen < SCREENS - 1)
{
current_screen += 1;
load_screen();
player1.x = 8;
} else {**
player1.x = 247;
**}**
}

Most of this should have been fairly obvious. Did you figure out that in order to stop the player moving past the last screen you needed to check against SCREEN - 1 rather than just SCREEN? If you got it wrong, just see how it affects your game before correcting.

You should also have transported the player to position 8 rather than 247 (or 0) after flipping the screen. Again, if you wrote something different why not see what effect it had before correcting.

Okay that’s flip-screen done!

Feel free to play around designing different screens, you can add as many as you like (well actually you can probably add about 1,500 of them before you run out of address space but I assume you won’t want to hand-craft quite so many screens as that…)

Goal interaction​

Now we’re going to do something a little more sophisticated with our goal sprite — we want to be able to interact with it. You may have noticed that the goal sprite reoccurs on every screen of our game, which is a bit odd. The first thing we’ll do is rewrite so that the goal is only present on one specific screen, a little like the player can only be present on one screen at a time. Then we’ll detect when the player walks over the goal, and make the game do something when that happens, but more on that later.

Refactor: specifying object screens​

We introduced the current_screen variable in the last section, to keep track of which screen the player was on, but the more we think about it, the more we probably want all of our game objects (i.e. the player, the goal and — later — enemies) to know which screen they are on. After all, game objects know what their x and y positions are, but now that we have multiple screens we really want to specify those x and y positions in combination with a screen number. That’s why our goal currently appears on all three screens — because we only hold the x and y values for it, not the screen number.

  1. Let’s modify our Object struct to give it a new field. The new code is in bold:

    **typedef** struct Object
    {
    uint8_t x;
    uint8_t y;
    int8_t vx;
    int8_t vy;
    **uint8_t screen;**

    } **Object**;

    You’ll notice that as well as specifying the new field screen, we have also added a new keyword typedef in front of our struct definition, and also repeated the identifier Object afterwards. What does this do?

    A typedef in C is a way to alias a type. Here are some examples of typedefs:

    typedef int[2] Point; // Whenever we use the type Point it really means an array of 2 integers
    typedef double Length; // Whenever we use the type Length it really means a double-length floating point number
    typedef unsigned int ScreenIndex; // Whenever we use the type ScreenIndex it really means an unsigned integer
    typedef uint8_t unsigned char; // Whenever we use the type uint8_t it really means an unsigned 8-bit value
    typedef bool int; // whever we use the type bool it really means an integer

    The last two examples, uint8_t and bool we have already seen — the stdint.h and stdbool.h include files respectively define typedefs which create these aliases so we can use them. You might be surprised to learn that, unlike many other programming languages, C doesn’t have a native boolean type for true/false values. Instead when we include stdbool.h at the top of our file, it includes a typedef creating the alias bool, which is really an alias for int; as well as two defines:

    #define false 0
    #define true 1

    If you hold down the Cmd key and click on either stdbool.h or on bool or true or false anywhere in your code, you should be able to see this for yourself.

    The typedef we have used for our Object struct is just for convenience really. In C, the full type descriptor of our Object struct is struct Object so every time we need to reference it, we have to type in the whole thing. We see that when we declare the player1 variable:

    struct Object player1 = {
    ...
    };

    But with our new typedef in place, we can now drop the struct part when we declare variables, because we have created a type alias called Object which actually refers to struct Object:

    Object player1 = {
    ...
    };

    It’s okay to both our typedef and our struct named Object like this because they live in different namespaces, so they don’t clash. But it’s also common to see programs where the typedefs intentionally use different names from the types they are aliasing to make it very explicit what is what. Sometimes they add a suffix like _t to the type, so if we’d have done that perhaps we would have called our struct Object but our type alias Object_t. Note that the stdint.h types like uint8_t use a suffix _t — the _t stands for “type”. But the stdbool.h type alias bool doesn’t have a _t suffix — it’s not bool_t 🤷‍♀️.

    We’ve made our screen field a uint8_t which means an 8-bit unsigned integer, so we should be able to accommodate up to 256 screens in our game — that’s more than enough!

    Having created a screen field on Object, we should now be able to get rid of the current_screen variable we created before and use the player1.screen field instead to track what screen we (the player) are on.

  2. First let’s explicitly initialise the field so that it starts with the player on the first screen 0:

    Object player1 = {
    .vx = 0,
    .vy = 0,
    **.screen = 0**
    };
  3. Next let’s delete entirely the line that declares the current_screen variable. It says something like:

    ```c
    int current_screen = 0;
    ```
  4. Now find and replace (using Cmd-Opt-F or Cmd-Shift-L) all remaining instances of the variable current_screen and instead write player1.screen.

  5. At this point you should be able to rebuild and see that everything still works, including screen-flipping.

  6. Okay, that’s our player object updated to have a screen, let’s do the same for our goal. We’ll need a new variable of type Object to hold the goal information. You can put this right underneath the player1 initialization we just modified. You just need the new code in bold

      ...
    .screen = 0,
    };

    **Object goal = {
    .x = 120,
    .y = 64,
    .screen = 0
    };**
  7. Now let’s make use of our new goal variable. Here’s how your code should currently look which draws the sprites:

    // 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);

    Notice that we have two calls to SMS_addSprite — one for the player and the other for the goal. We’ll change the second call so that we use position information from the goal object instead of hard-coding values:

    // 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(
    goal.x,
    MAP_Y(goal.y + GOAL_HEIGHT),
    SPRITE_GOAL
    );**
  8. Great, now the two calls to SMS_addSprite are starting to look more similar. It seems a little strange now that we have a PLAYER_HEIGHT define as well as a GOAL_HEIGHT define — both values are 8. Perhaps it would be better if we made height a property of our Object struct:

    typedef struct Object
    {
    uint8_t x;
    uint8_t y;
    int8_t vx;
    int8_t vy;
    uint8_t screen;
    **uint8_t height;**
    } Object;

    Now we can initialise the height of both player1 and goal to be 8:

    Object player1 = {
    .vx = 0,
    .vy = 0,
    ****.screen = 0,
    ******************.height = 8******************
    };

    Object goal = {
    .vx = 120,
    .vy = 64,
    ****.screen = 0,
    ******************.height = 8******************
    };

    and finally we can use the height field instead of the PLAYER_HEIGHT and GOAL_HEIGHT defines:

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

    Of course we can now delete the define because it is no longer used:

    **// delete this line
    #define PLAYER_HEIGHT
    ...**

    By replacing the defines with fields in variables, we’ve made our program a little less efficient, but we’ve also made it more flexible because we can now handle Objects with different heights if we want to. Programming is full of compromises like these that need to be considered all the time.

  9. We have one more thing to do, which is to respect the screen that the goal is set to display on, but first it’s maybe a good idea to rebuild and make sure your game still works the same as before.

    How should we handle the screen field for the goal? We’re already handling it for the player — we use the value of screen to decide which set of 32 columns we want to display and use for scenery collision. This is because there is something fundamental but implicit about the player: we always draw the screen that the player is on. It’s a little like there is a virtual “camera” that’s following the player wherever she goes. If the player moves from screen 0 to screen 1, well then our “camera” follows them and next time we render the background we want to render screen 1's background rather than screen 0's.

    The goal has different logic though. Our “camera” doesn’t follow the goal around like it follows the player. Instead, we only want to print the goal to the screen if the goal lives on the screen which we are currently displaying. How do we know which screen we are currently displaying? Well, it will be the same screen as the player is on, won’t it? We just need to make this change to our sprite rendering code:

    // draw the sprites
    SMS_initSprites();
    SMS_addSprite(
    player1.x,
    MAP_Y(player1.y + PLAYER_HEIGHT)**,**
    player1.facing_left ? SPRITE_PLAYER_L : SPRITE_PLAYER_R
    ****);
    **if (goal.screen == player1.screen)
    {**
    SMS_addSprite(
    goal.x,
    MAP_Y(goal.y + GOAL_HEIGHT),
    SPRITE_GOAL
    );
    **}**

    So we only display the goal sprite on the screen if the goal is on the same screen as the player. Make sense?

  10. Try building this and test it out. You should find that now the goal sprite is only visible when the player is on the first screen, 0, because that’s what we set the goal.screen field to be.

    You can play around with setting the .screen field of the goal object to different values and see how it changes things. What do you think will happen if you give it a value of greater than 3 for example — i.e. put it on a screen that doesn’t “exist”?

Collision detection​

Let’s think for a moment about how we would detect mathematically whether two square objects coincide:

Untitled

These two squares do not coincide, right? But how do you know?

Looking at the edges of sprite1, we can see that sprite1’s top edge lies somewhere in between sprite2’s top edge and its bottom edge. However, sprite1’s right-hand edge lies completely to the left of sprite2’s left-hand edge, so these shapes cannot possibly overlap. If we wanted them to overlap, we would have to “slide” sprite1 to the right, until its right-hand edge overlapped sprite2’s left-hand edge.

In our screen coordinate system, we can say that sprite1’s left-hand edge is at sprite1.x and its right-hand edge is at sprite1.x + sprite.width. We can make similar observations about sprite1’s bottom edge vs. its height, as well as the same for sprite2.

Given this, let’s write down a set of inequalities about the two shapes above.

// sprite1's left edge < sprite2's right edge?
sprite1.x < sprite2.x + sprite2.width - TRUE
// sprite1's right edge > sprite2's left edge?
**sprite1.x + sprite1.width > sprite2.x - FALSE**
// sprite1's top edge < sprite2's bottom edge?
sprite1.y < sprite2.y + sprite2.height - TRUE
// sprite1's bottom edge > sprite2's top edge?
sprite1.y + sprite1.height > sprite2.y - TRUE

The second condition, marked above in bold, is the one which proves that these two shapes don’t coincide. Since sprite1’s right edge is smaller (further to the left) than sprite2’s left edge, there’s no way the shapes can overlap; no matter how far up or down the screen we slide either of them, horizontally they don’t coincide.

We can make similar arguments for other arrangements:

Untitled

// sprite1's left edge < sprite2's right edge?
**sprite1.x < sprite2.x + sprite2.width - FALSE**
// sprite1's right edge > sprite2's left edge?
sprite1.x + sprite1.width > sprite2.x - TRUE
// sprite1's top edge < sprite2's bottom edge?
sprite1.y < sprite2.y + sprite2.height - TRUE
// sprite1's bottom edge > sprite2's top edge?
sprite1.y + sprite1.height > sprite2.y - TRUE

Again, the vertical positions of these two shapes do coincide, but this time sprite1’s left edge is too far to the right of sprite2’s right edge, so the first test fails.

Untitled

// sprite1's left edge < sprite2's right edge?
sprite1.x < sprite2.x + sprite2.width - TRUE
// sprite1's right edge > sprite2's left edge?
sprite1.x + sprite1.width > sprite2.x - TRUE
// sprite1's top edge < sprite2's bottom edge?
sprite1.y < sprite2.y + sprite2.height - TRUE
// sprite1's bottom edge > sprite2's top edge?
**sprite1.y + sprite1.height > sprite2.y - FALSE**

This time we have horizontal coincidence but vertically sprite2’s top edge is too low compared with sprite1’s bottom edge.

Untitled

// sprite1's left edge < sprite2's right edge?
sprite1.x < sprite2.x + sprite2.width - TRUE
// sprite1's right edge > sprite2's left edge?
sprite1.x + sprite1.width > sprite2.x - TRUE
// sprite1's top edge < sprite2's bottom edge?
**sprite1.y < sprite2.y + sprite2.height - FALSE**
// sprite1's bottom edge > sprite2's top edge?
sprite1.y + sprite1.height > sprite2.y - TRUE

Similarly, here sprite1’s top edge is too low compared with sprite2’s bottom edge.

Untitled

// sprite1's left edge < sprite2's right edge?
**sprite1.x < sprite2.x + sprite2.width - FALSE**
// sprite1's right edge > sprite2's left edge?
sprite1.x + sprite1.width > sprite2.x - TRUE
// sprite1's top edge < sprite2's bottom edge?
**sprite1.y < sprite2.y + sprite2.height - FALSE**
// sprite1's bottom edge > sprite2's top edge?
sprite1.y + sprite1.height > sprite2.y - TRUE

Here we have not just one but two conditions failing for coincidence: sprite1’s top edge is lower than sprite2’s bottom edge, AND sprite1’s left edge is greater than sprite2’s right edge.

Indeed, the only way these two shapes can coincide is if all these conditions are true.

Untitled

// sprite1's left edge < sprite2's right edge?
sprite1.x < sprite2.x + sprite2.width - TRUE
// sprite1's right edge > sprite2's left edge?
sprite1.x + sprite1.width > sprite2.x - TRUE
// sprite1's top edge < sprite2's bottom edge?
sprite1.y < sprite2.y + sprite2.height - TRUE
// sprite1's bottom edge > sprite2's top edge?
sprite1.y + sprite1.height > sprite2.y - TRUE

Spend some time and convince yourself that all four conditions are true.

It works the same when we swap sprite1 and sprite2 around of course:

Untitled

Now we understand how we can decide whether two rectangles coincide, we can implement a simple collision detection by treating each sprite as a rectangle (or square). Such a mechanism is commonly called a “hitbox”.

  1. In order to store a hitbox for each Object, we’ll need to store four things:
  • The x coordinate

  • The y coordinate (the x and y coordinates together form the *“origin”*, or reference point for the hitbox)

  • The height of the hitbox

  • The width of the hitbox

    We already store the first three of these things, so we need only to add a field to store the width of the hitbox.

    Let’s add a new width field to our Object struct:

    typedef struct Object
    {
    uint8_t x;
    uint8_t y;
    int8_t vx;
    int8_t vy;
    uint8_t screen;
    uint8_t height;
    **uint8_t width;**
    } Object;

    and we’ll also initialise the width for our player and goal as 8 pixels:

    Object player1 = {
    .vx = 0,
    .vy = 0,
    .height = 8,
    **.width = 8**
    };

    Object goal = {
    .x = 120,
    .y = 64,
    .height = 8,
    **.width = 8**
    };
  1. Now we’ll implement the hitbox collision detection. We’ll do this after we’ve done all the scenery interactions, and before we draw the sprites:

    **// handle goal collision
    if ((goal.screen == player1.screen)
    && (player1.x < (goal.x + goal.width))
    && ((player1.x + player1.width) > goal.x)
    && (player1.y < (goal.y + goal.height))
    && ((player1.y + player1.height) > goal.y))
    {
    goal.screen += 1;
    goal.y -= 20;
    goal.x += 20;
    }**

    // draw the sprites
    ...

    Notice that we’ve added one more condition to the hitbox inequalities we discussed above:

    if ((goal.screen == player1.screen) ...

    This ensures that we only do the collision detection when the player and the goal are on the same screen. Otherwise we would get the odd behaviour of detecting a goal collision when the player walked over where the goal would be on another screen. C, like many languages, performs something called “short-circuit evaluation” when it evaluates boolean expressions. That means that if, for example in this situation, goal.screen does NOT equal player1.screen then none of the other four inequality conditions will be evaluated at all — there is no point calculating them because the overall result would always be false if any of the conditions was false. This saves the computer from having to do calculations that it doesn’t actually need to. For this reason it’s usually a good idea to think about the order you write your conditions in an if statement — you might put the more “expensive” (i.e. computationally taxing) calculations later to make your program more efficient.

    You’ll see that in the case where we detect a hitbox collision, we increase the screen field of the goal object by 1, and also move it diagonally upwards, 20 pixels to the right and 20 pixels upwards. This is arbitrary and to demonstrate the collision detection — you can do anything you like when you detect your collision.

  2. Build and test. Your player and the goal should both start out on the first screen. If you walk (or jump!) past the goal, and onto the second and/or third screens you should see that the goal is still only on the first screen. Now if you intentionally collide with the goal, you’ll hopefully see it disappear! It has moved to the next screen along — go and find it there!

    Play around with entering the hitbox from different angles — you should find that whether it’s from the left, the right, above or below; the goal always disappears just as the square around the player coincides with the square around the goal.

Hardware sprite collision​

Using rectangular bounding boxes for hitbox collision is very easy — we only need to make four comparisons to detect a collision (five if you include the screen comparison), and in the case where objects do not coincide we can expect the boolean expression to terminate as soon as one of the conditions fails, so it’s relatively efficient for the case where we’re not colliding (most of the time).

However there’s a fairly obvious disadvantage with rectangular hitboxes — our sprites generally aren’t perfect rectangles. Usually real sprites are somewhat rounded, and often they don’t fill all the way to the corners of the bounding box. My example player sprite has transparent pixels in both the top right and top left corners, and my goal tile has transparent pixels in all four corners:

Untitled

If yours are the same, you should be able to observe this approximation by carefully aiming a collision between the empty corners of the player and the goal. The apparent effect will be that the collision occurs when the player and goal don’t appear to have actually made contact yet.

To solve this problem perfectly in software would require some much more sophisticated techniques, like for example comparing every pixel of both sprites. However the SMS provides a hardware solution for this problem:

The VDP has a special register inside it called the status register and every time two non-transparent pixels of two or more sprites overlap on screen one bit of this status register will be set.

SMSLib reads these status bits once per frame, and stores them in a variable called SMS_VDPFlags, which we can interrogate to see if the VDP detected a sprite collision. Unfortunately the VDP sprite collision detection can’t tell us which two sprites collided, so its usefulness is somewhat limited. Right now, we only have two sprites — the player and the goal — so we could actually use hardware sprite detection alone, instead of the hitbox detection we just wrote. However, we’ll soon be adding enemy sprites to our game and then we’ll definitely need hitbox detection to differentiate between when our player collides with a goal or with an enemy.

  1. Wrap our hitbox detection in an if statement which interrogates the VDP hardware collision bit:

    **if (SMS_VDPFlags & VDPFLAG_SPRITECOLLISION) {**
    // handle goal collision
    if ((goal.screen == player1.screen)
    && (player1.x < (goal.x + goal.width))
    && ((player1.x + player1.width) > goal.x)
    && (player1.y < (goal.y + goal.height))
    && ((player1.y + player1.height) > goal.y))
    {
    goal.screen += 1;
    goal.y -= 20;
    goal.x += 20;
    }
    **}**

    // draw the sprites
    ...

This modified code will only bother to do the hitbox calculations if the VDP has told us there is a sprite collision. Remember also that the VDP’s hardware collision only triggers if two non-transparent pixels overlap. That should solve our problem with non-rectangular sprites as now the transparent pixels in the corners of sprites will not trigger the VDP sprite collision and so we won’t execute our hitbox code either. Try it out!

Enemies​

WIP — ignore me!

  1. We are going to make a few more tweaks to our function to make it work a bit better. The new code here is in bold:

    void load_screen(void)
    {
    **SMS_waitForVBlank();
    SMS_displayOff();**
    SMS_setNextTileatLoc(0);
    for (int row = 23; row >= 0; row--)
    {
    for (int col = 0; col < 32; col++)
    {
    if ((screens[current_screen][col] - row) == 1)
    SMS_setTile(TILE_GRASS);
    else if ((screens[current_screen][col] - row) > 1)
    SMS_setTile(TILE_EARTH);
    else
    SMS_setTile(TILE_BLANK);
    }
    }
    **SMS_waitForVBlank();
    SMS_displayOn();**
    }

    The extra call to SMS_waitForVBlank() on the first line forces the function to wait until the active display has finished drawing before it starts its work. This minimises the changes of us changing the display while it’s drawing to screen. To do so could introduce a very brief but noticeable visual glitch where part of the screen is drawn and then it suddenly starts to redraw something different or nothing at all. We call these kinds of visual glitches tearing, because it can resemble a torn piece of paper.

    We are also wrapping our redraw with SMS_displayOff() and SMS_displayOn(). This ensures that the redraw happens as fast as possible, because the SMS VDP can do things much faster when the screen is off. Although the screen is automatically “off” immediately after a VBlank, the blanking period only last for a limited amount of time, and we may not have finished all our redrawing by the time the screen starts drawing again. This way we are in control of when we turn the screen back on. Again, we do an SMS_waitForVBlank() immediately before we turn the screen back on. The previous SMS_waitForVBlank() prevents us from tearing the screen as it turns off, but this SMS_waitForVBlank() prevents us from tearing the screen as it turns on again. Both turning the screen off and on could cause a tearing glitch, but they would look opposite from each other — if the screen turned off midway through the scan it would leave some pixels in the top half of the screen and then an empty bottom half; whereas if the screen turned on midway through the scan it would leave an empty top half and some pixels in the bottom half, under the “tear”.

  2. Since we are always turning the screen on at the end of load_screen that means we don’t need to do it from inside main any more, so if you spot a call to SMS_displayOn() inside main you can delete it. In fact, you can (and should) arrange it so that load_screen is the final thing which happens just before we start our main loop with while (1):

    // ... etc. ...

    **load_screen();**

    // main game loop
    while (1)
    {
    // ... etc. ...