Megalaga 7 - Enemy Bullets
NOTE:
This tutorial is most likely not compatible with versions of SGDK above 1.70.
Unfortunately, I simply do not have the capacity or ability to update them right now. Read more about it here. Sorry.
So far, enemies are nothing more than cannon fodder. They don’t shoot or even move towards the player, so there’s not really any way to fail the game. Let’s change that by having enemies shoot at the player!
Shooting Bullets
We will be efficient and use our existing shootBullet()
function to handle the enemy bullets. To do that, first we’ll need to change two defines. All bullets in the game will come from the same pool, so we need to increase the max amount of bullets that can be visible at one time. When only 3 bullets can be on the screen at once, there will not be much shooting going on, which is not really exciting. Additionally, we will define the bottom edge of the screen, so we can check if the enemy bullets go offscreen (since they move downwards, they can only leave the screen at the bottom).
#define BOTTOM_EDGE 224
#define MAX_BULLETS 6
Now we modify shootBullet()
. So far this function only dealt with the player, but now we want to make it more universal. To that end, we’ll make it so you pass in the entity that is shooting. This way we can use the same function for both player and enemies. Add an argument to the definition:
void shootBullet(Entity Shooter){
}
And then update the one call to the function we have, which is inside the joypad callback:
if (state & BUTTON_B & changed)
{
shootBullet(player);
}
Now we need to figure out a way to see who is shooting. If the player is shooting the bullets will go up, while enemy bullets will move downwards. We’ll also have to flip the bullet sprite vertically depending on where it’s going. So how do we figure out the shooter? There are many approaches, but we’re gonna be clever and keep it simple. In our game, the player is always at the bottom of the screen, while enemies are always at the top. So we can simply check the y-position of the Shooter
we’re passing into the function. If the entity is below, say, 100, it has to be the player. Anything above that is an enemy. So add this line at the beginning of shootBullet
:
bool fromPlayer = (Shooter.y > 100);
Next we need to modify these two lines:
b->x = player.x + 4;
b->y = player.y;
These place the bullet at the player’s position before firing them. But since our function is supposed to be universal now, we have to replace the player reference like this:
b->x = Shooter.x + 4;
b->y = Shooter.y;
This shows how useful structs are. Since the player and enemies are both Entity
-structs, both are guaranteed to have y-values, so we can simply replace player
with Shooter
here.
This places the bullet at the right spot, now to handle the actual firing. Since now we have two possible directions for bullets (up or down), we need to add an if-statement to set the proper velocity. After reviveEntity(b);
replace the line b->vely = -3;
with this:
if(fromPlayer == TRUE){
b->vely = -3;
} else{
b->vely = 3;
}
So if the player is shooting, we shoot the bullet upwards, otherwise we shoot it downwards. We also flip the sprite vertically, so it points in the right direction.
And finally, we have to expand our check if the bullets are on screen or not. So far, we’ve only checked if a bullet leaves the upper edge of the screen, but now we’ll have to check the bottom edge as well. Otherwise the bullets shot by enemies would loop forever, which is not exactly fair. So add an else if
-statement to our function positionBullets
so that it looks like this:
if (b->y + b->h < 0)
{
killEntity(b);
bulletsOnScreen--;
} else if(b->y > BOTTOM_EDGE){
killEntity(b);
bulletsOnScreen--;
} else { ... }
And now we can use our shootBullet
function for enemies as well! But of course the enemies are not shooting yet, because we never tell them to. But if we did, we’d run into a problem: We’re always checking for collisions between bullets and enemies. This means that the moment an enemy shoots a bullet, they get killed by it. Also, the player would never get hit a by bullet, because we don’t check for that collision. So let’s change that!
Collision
We will fix the collision issues in handleCollisions()
. Take a minute to re-familiarize yourself with the function. We cycle through bullets in our pool and grab one. If it is dead, we take it and check it against all enemies. And this is where we have to add a new condition. Because now we don’t want to just check collisions with the enemy, but also with the player. At the same time, we want to not check for certain collisions; if an enemy shoots a bullet, that bullet should only collide with the player, not with enemies.
How do we decide what collisions to check for? We can again use a clever trick. If a bullet travels upwards, it has to come from the player and should therefore only collide with enemies. If a bullet moves downwards, it’s other way around; we’ll only check for collisions with the player, so the enemies won’t blow themselves up.
In code terms this means wrapping our existing code in a new if
-statement, then adding an else
-statement where we check for collision with the player:
void handleCollisions()
{
// ...
if (b->health > 0)
{
if(b->vely < 0){ //This is new!
for (j = 0; j < MAX_ENEMIES; j++)
{
e = &enemies[j];
//...
}
} else{ //Also this
if(collideEntities(b,&player)){
killEntity(&player);
}
}
}
}
And that’s all we need to do. Now bullets will not collide with whoever shot them, only the other party. Note that killEntity
takes a pointer, so we have to pass in the address of the player
entity, not the entity itself.
Making enemies shoot
We’ve already done a lot, but enemies still aren’t actually shooting. Let’s finally take care of that now. We’ll do it like this: We will have a timer running that triggers every 2 seconds. When it triggers, we give each enemy in turn a random chance to fire. If one fires, we restart the timer. That way it is random which enemy shoots and when, but we don’t flood the screen with bullets. Of course there are other ways to handle this, but I find this one works well enough.
First we’ll define the interval of shots. At the top of main.c
add a define:
#define SHOT_INTERVAL 120
Then we need a variable that acts as our timer:
u16 shotTicker = 0;
Now to implement the shooting. We will do this in the positionEnemies()
function. This makes the function seem slightly misnamed, but it’s the most convenient place to put our new code. We want to give each enemy a chance to shoot, and in positionEnemies()
we’re already looping through all of them. So at the very top of the function, increase the shotTicker
:
void positionEnemies()
{
shotTicker++;
//...
We’ll add the shooting code at the end of that function, after we use SPR_setPosition
to position the enemy sprite on screen. Start with an if
-statement:
//...
e->x += e->velx;
SPR_setPosition(e->sprite, e->x, e->y);
/*Shooting*/
if(shotTicker >= SHOT_INTERVAL){
}
If shotTicker
equals the defined SHOT_INTERVAL
, it is time to shoot. Our game will run at 60fps, so a SHOT_INTERVAL
of 120 equals 2 seconds. Now, inside the if
-statement add this:
if( (random() % (10-1+1)+1) > 4 ){
shootBullet(*e);
shotTicker = 0;
}
That condition might seem familiar to you. That’s right, it’s the same formula we used in part 1 of this tutorial to generate a random space background! Basically we’re generating a number between 1 and 10. If that number is bigger than 4, the enemy fires, in which case we also reset the shotTicker
. If the number is less than 4, positionEnemies()
will loop to the next enemy and give him a chance to shoot. This will loop until one of the enemies fires and the cycle starts anew.
And that is it! Now we have enemies that randomly fire bullets that can kill us, but not them. And we did it all by extending some of our functions! Now that’s good programming.
However…there is actually a slight potential issue with our code. It concerns the fact that both players and enemies share the same bullet pool. We’ll fix it next time, but feel free to look at the code again and try to figure out what I’m talking about! Until next time and be excellent to each other!
If you've got problems or questions, join the official SGDK Discord! It's full of people a lot smarter and skilled than me. Of course you're also welcome to just hang out and have fun!
Want To Buy Me a Coffee?
Coffee rules, and it keeps me going! I'll take beer too, though.
Check out the rest of this tutorial series!
Comments
By using the Disqus service you confirm that you have read and agreed to the privacy policy.
comments powered by Disqus