• 2D shoot 'em up
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
TBFTSS: The Pandoran War - Amiga OS4 Port
— Creating a basic widget system —
With all our widgets now done, we can look at how they would be implemented in an actual game. We're going to make a faux space shooter, featuring an alien. Things such as the starfield density, the sound and music volumes, the sprite, and whether to show the frame rate can be changed.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./widgets08 to run the code. You will see a window open like the one above. Using the arrow keys to control the alien, and the left control key to fire. Pressing Escape will pause the game and open the options menu. Change the options as you like, and then either select Resume or press Escape to return to the game. When you're done, either select Exit from the in-game menu or close the window.
Inspecting the code
Since we're now done with our widgets, the bulk of our changes are being done in demo.c. Before we go there, there's a minor update to structs.h:
Alongside fps in App, we've added showFPS. This will be used to control whether we display the frame rate. By default, it is will be 0 (false).
demo.c has received many, many more updates. There are some we'll not cover here, such as controlling the player, etc. as these were done in previous tutorials. We'll focus on how we deal with the widget interactions instead. Starting with initDemo:
Wow, there's a lot happening there. However, it's mostly setup, and some of it we've seen before (such as getting the widgets and assigning their actions). We'll work our way down from the top.
We're starting by setting up our bullet linked list, then grabbing 4 images from our texture atlas that will be used for our alien, storing them in an array called alienImages. We're then preparing the player by zeroing its memory, and setting its x and y coordinates. Following on from that, we're fetching the image that will be used for our bullet. We're then setting up our starfield, but looping through all the stars in the array, and giving them random screen coordinates and speeds.
After that, we're setting a variable called paused to 0. This will tell our game that we're currently playing and not to show the menu. It is a key element to how our logic and drawing will work for our in-game menu. We're then fetching all of our widgets, setting their function pointers, coordinates, and default values as needed. One thing you'll notice is that we're assigning almost every widget as the activeWidget and immediately calling their action function pointer. This is so that things such as the player name, starfield density, and volumes are invoked. It helps to keep our game state consistent with the states of the widgets.
As you can see, the init function wasn't all that complex, it was just long. Now, let's move onto the logic function to see how the main game loop is working:
The first thing we're doing is testing the paused flag. If its 0, we'll process our player, stars, and bullets. If it's 1, we'll call doWidgets, to process our widgets. As you can clearly see, when the paused flag is set to 1, all the action in the game will stop and the focus will be on the widget processing. Finally, notice how that whether the game is paused or not, we're testing if Escape has been pressed on the keyboard. If it has, we'll clear the key, and then toggle the pause state. This allows us to enter the in-game menu when playing the game, and exiting it if it's currently displayed.
Our draw function also makes use of the pause flag:
In all cases, we'll always draw our stars, player, and bullets. However, we'll then test if we're paused or not. If we're not paused, we'll draw the hud. Otherwise, we'll dim the screen a little by rendering a transparent black rectangle, and then call drawWidgets, to render all the widgets. Ultimately, this means that our widgets will be hidden until the game is paused. The reason the hud is hidden is merely to remove the player name from the display while the widgets are being shown. Having it displayed at the same time could cause a little confusion, so it's best to remove it.
Now, let's move onto the rest of the code. Again, since this tutorial is focused on the widgets, we'll not look at how the bullets, etc. work. So, let's look at all the widgets' functions pointers. Starting with resume:
There isn't much to what happens when we select Resume from the in-game menu - we simply set the paused flag to 0, so that we return to the game.
Next, the name function pointer:
We saw this in a previous tutorial. We're grabbing the text value of the InputWidget and copying it into the player's name, using our STRCPY macro.
Our colour function pointer is equally as straightforward:
We're extracting the SelectWidget data, and using its value as the index to the alienImages array, which we'll use as the player's texture. Remember that our SelectWidget has an array of char*, backed by a value int. All we need to do is ensure that the text of our SelectWidget indexes matches up with the alienImages.
Next, our music function:
Remember that our SliderWidgets hold a value between 0 and 100, effectively representing a percentage. We're going to use this to affect our sound volume. MIX_MAX_VALUE is an SDL2 Mixer constant, with a value of 128. By taking our SliderWidget's value and dividing by 100 (in this case, multiplying by 0.01), we'll get a decimal value between 0 and 1. Multiplying MIX_MAX_VALUE by this result will give us a number between 0 and 128. We'll then use this with Mix_VolumeMusic, to change the volume of the music. Our music SliderWidget is a smooth type, so holding the left and right array keys will smoothly update the bar.
Our sound function pretty much works the same:
The only difference is that we're feeding the result into Mix_Volume and telling it to apply the volume to all sound channels (passing -1 to channel number, the first parameter). Our sound widget requires us to press left and right to change the volume, and its step is set to 10, so it changes at 10% increments.
The function for toggling the display of the frame rate is easy enough:
We need only extract the SelectWidget and find out its value. Off and on will be 0 and 1, so we just have to assign this value to the showFPS variable in app.dev.
The starfield function has a little more to consider:
Our starfield options are Very Low, Low, Normal, High, and Max. Again, the value of the SelectWidget is a number from 0 to 4, meaning we can simply grab the desired value from a pre-defined index. numStarsOptions is a static variable, defined at the top of demo.c:
Just like choosing the image for the alien, the index of our SelectWidget will determine how many stars we want to process and render. When it comes to doing so, our logic and render functions for our stars is simple:
While we have created 1500 stars (NUM_STARS), we will only process the value of numRenderedStars. The SelectWidget for our starfield allows us to change this on the fly. The same logic is applied when drawing the stars:
In both cases, we want to use numRenderedStars, instead of NUM_STARS, to reduce the processing and rendering time for the starfield.
We're almost done. So let's look quickly at where our frame rate is displayed. Our main loop will always call a function in draw.c called presentScene. It is here that we'll test if we want to draw our frame rate:
As we're testing and handling showFPS in presentScene, our frame rate will be shown, no matter where in the application we are. The showFPS function itself merely calls out to drawText:
And that's it for implementing widgets in a game. We've one more thing to consider before we wrap up this tutorial, and that's grouping our widgets. Right now, we have an all-or-nothing approach to drawing our widgets. It would be good if we could only show certain ones. In our final part, we'll do just that.
The source code for all parts of this tutorial (including assets) is available for purchase:
It is also available as part of the SDL2 tutorial bundle: