— Simple 2D adventure game — 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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: | |
Desktop site |