PC Games

Orb
Lasagne Monsters
Three Guys Apocalypse
Water Closet
Blob Wars : Attrition
The Legend of Edgar
TBFTSS: The Pandoran War
Three Guys
Blob Wars : Blob and Conquer
Blob Wars : Metal Blob Solid
Project: Starfighter
TANX Squadron

Android Games

DDDDD
Number Blocks
Match 3 Warriors

Tutorials

2D shoot 'em up
2D top-down shooter
2D platform game
Sprite atlas tutorial
Working with TTF fonts
2D adventure game
Widget tutorial
2D shoot 'em up sequel
2D run and gun
Roguelike
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Rogue tutorial
Wed, 29th September 2021

SDL2 Gunner tutorial
Thu, 26th August 2021

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (6)
games (37)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (8)
water-closet (3)

Books

« Back to tutorial listing

— 2D Shoot 'Em Up Tutorial —
Part 9: Effects and background graphics

Introduction

Note: this tutorial builds upon the ones that came before it. If you aren't familiar with the previous tutorials in this series you should read those first.

We can shoot the enemies to destroy them, but right now they just vanish. That's no fun. How about we make them explode, instead? Unpack the code and then type make to build. Once compiling is finished type ./shooter09 to run the code.

A 1280 x 720 window will open, with a colorful background. A spaceship sprite will also be shown, as in the screenshot above. The ship can now be moved using the arrow keys. Up, down, left, and right will move the ship in the respective directions. You can also fire by holding down the left control key. Enemies (basically red versions of the player's ship) will spawn from the right and move to the left. Shoot enemies to destroy them. Enemies can fire back, so you should avoid their shots. Close the window by clicking on the window's close button.

Inspecting the code

This update could be summarized as the "prettification" stage. Because we've added effects such as explosions, debris, a starfield, and a scrolling background, there have been a considerable number of new additions made. This update is therefore longer than most. We'll start by taking a look at the changes made in structs.h and defs.h.

In structs.h we've added two new declarations:


struct Explosion {
	float x;
	float y;
	float dx;
	float dy;
	int r, g, b, a;
	Explosion *next;
};

struct Debris {
	float x;
	float y;
	float dx;
	float dy;
	SDL_Rect rect;
	SDL_Texture *texture;
	int life;
	Debris *next;
};

typedef struct {
	...
	Explosion explosionHead, *explosionTail;
	Debris debrisHead, *debrisTail;
} Stage;

typedef struct {
	int x;
	int y;
	int speed;
} Star;

Explosion will be used to hold details of an explosion, while Debris will do the same for debris that is thrown when a ship is destroyed. Both are linked lists and are introduced into the Stage object. We're also declaring a Star object, as part of that the starfield we'll be making. We'll detail these fully when we get to stage.c.

Next, let's look at defs.h:


#define MAX_STARS   500

Were adding a starfield to the game and will be handling the stars as a fixed sized array, rather than a linked list. We'll put 500 stars on screen at a time (it may sound a lot, but screen resolution means that with only a handful they would not be too visible). Onto draw.c, where a new function has been added:


void blitRect(SDL_Texture *texture, SDL_Rect *src, int x, int y)
{
	SDL_Rect dest;

	dest.x = x;
	dest.y = y;
	dest.w = src->w;
	dest.h = src->h;

	SDL_RenderCopy(app.renderer, texture, src, &dest);
}

blitRect is a second blit function (note: it doesn't replace the existing one). As well as taking a texture and coordinates, it also takes a rectangular region as the second argument. This is used to tell the SDL_RenderCopy function what portion of the source texture we want to draw. This means that we don't have to draw the entire source texture on screen, and can pick and choose what to display. The dest rect takes src rect's w and h as its width and height, and then both src and dest are passed to SDL_RenderCopy. We're going to be using this in stage.c when we come to destroying the ships; they'll be broken into pieces when destroyed, so we'll tell our game to pick some rectangular regions of the source sprite to work with.

As always, it is stage.c that has seen the bulk of the updates:


void initStage(void)
{
	...
	stage.explosionTail = &stage.explosionHead;
	stage.debrisTail = &stage.debrisHead;

	...
	background = loadTexture("gfx/background.png");
	explosionTexture = loadTexture("gfx/explosion.png");

In the initStage function we're now loading a background graphic and a new explosion texture (as well as setting up the explosion and debris linked lists). Nothing major. resetStage also sees some minor updates:


static void resetStage(void)
{
	...
	Explosion *ex;
	Debris *d;

	...
	while (stage.explosionHead.next)
	{
		ex = stage.explosionHead.next;
		stage.explosionHead.next = ex->next;
		free(ex);
	}

	while (stage.debrisHead.next)
	{
		d = stage.debrisHead.next;
		stage.debrisHead.next = d->next;
		free(d);
	}

	...
	stage.explosionTail = &stage.explosionHead;
	stage.debrisTail = &stage.debrisHead;

	...
	initStarfield();

	...
	stageResetTimer = FPS * 3;
}

We're clearing down the linked lists, as expected, but are also calling a new function called initStarfield. You'll notice we're also extending the stage reset time to 3 seconds. This is so that we can better appreciate the player being killed and throwing their own debris about the screen.

Moving onto the first new function initStarfield, we can see that things are quite simple:


static void initStarfield(void)
{
	int i;

	for (i = 0 ; i < MAX_STARS ; i++)
	{
		stars[i].x = rand() % SCREEN_WIDTH;
		stars[i].y = rand() % SCREEN_HEIGHT;
		stars[i].speed = 1 + rand() % 8;
	}
}

We loop through all our stars array (this array can be found a static variable in stage.c) and randomly assigning them positions about the entire screen. We also give them a random speed, from 1 to 3. This speed will not only affect how fast they move but also how bright they appear when we come to draw them.

We'll deal with all the updates to the logic phase of the game now, beginning with the main logic function.


static void logic(void)
{
	doBackground();

	doStarfield();

	...

	doExplosions();

	doDebris();

We're calling four new functions here: doBackground, doStarfield, doExplosions, and doDebris. We'll cover these in order, starting with doBackground:


static void doBackground(void)
{
	if (--backgroundX < -SCREEN_WIDTH)
	{
		backgroundX = 0;
	}
}

The doBackground function simply decrements a variable called backgroundX (local to stage.c), resetting it to 0 if it reaches negative SCREEN_WIDTH (so, about -1280). This means that, when drawing it, the background will wrap around. More on this when we come to drawing. Onto doStarfield:


static void doStarfield(void)
{
	int i;

	for (i = 0 ; i < MAX_STARS ; i++)
	{
		stars[i].x -= stars[i].speed;

		if (stars[i].x < 0)
		{
			stars[i].x = SCREEN_WIDTH + stars[i].x;
		}
	}
}

Nothing out of the ordinary here. We're looping through our stars array and decreasing the star's x value according to speed. When the value of x is less than 0 we'll return it to the other side of the screen. One little thing we're doing is taking into consideration what the negative value was at the time, rather than setting the star's x exactly to SCREEN_WIDTH. This should help to avoid a situation where the stars could all start to line up over time.

doExplosions is the next new function, and again nothing too taxing to consider:


static void doExplosions(void)
{
	Explosion *e, *prev;

	prev = &stage.explosionHead;

	for (e = stage.explosionHead.next ; e != NULL ; e = e->next)
	{
		e->x += e->dx;
		e->y += e->dy;

		if (--e->a <= 0)
		{
			if (e == stage.explosionTail)
			{
				stage.explosionTail = prev;
			}

			prev->next = e->next;
			free(e);
			e = prev;
		}

		prev = e;
	}
}

This function is basically stepping through our list of explosions, moving them according to their dx and dy (to make the explosion appear to be spreading out, rather than sitting in the same place), and decrementing the value of their a variable. The a variable holds the explosion's alpha value. Decrementing this means that the explosion will become less visible as time goes on (we'll cover this when we come to drawing the explosions). When the a value is 0, we'll delete the explosion.

Looking next at doDebris, we'll not see anything too wildly different:


static void doDebris(void)
{
	Debris *d, *prev;

	prev = &stage.debrisHead;

	for (d = stage.debrisHead.next ; d != NULL ; d = d->next)
	{
		d->x += d->dx;
		d->y += d->dy;

		d->dy += 0.5;

		if (--d->life <= 0)
		{
			if (d == stage.debrisTail)
			{
				stage.debrisTail = prev;
			}

			prev->next = d->next;
			free(d);
			d = prev;
		}

		prev = d;
	}
}

Each of our debris items is moved according to their dx and dy, their life decremented, and the debris item deleted when it falls to 0. One small thing we're doing here is increasing the dy value by 0.5. This means that the debris will accelerate down the screen each frame, giving the impression of it falling due to gravity (which is odd, given that we're in space, but hey, they did it in The Last Jedi[1], right..?).

Let's move onto something more interesting - a function to add explosions:


static void addExplosions(int x, int y, int num)
{
	Explosion *e;
	int i;

	for (i = 0 ; i < num ; i++)
	{
		e = malloc(sizeof(Explosion));
		memset(e, 0, sizeof(Explosion));
		stage.explosionTail->next = e;
		stage.explosionTail = e;

		e->x = x + (rand() % 32) - (rand() % 32);
		e->y = y + (rand() % 32) - (rand() % 32);
		e->dx = (rand() % 10) - (rand() % 10);
		e->dy = (rand() % 10) - (rand() % 10);

		e->dx /= 10;
		e->dy /= 10;

		switch (rand() % 4)
		{
			case 0:
				e->r = 255;
				break;

			case 1:
				e->r = 255;
				e->g = 128;
				break;

			case 2:
				e->r = 255;
				e->g = 255;
				break;

			default:
				e->r = 255;
				e->g = 255;
				e->b = 255;
				break;
		}

		e->a = rand() % FPS * 3;
	}
}

This function accepts three parameters: the x and y of the origin of the explosion, and the number of explosion effects we want to add (num). We use a for loop to malloc num number of explosions, setting each one's attributes. The explosion's x and y are set to the ones we passed into the function, plus/minus a random 32 pixels in each direction. We also set the explosion's dx and dy to between plus/minus 9, dividing by 10 to give us -0.9/0.9 so that the explosion doesn't move around too fast. Finally, we set the explosion to a random colour of red, orange, yellow, or white, assigning the explosion's r, g, and b variables to the appropriate value of 255 (SDL colour values go between 0 and 255, from none to full). We also set the explosion's a (alpha) to a random amount of 0 to 3 seconds, governing how long it will live for.

The new addDebris function will be used to shatter our ship into pieces when it's destroyed. We pass over the Entity we wish to work with:


static void addDebris(Entity *e)
{
	Debris *d;
	int x, y, w, h;

	w = e->w / 2;
	h = e->h / 2;

	for (y = 0 ; y <= h ; y += h)
	{
		for (x = 0 ; x <= w ; x += w)
		{
			d = malloc(sizeof(Debris));
			memset(d, 0, sizeof(Debris));
			stage.debrisTail->next = d;
			stage.debrisTail = d;

			d->x = e->x + e->w / 2;
			d->y = e->y + e->h / 2;
			d->dx = (rand() % 5) - (rand() % 5);
			d->dy = -(5 + (rand() % 12));
			d->life = FPS * 2;
			d->texture = e->texture;

			d->rect.x = x;
			d->rect.y = y;
			d->rect.w = w;
			d->rect.h = h;
		}
	}
}

We want to quarter our entity and throw four pieces of debris. First, we divide the entity's w and h by two, then create a for loop across the x and y (incrementing by the w and h values we calculated). We malloc a piece of Debris, setting each one's x and y to the centre of the source entity, and also giving each a random dx velocity. The dy velocity will always be a negative between -5 and -16, meaning that the debris will travel up screen to begin with. If you remember from our doDebris function, the debris will soon begin to move back down. This will give the debris the impression of being thrown up. We set the debris' life to 2 seconds and the texture to that of the source entity. Now comes the important bit: the debris object contains an SDL_Rect which we'll use to hold the texture coordinates. We set the rect's x and y to those of our for loop, and the w and h to the w and h we worked out earlier. In effect, we'll get the texture coordinates of our source texture in quarters.

With all our new logic and processing dealt with, we can move onto the rendering phase:


static void draw(void)
{
	drawBackground();

	drawStarfield();

	...

	drawDebris();

	drawExplosions();

	...

To our draw function we've added code to draw the background, starfield, debris, and explosions. We'll go through the in order:


static void drawBackground(void)
{
	SDL_Rect dest;
	int x;

	for (x = backgroundX ; x < SCREEN_WIDTH ; x += SCREEN_WIDTH)
	{
		dest.x = x;
		dest.y = 0;
		dest.w = SCREEN_WIDTH;
		dest.h = SCREEN_HEIGHT;

		SDL_RenderCopy(app.renderer, background, NULL, &dest);
	}
}

The drawBackground function draws our background texture using the backgroundX variable and the SDL_RenderCopy function. Because backgroundX is being decremented, it could be negative and therefore not cover the entire screen. Therefore, we set up a for loop to draw the background again, offset where the previous draw command ended. There is an optimisation we could be doing here: only drawing the portions of the background that we actually need. While this might be done in a later tutorial, right now it will be left an exercise for the reader. Something to notice is how we're setting dest's w and h to SCREEN_WIDTH and SCREEN_HEIGHT. We're doing this because our background texture is smaller than the screen (about 512 x 512). We're therefore stretching it to make it fit.

Next, we come to drawStarfield:


static void drawStarfield(void)
{
	int i, c;

	for (i = 0 ; i < MAX_STARS ; i++)
	{
		c = 32 * stars[i].speed;

		SDL_SetRenderDrawColor(app.renderer, c, c, c, 255);

		SDL_RenderDrawLine(app.renderer, stars[i].x, stars[i].y, stars[i].x + 3, stars[i].y);
	}
}

As can be seen, we're stepping through each of our stars in the array. We want to draw the stars as a horizontal line. What we're doing is calling SDL_SetRenderDrawColor to set the colour of the line according to the speed of the star (the higher the speed of the star, the brighter the color). The SDL_SetRenderDrawColor function takes four arguments: the renderer, and the red, green, blue, and alpha values (between 0 and 255 each). The SDL_RenderDrawLine function takes five parameters: the renderer, and the starting x,y and end x,y of the line we want to draw. We use a line instead of a point to make the stars a little easier to see and provide a sense of speed to the proceedings.

Our drawDebris function comes next. This is where we'll use our new blitRect function:


static void drawDebris(void)
{
	Debris *d;

	for (d = stage.debrisHead.next ; d != NULL ; d = d->next)
	{
		blitRect(d->texture, &d->rect, d->x, d->y);
	}
}

As expected, this function loops through each debris item, drawing it according to the portion of the texture that we set up earlier. Nothing particularly out of the ordinary.

Finally, let's look at the drawExplosions function. This is where things get a little more interesting:


static void drawExplosions(void)
{
	Explosion *e;

	SDL_SetRenderDrawBlendMode(app.renderer, SDL_BLENDMODE_ADD);
	SDL_SetTextureBlendMode(explosionTexture, SDL_BLENDMODE_ADD);

	for (e = stage.explosionHead.next ; e != NULL ; e = e->next)
	{
		SDL_SetTextureColorMod(explosionTexture, e->r, e->g, e->b);
		SDL_SetTextureAlphaMod(explosionTexture, e->a);

		blit(explosionTexture, e->x, e->y);
	}

	SDL_SetRenderDrawBlendMode(app.renderer, SDL_BLENDMODE_NONE);
}

If you've looked at the explosion texture in the source code (gfx/explosion.png) you'll see that it's a black and white gradiated sphere, lighter in the middle, growing darker as it expands out. What we'll be doing to create an explosion effect is colouring this texture and blending the results together to make a convincing glow-type effect. First, we'll call SDL_SetRenderDrawBlendMode, telling it to use SDL_BLENDMODE_ADD. This is additive blending, meaning that as we layer textures upon one another the colour of the affected pixels will add together, becoming brighter[2] (and eventually reaching white). We also tell our explosion texture itself to also be SDL_BLENDMODE_ADD. We then loop through each item in our explosion list and set the colour and alpha of the explosionTexture to that of the object (which we did when we created them in addExplosions). Finally, we blit our explosionTexture as normal. We also reset the app.renderer's blend mode to SDL_BLENDMODE_NONE, to avoid any ill effects.

One final thing we want to do is hide the mouse cursor, so it's not in the way. We do this in initSDL:


void initSDL(void)
{
	...
	SDL_ShowCursor(0);
}

We can turn it back on at any time by calling SDL_ShowCursor(1).

And that's it for all our effects. The game is now more attractive to look at and play, and the explosions and debris are rather pleasing. More effects could be added, such as engine trails behind the ships and making the debris trail fire, but right now these will be left as an exercise for the reader.

You've probably noticed by now that the stage.c file is getting rather big; it is currently over 700 lines. This is fine for our tutorial, but if we were expanding this into a much larger game, we'd want to push the debris and explosion functions into their own compilation unit (and probably all the stuff to deal with the bullets, the player, and the enemies). Having said so, this might happen in a later update.

Exercises

  • Scroll the background and stars in the reverse direction (or even up or down).
  • Break the ships into more pieces.
  • Add engine trails to the ships (you may want to create a new, smaller texture for this).
  • Add fire trails to the scattered debris (again, you might want a new texture).

Purchase

The source code for all parts of this tutorial (including assets) is available here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.

Footnotes

[1] - Dear god, that film was so bad.
[2] - The blend operation is essentially: dstRGB = (srcRGB * srcA) + dstRGB; dstA = dstA.

Comments

Share your comments and thoughts below. All comments are anonymous and cannot be edited.

 

This tutorial is life changing.
22 October 2021, 14:59PM

Mobile site