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


The Third Side (Battle for the Solar System, #2)

The White Knights have had their wings clipped. Shot down and stranded on a planet in independent space, the five pilots find themselves sitting directly in the path of the Pandoran war machine as it prepares to advance The Mission. But if they can somehow survive and find a way home, they might just discover something far more worrisome than that which destroyed an empire.

Click here to learn more and read an extract!

« Back to tutorial listing

— Simple 2D adventure game —
Part 8: Message Boxes

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

Introduction

Something we want to have in our little adventure game is for the player to be able to talk to NPCs; there will be a handful, that will offer hints, etc. Right now, we have the ability to display a short message when picking up an item or short interaction with a chest (to say it's locked), but it would be good to offer something more substantial.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./adventure08 to run the code. You will see a window open like the one above, showing the prisoner on a tiled background. Use WASD to move around. Right away, the prisoner will talk about wanting to escape. Press Space or Return to advance the message box. Exploring the dungeon, you'll find signposts, that will open up more message boxes. Close the window to exit.

Inspecting the code

Our message boxes are basically little more than text, one for the speaker and another for the text itself. We can see this by looking at structs.h, where we've designed it:


struct MessageBox {
	char speaker[MAX_NAME_LENGTH];
	char message[MAX_LINE_LENGTH];
	SDL_Color color;
	MessageBox *next;
};

We can also give the background of the message box a colour, just to make things more interesting. Different things speaking can have different colours, that can also be used to more easily identify who is speaking, apart from the speaker text alone. Our MessageBox is going to be handled as a linked list, so we're adding the head and tail for it to our dungeon, so we can access it throughout the game:


typedef struct {
	SDL_Point renderOffset;
	SDL_Point camera;
	Entity entityHead, *entityTail;
	MessageBox messageBoxHead, *messageBoxTail;
	Map map;
} Dungeon;

To handle our message boxes, we've added a new file: messageBox.c. This will be where we handle all our message box functions. Starting with our init function, initMessageBox:


void initMessageBox(void)
{
	messageBoxArrow = getAtlasImage("gfx/misc/messageBoxArrow.png", 1);

	messageBoxTimer = 1;
	messageBoxTextIndex = 0;
	messageBoxArrowPulse = 0;
}

We're grabbing an AtlasImage that will show an arrow on a message box when there are more to follow (more on this later). We're also setting up a some control variables. messageBoxTimer will be used to control the speed at which our text appears in the message box, messageBoxTextIndex will control how many characters of the current message to display, and messageBoxArrowPulse will be used to control the arrow position. A rather standard init function.

Moving on, the next function to consider is the doMessageBox function. This is where we'll perform all our logic for displaying and handling message boxes. It's not too complicated:


void doMessageBox(void)
{
	MessageBox *msg;

	msg = dungeon.messageBoxHead.next;

	if (msg != NULL)
	{
		messageBoxArrowPulse += (0.15 * app.deltaTime);

		messageBoxTimer = MAX(messageBoxTimer - app.deltaTime, 0);

		if (messageBoxTimer == 0)
		{
			messageBoxTextIndex = MIN(messageBoxTextIndex + 1, strlen(msg->message) + 1);

			messageBoxTimer = 1;
		}

		if (app.keyboard[SDL_SCANCODE_SPACE] || app.keyboard[SDL_SCANCODE_RETURN])
		{
			app.keyboard[SDL_SCANCODE_SPACE] = app.keyboard[SDL_SCANCODE_RETURN] = 0;

			dungeon.messageBoxHead.next = msg->next;

			if (msg->next == NULL)
			{
				dungeon.messageBoxTail = &dungeon.messageBoxHead;
			}

			free(msg);

			messageBoxTimer = 1;

			messageBoxTextIndex = 0;
		}
	}
}

We're grabbing the first MessageBox that follows the head of our chain (dungeon.messageBoxHead) and assigning it to a variable called msg. If msg isn't NULL (effectively meaning there is MessageBox in our chain), we'll start to process it. The first thing we do is update both messageBoxArrowPulse and messageBoxTimer. We want to decrease the value of messageBoxTimer, while not allowing it to fall below 0. Next, we test the value of messageBoxTimer to see if it has hit 0. If so, we know that we're free to advance to the next character in our message. We will increase the value of messageBoxTextIndex by 1, while telling it not to exceed the length of the message text itself (plus 1, due to fencepost error correction). With that done, we reset messageBoxTimer to 1. All this will create a typewriter effect for our MessageBox, so that the text appears one letter at a time on screen. This is purely to make the message boxes more interesting to look at.

With that done, we want to test whether the player has pressed Space or Return, to advance the message box. We're allowing them to do this at any time, and not requiring them to wait for the message to finish typing first. If the player has pressed either of these keys, we want to zero both values, to prevent them from triggering again immediately on the next frame, and the advance to the next message. To advance to the next message, we're assigning dungeon's messageBoxNext's next to the next of our current message, effectively now cutting it out of the chain. We then free it, first testing to see if it's the tail in our chain and resetting the tail to the messageBoxHead if so. We need to do this to ensure the tail isn't pointing to memory that's already been free'd, and lead to problems. Now that we've advanced to the next message, we reset the messageBoxTimer to 1 and messageBoxTextIndex to 0.

With out MessageBox logic out of the way, we can look at the rendering. We've got a function called drawMessageBox to deal with this:


void drawMessageBox(void)
{
	MessageBox *msg;
	char text[MAX_LINE_LENGTH];
	SDL_Rect box;

	msg = dungeon.messageBoxHead.next;

	if (msg != NULL)
	{
		memset(text, 0, sizeof(text));

		STRNCPY(text, msg->message, messageBoxTextIndex);

		box.w = 600;
		box.h = getWrappedTextHeight(msg->message, box.w);
		box.x = (SCREEN_WIDTH - box.w) / 2;
		box.y = 80;

		box.x -= 10;
		box.w += 20;
		box.y -= 5;
		box.h += 5;

		drawRect(box.x, box.y, box.w, box.h, msg->color.r, msg->color.g, msg->color.b, 192);

		drawOutlineRect(box.x, box.y, box.w, box.h, 255, 255, 255, 128);

		drawText(msg->speaker, box.x, box.y - 45, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
		drawText(text, box.x + 10, box.y, 255, 255, 255, TEXT_ALIGN_LEFT, 600);

		if (msg->next != NULL)
		{
			blitAtlasImage(messageBoxArrow, box.x + box.w, box.y + box.h + (sin(messageBoxArrowPulse) * 8), 1, SDL_FLIP_NONE);
		}
	}
}

Once again, you can see that we're testing to see if there is a MessageBox in our chain by grabbing the next from the messageBoxHead. If there is one, we want to draw it. We start by preparing a text array into which we'll copy a our message, using our STRNCPY macro to copy a number of characters equal to messageBoxTextIndex. This will effectively be our typewriter effect in action. Next, we want to prepare a background rectangle onto which we'll draw our text. Our box will be 600 pixels wide, and the height determined by the height of the text when wrapped at 600 pixels. We can discover the height of the text by calling getWrappedTextHeight, which is defined in text.c (you may which to visit the TTF tutorial, to see how this works). Next, we'll centre the box horizontally (box.x) and setting it's vertical position (box.y).

Notice next that we're making some adjustments to the box's values. This is just to add some padding to the box, so that the text isn't exactly at the edge of the box; basically, to stop it looking a bit messy. We'll then draw a filled rectangle using the box's details, calling on drawRect. We'll colour the box according to the details in the MessageBox's SDL_Color. However, we'll set the alpha to 128, so that it doesn't completely obscure the playfield behind it. We'll then outline it in white, with a call to drawOutlineRect.

With our box drawn, we can now render the text. We'll render the name of the speaker (msg->speaker) just above the box, and then the text of the message itself. Again, we're indenting the message itself by 10 pixels when rendering, so that it doesn't meet the edge of the box.

Finally, we're testing to see if there's another message following after this one. If so, we'll draw an arrow in the bottom right of the message box, so give an indication there is more text to follow. To make the arrow bob, we're calling the sin of messageBoxArrowPulse multiplied by 8 to the y value of the blit, a technique we saw for the arrow in the inventory screen.

The last function to look at is addMessageBox. It's quite straightforward, creating a MessageBox from the supplied parameters:


void addMessageBox(char *speaker, char *message, int r, int g, int b)
{
	MessageBox *msg;

	msg = malloc(sizeof(MessageBox));
	memset(msg, 0, sizeof(MessageBox));
	dungeon.messageBoxTail->next = msg;
	dungeon.messageBoxTail = msg;

	STRCPY(msg->speaker, speaker);
	STRCPY(msg->message, message);
	msg->color.r = r;
	msg->color.g = g;
	msg->color.b = b;
}

We're creating a new MessageBox and adding it to the message box linked list. We're then copying the speaker, message, and RGB parameters into the values of the MessageBox.

That's our message box done, so we can now look at how we're using it. Starting with dungeon.c, we're going to add some new calls to the initDungeon function:



void initDungeon(void)
{
	// snipped

	initMessageBox();

	// snipped

	initSignPost(54, 0, "Dead end. Try going another way.");

	initSignPost(55, 14, "Golf sale.");

	initSignPost(14, 28, "Insert player start position here.");

	initSignPost(20, 16, "Two chests, one key.");

	// snipped
}

The initDungeon function is getting rather big now, so some of the lines have been snipped here for brevity. We're called initMessageBox to set up the message box functions, and also adding in some calls to a function called initSignPost. This will be a new entity we've created to further demonstrate the message box in action. We'll see more on this in a bit. For now, let's look at the logic function. It contains a rather important change:


static void logic(void)
{
	if (dungeon.messageBoxHead.next == NULL)
	{
		doPlayer();
	}
	else
	{
		doMessageBox();
	}

	doEntities();

	doHud();
}

Notice how we're testing to see if there is a message box available before choosing to call doPlayer or doMessage box. This is important to do, as if we're displaying a message box, we don't want to player to be able to walk around. Instead, the doMessageBox is called, with the player needing to read the messages before doing anything else.

Our draw function has also been updated:


static void draw(void)
{
	drawMap();

	drawEntities();

	drawFogOfWar();

	drawHud();

	drawMessageBox();
}

We've added in the drawMessageBox function, calling it last, so that it is drawn on top of everything else; we don't want the map, entities, fog or war, etc. to block it from view.

Now, if we turn to player.c, we've can see we've updated initPlayer:


void initPlayer(void)
{
	// snipped

	addMessageBox("Prisoner", "Gosh, it's really dark in here. I hope I can find a way out.", 64, 64, 64);
	addMessageBox("Prisoner", "Maybe there's a signpost around here that will provide directions.", 64, 64, 64);
}

Here, we're calling addMessageBox, to make the player speak his introduction. You will have seen this when starting up the game. Notice how there are two calls to addMessageBox, creating two messages that follow one after the other (FIFO order).

As mentioned earlier, we also added in a new entity, a Signpost. Returning to structs.h, we can see it defined as the following:


typedef struct {
	char message[MAX_LINE_LENGTH];
} Signpost;

A simple struct that holds just the message to display. The message is rather long; MAX_LINE_LENGTH is defined as 1024, making it more of an essay than a simple sign. Still, it goes scope to include unicode, if we wanted. The signpost's functions are defined in a file called signpost.c. We'll start with initSignPost:


void initSignPost(int x, int y, char *message)
{
	Entity *e;
	Signpost *signpost;

	signpost = malloc(sizeof(Signpost));
	memset(signpost, 0, sizeof(Signpost));
	STRCPY(signpost->message, message);

	e = spawnEntity();
	e->x = x;
	e->y = y;
	e->texture = getAtlasImage("gfx/entities/signpost.png", 1);
	e->solid = SOLID_SOLID;
	e->data = signpost;

	e->touch = touch;
}

Like most entity functions, the init function takes the x and y coordinates of its position. It's also accepting a message parameter. Simply put, the function will create a signpost struct and copy the message into its message member. It will then create a standard entity, and set the position, texture, solid state, and set the data member as the signpost we created. Finally, the touch function is set. It is defined below:


static void touch(Entity *self, Entity *other)
{
	Signpost *signpost;

	if (other == player)
	{
		signpost = (Signpost*) self->data;

		addMessageBox("Signpost", signpost->message, 90, 70, 30);
	}
}

When our player walks into the signpost (remember it is solid, so will block the player), it will display its message. We're using a light brown background for the messages for our signpost, to go along with the wood colour. Once again, you can find the signposts scattered around the dungeon.

That's it for our message box tutorial. Now, as mentioned earlier, you'll have noticed that the initDungeon function is getting rather large. We're hardcoding in our entities. It's not too practical, especially with the map data being loaded from a file. What would be best is if we load the entities, too. In the next tutorial, we'll look at how we can load the dungeon data from a file.

Purchase

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

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Mobile site