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

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
Medals (Achievements)
2D turn-based strategy game
2D isometric game
2D map editor
2D mission-based shoot 'em up
2D Santa game
2D split screen game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Versus game tutorial
Wed, 20th March 2024

Download keys for SDL2 tutorials on itch.io
Sat, 16th March 2024

The Legend of Edgar 1.37
Mon, 1st January 2024

SDL2 Santa game tutorial 🎅
Thu, 23rd November 2023

SDL2 Shooter 3 tutorial
Wed, 15th February 2023

All Updates »

Tags

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

Books


H1NZ

Arriving on the back of a meteorite, an alien pathogen has spread rapidly around the world, infecting all living humans and animals, and killing off all insect life. Only a handful are immune, and these survivors cling desperately to life, searching for food, fresh water, and a means of escape, find rescue, and discover a way to rebuild.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 1: Scrolling

Note: this tutorial assumes knowledge of C, as well as prior tutorials.

Introduction

It's the most wonderful time of the year! If it's December, and you live in a country that celebrates Christmas, that is! In this tutorial series, we'll be looking at how to create an infinite scrolling game, where the player takes on the role of Santa Clause, in his bid to deliver as many gifts as possible on Christmas Eve. We'll discuss the naturally absurd plot of our game at a later point. For now, we'll look into getting the basics working.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa01 to run the code. You will see a window open like the one above, with the ground moving from right to left. There's not much more to see or do here, so when you're finished, close the window to exit.

Inspecting the code

Note: Ultimately, this tutorial set will purposely leave out certain non-essential features, such as loading and saving highscores, redefining the controls, setting sound and music volumes, and the like. These will be left as an exercise for the reader to implement if they desire. Tutorials such as SDL2 Shooter and SDL2 Shooter 2 support highscores, while the widget tutorial shows how to set sound and music volumes in game. All optional things, of course.

In this first part, we're going to be implementing our infinite scrolling ground plane, using a tile map approach. You'll find it's quite easy to do. None of these parts are going to be super detailed, as by now the pattern of our game development has been well established, and so we'll focus on the things that make SDL2 Santa the game it is.

We'll start with defs.h:


#define GROUND_TILE_SIZE 64
#define GROUND_Y         (SCREEN_HEIGHT - GROUND_TILE_SIZE)

We're setting us two defines here. GROUND_TILE_SIZE is the width and height of our ground tile in pixels, while GROUND_Y is the vertical position of our ground plain. For its value, we're just subtracting the size of our ground tile from the height of the screen, so the ground is pinned to the bottom.

Now over to structs.h. We've only got one major struct to consider here:


typedef struct
{
	double speed;
} Stage;

Our Stage struct holds just the horizontal speed of our scrolling. We're adding this here as we'll want our game to speed up over time, and so entities and other things in the game will need to know how fast we're moving.

Nothing major so far. Let's turn our attention now to stage.c, where the bulk of this first part resides. As with everything before, this will remain simple (and once again, we're skipping over things that have been covered in past tutorials - we'll refer back to them as needed).

Our first function is initStage:


void initStage(void)
{
	memset(&stage, 0, sizeof(Stage));

	if (groundTextures[0] == NULL)
	{
		loadTextures();
	}

	initGround();

	stage.speed = INITIAL_GROUND_SPEED;

	app.delegate.logic = doStage;
	app.delegate.draw = drawStage;
}

We're clearing our `stage` object using memset, and then checking if we need to load our textures. groundTextures is an array of AtlasImages of a fixed length (NUM_GROUND_TEXTURES). When declaring our array, we're setting the first element as NULL, so we can test whether we have already loaded it. If we need to load the texture, we're calling loadTextures (we'll come to this in a bit). Next, we're calling initGround, to prepare the ground. We're setting Stage's `speed` to the value of INITIAL_GROUND_SPEED (defined as 2 in stage.c), and finally setting App's delegate's logic and draw functions points to doStage and drawStage, respectively.

Nothing we've not seen before. So, let's move onto initGround:


static void initGround(void)
{
	int i;

	for (i = 0; i < GROUND_WIDTH; i++)
	{
		ground[i] = rand() % NUM_GROUND_TEXTURES;
	}
}

A pretty simple function. We're randomly setting the texture we want to use for our ground indices. `ground` is an array of ints, of GROUND_WIDTH in size (defined as SCREEN_WIDTH / GROUND_TILE_SIZE + 1). We're looping through each element in the array and setting a random number between 0 and NUM_GROUND_TEXTURES - 1 to its index. We'll be using this for our drawing. One can think of this is a 1D tile map.

With our ground tiles randomly redefined, we can move over to the first part of our game's logic step, in doStage:


void doStage(void)
{
	doGround();
}

doStage will ultimately be the main function of our game, that will be called from the game itself, as well as the title screen. Right now, this function does only one thing - calls doGround:


static void doGround(void)
{
	groundX -= stage.speed * app.deltaTime;
}

Once again, we're looking at a simple function. doGround decreases the value of groundX (a static variable in stage.c) upon each call, by the value of Stage's `speed`. This will ultimately cause our ground to move from right to left.

That's all our logic done! Now, let's turn to our rendering phase. Starting with drawStage:


void drawStage(void)
{
	drawGround();
}

drawStage will, in the end, handle all our game's rendering. Right now, however, it is delegating to another function - drawGround:


static void drawGround(void)
{
	int i, x, n, t;

	n = (int)groundX * -1 / GROUND_TILE_SIZE;

	x = (int)groundX % GROUND_TILE_SIZE;

	for (i = 0; i < GROUND_WIDTH; i++)
	{
		t = (n + i) % GROUND_WIDTH;

		blitAtlasImage(groundTextures[ground[t]], x, GROUND_Y, 0, SDL_FLIP_NONE);

		x += GROUND_TILE_SIZE;
	}
}

drawGround is where things get a bit more interesting, as this is where we'll be giving the impression of our ground moving.

The theory behind this is easy enough to understand - draw our ground tiles from left to right, in the position and order they are in according to the value of groundX. To achieve this, we're multiplying groundX by -1, to make it positive (remember, we're subtracting from groundX during our logic step), and then dividing the value by GROUND_TILE_SIZE to find out which tile index to begin at. We're assigning this value to a variable called `n`. We're then taking the GROUND_TILE_SIZE modulo of groundX, giving us a value between 0 and -(GROUND_TILE_SIZE - 1). This is our `x` position. Our drawing will therefore start from a position off the left-hand side of the screen or at 0, depending on groundX's value.

With our `x` offset known and our starting tile determined (`n`), we're setting up a loop to draw our tiles from left to right. Remember that GROUND_WIDTH covers the width of the screen, plus an extra tile for some overscan on the right-hand side, so our final tile doesn't "pop" into existence. We next setup a variable called `t` which will be the value of the index in our ground array that we want to use for rendering our tile. We add the current value of `i` (our loop variable) to `n`, and then take the GROUND_WIDTH modulo of this value, to give us an index between 0 and GROUND_WIDTH - 1, so that as we each the end of our ground array, we'll automatically wrap around again (assuming an array length of 25 - 22, 23, 24, 0, 1, 2, 3 ...). We then draw the ground tile using blitAtlasImage, indexing the wanted tile using the value at `ground`, indexed by `t`, positioned at `x` and GROUND_Y. Finally, we increase the value of `x` by the value of GROUND_TILE_SIZE.

It might look complicated, but it's really quite simple If you wish to see what is happening with the `n` and `x` variables, you could add printf statements to debug the values and see how they are working. You will see the value of `n` continue to increase from 0, and x go from 0 to -GROUND_TILE_SIZE - 1 (negative numbers).

There were a number of ways that we could have approached this. One alternate method would be to create a struct holding positions and texture of each section of the ground, and moving them from right to left, wrapping them back to the right as they moved off the screen. This would work, but is slightly tricker to accomplish correctly, due to the prospect of misaligning the objects when wrapping them to the right. Doing so incorrectly would result in ugly seams and gaps.

Just one function left to look at in stage.c, which is loadTextures:


static void loadTextures(void)
{
	int  i;
	char filename[MAX_NAME_LENGTH];

	for (i = 0; i < NUM_GROUND_TEXTURES; i++)
	{
		sprintf(filename, "gfx/ground%02d.png", i + 1);
		groundTextures[i] = getAtlasImage(filename, 1);
	}
}

Not a lot to see here! We're just looping through NUM_GROUND_TEXTURES and using a formatted string to load our groundTextures (named gfx/ground01.png, gfx/ground02.png, etc). These images are expected to exist in our atlas.

That's stage.c finished, so finally, we'll be calling initStage from our main function:


int main(int argc, char *argv[])
{
	long then;

	memset(&app, 0, sizeof(App));

	initSDL();

	initGameSystem();

	initStage();

	atexit(cleanup);

	while (1)
	{
		then = SDL_GetTicks();

		prepareScene();

		doInput();

		logic();

		app.delegate.draw();

		presentScene();

		/* allow the CPU/GPU to breathe */
		SDL_Delay(1);

		app.deltaTime = LOGIC_RATE * (SDL_GetTicks() - then);
	}

	return 0;
}

You'll notice this is the standard way we've been starting our games: calling initSDL, initGameSystem, and setting up a loop to process our inputs, and call our logic and draw delegates.

A gentle start to our game, but one that sets up some good foundations (one could even say 'ground work') to move forward with. If you've ever wanted to create an infinite scrolling ground layer, here is one way that it can be achieved. In our next part, we'll look into introducing houses, to go along with our ground.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle:

From itch.io

Mobile site