Sound effects, flip-screen, goal interaction and enemies
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!
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.
Add some new defines to hold the constant array sizes:
#define SCREENS 1
#define COLUMNS_PER_SCREEN 32Change the declaration of the
columns
variable fromuint8_t columns[32]
touint8_t screens[SCREENS][COLUMNS_PER_SCREEN]
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 thecolumns
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 insidescreens
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 asconst
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 toscreens[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 thescreens[0]
are about. If we defined some more screens and instead ofscreens[0]
we wrotescreens[1]
for example, we would be working with a different set of columns. Of course, rather than manually changing the screen number from0
to1
etc. we could use a variable.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.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, alongsideplayer1
.(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]
toscreen[current_screen]
.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
from0
to1
or2
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, souint8_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.
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 saysvoid main(void)
). The new code is in bold here:**void load_screen(void)
{
}**
void main(void)
{
//... etc. ...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 nestedfor
loops. Highlight all that code and cut it to the clipboard withCmd-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.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).
Find the point at which we first update the player’s
x
position. It should say something likeplayer1.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;
}**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:
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!
Put the following line (in bold below) anywhere in
main
before the call toload_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:
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’.
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 doCmd-F
to find any text in your file)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 be8
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 callingload_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 be247
we don’t want to overwrite it with the value8
, which now only applies in the case when we are already on screen0
.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.
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 keywordtypedef
in front of our struct definition, and also repeated the identifierObject
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 integerThe last two examples,
uint8_t
andbool
we have already seen — thestdint.h
andstdbool.h
include files respectively definetypedefs
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 includestdbool.h
at the top of our file, it includes atypedef
creating the aliasbool
, which is really an alias forint
; as well as two defines:#define false 0
#define true 1If you hold down the
Cmd
key and click on eitherstdbool.h
or onbool
ortrue
orfalse
anywhere in your code, you should be able to see this for yourself.The
typedef
we have used for ourObject
struct is just for convenience really. In C, the full type descriptor of ourObject
struct isstruct Object
so every time we need to reference it, we have to type in the whole thing. We see that when we declare theplayer1
variable:struct Object player1 = {
...
};But with our new
typedef
in place, we can now drop thestruct
part when we declare variables, because we have created a type alias calledObject
which actually refers tostruct Object
:Object player1 = {
...
};It’s okay to both our
typedef
and ourstruct
namedObject
like this because they live in different namespaces, so they don’t clash. But it’s also common to see programs where thetypedefs
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 ourstruct
Object
but our type aliasObject_t
. Note that thestdint.h
types likeuint8_t
use a suffix_t
— the_t
stands for “type”. But thestdbool.h
type aliasbool
doesn’t have a_t
suffix — it’s notbool_t
🤷‍♀️.We’ve made our
screen
field auint8_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 onObject
, we should now be able to get rid of thecurrent_screen
variable we created before and use theplayer1.screen
field instead to track what screen we (the player) are on.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**
};Next let’s delete entirely the line that declares the
current_screen
variable. It says something like:```c
int current_screen = 0;
```Now find and replace (using
Cmd-Opt-F
orCmd-Shift-L
) all remaining instances of the variablecurrent_screen
and instead writeplayer1.screen
.At this point you should be able to rebuild and see that everything still works, including screen-flipping.
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 theplayer1
initialization we just modified. You just need the new code in bold...
.screen = 0,
};
**Object goal = {
.x = 120,
.y = 64,
.screen = 0
};**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 thegoal
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
);**Great, now the two calls to
SMS_addSprite
are starting to look more similar. It seems a little strange now that we have aPLAYER_HEIGHT
define as well as aGOAL_HEIGHT
define — both values are8
. Perhaps it would be better if we madeheight
a property of ourObject
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
andgoal
to be8
: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 thePLAYER_HEIGHT
andGOAL_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.
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 ofscreen
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 screen0
to screen1
, well then our “camera” follows them and next time we render the background we want to render screen1
's background rather than screen0
'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?
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 thegoal.screen
field to be.You can play around with setting the
.screen
field of thegoal
object to different values and see how it changes things. What do you think will happen if you give it a value of greater than3
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:
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:
// 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.
// 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.
// 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.
// 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.
// 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:
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”.
- 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 ourObject
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**
};
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 equalplayer1.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 anif
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 thegoal
object by1
, and also move it diagonally upwards,20
pixels to the right and20
pixels upwards. This is arbitrary and to demonstrate the collision detection — you can do anything you like when you detect your collision.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:
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.
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!
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()
andSMS_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 anSMS_waitForVBlank()
immediately before we turn the screen back on. The previousSMS_waitForVBlank()
prevents us from tearing the screen as it turns off, but thisSMS_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”.Since we are always turning the screen on at the end of
load_screen
that means we don’t need to do it from insidemain
any more, so if you spot a call toSMS_displayOn()
insidemain
you can delete it. In fact, you can (and should) arrange it so thatload_screen
is the final thing which happens just before we start our main loop withwhile (1)
:// ... etc. ...
**load_screen();**
// main game loop
while (1)
{
// ... etc. ...