— Creating a basic widget system — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction So far, we've been able to create simple button widgets, that perform an action when invoked. Another common widget seen in games is one where the user is able to cycle through a number of options, such as changing the game difficulty, the language, the colour of their player character, etc. In this part of the tutorial, we'll look at how we can create an option widget. Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./widgets04 to run the code. You will see a window open like the one above. Use the Up and Down arrows on you keyboard to change the highlighted menu option. Press Left and Right to cycle through the options on the Select widgets (difficulty, language, subtitles). When you're done, either select Exit or close the window. Inspecting the code Our select widget is defines in JSON, and looks a little different to our regular widget. It now features an "options" array: { "type" : "WT_SELECT", "name" : "difficulty", "x" : 0, "y" : 250, "label" : "Difficulty", "options" : [ "Very easy", "Easy", "Normal", "Hard", "Very hard" ] } We'll see how this all works when we come to creating the widget. For now, let's look at structs.h, and see what changes have been made there. Starting with our Widget struct:
We've added a data pointer, which we're going to use to extend our Widget with additional data. Our select widget (and other widgets to come) will feature data that is only used by them, and rather than clutter up the base widget, we're going to optionally extend it. This technique was used in the SDL2 Adventure tutorial, for all the entities. Our SelectWidget is defined below:
The SelectWidget will hold the count of the number of options available; a char array pointer, of the actual options themselves; x and y values, to say where the options should be drawn; and a value variable, to hold the index of the currently selected option. Since our SelectWidget is a new type, we've also updated defs.h and added its type to our enum:
Our SelectWidget will have a type of WT_SELECT. Now, let's move onto actually loading and configuring the widget. We've made a lot of changes to widgets.c to support this new widget, so we'll go from the start. Our initWidgets function has a minor change:
We're going to load a different widget file from before, as we've defined all our select widgets in options.json. Our createWidget function has also been updated to handle the new type:
When we encounter a WT_SELECT widget type, we'll call createSelectWidget, to build our widget. createSelectWidget is quite a bit more involved than when we create a simple button, as you'll see:
The first thing the function does to malloc and memset a SelectWidget struct, and assign it to the Widget's data pointer. After that, it extracts the options JSON array from the JSON object. We'll call cJSON_GetArraySize on the options array to find out its size, and assign the value to our SelectWidget's numOptions. If the array isn't empty (it has at least 1 element), we'll want to grab all the options and add them into our SelectWidget's options array. We do this by mallocing an array of char* the same size as the number of options available. With that done, we'll then loop through the options defined in our JSON array. For each one, we'll allocate some memory the same length as the text string (adding one, to allow for the null terminator) and copy that into the SelectWidget's current option index. We're making use of a macro called STRNCPY, which will limit the number of characters copied and also add the null terminator (a very useful macro). With that done, we'll assign calculate the size of the widget, according to its label. We'll finally also default the x and y of the select options to lie 50 pixels to the right-hand side of the widget label. Before moving onto how the logic and rendering are done for the Select widget, here's a quick look at getWidgetType, that was also updated:
Nothing that wasn't expected, but I've included it for completeness. Now we can look at how our logic has changed, now that we've introduced SelectWidgets. Starting with doWidgets:
The main thing to note here is that we're now testing if Left and Right have been pressed on the keyboard. If so, we'll be calling a new function called changeWidgetValue, passing over -1 or 1 depending on the key pressed. changeWidgetValue is a fairly simple function, as we'll see:
We test the widget, to see what type it is. If it's a WT_SELECT widget, we'll change the SelectWidget's value variable by dir. So, if dir is -1, we'll decrease. If it's 1, we'll increase. We next test to see if value is less than 0. If so, we'll wrap around to numOptions - 1 (returning to the top of the list). If it's more than or equal to numOptions, we'll return to 0 (start of the list). In effect, pushing left and right, will move backwards and forwards through our list of options, wrapping around when it come to the start or end of the list. Finally, we'll test if the widget has an action assigned. If so, we'll invoke it. That's the logic handled. Now, we can look at how we're drawing our SelectWidgets. First, we'll update drawWidgets to handle the type:
If our widget is a WT_SELECT type, we'll be calling drawSelectWidget, a new function we've defined to handle the rendering of SelectWidgets:
drawSelectWidget isn't too different from drawButtonWidget. We're testing to see if the widget is the currently active widget, in order to choose the colour that it should be drawn (green if active, otherwise white). Next, we'll draw the widget's label. After that, we'll draw the SelectWidget's currently selected option. We'll do so with a formatted string, adding > and < chevons to the left and right of the text, to give an indication that they can be cycled through. Somewhat crude, yes. It might be more visually appealing to use an image of an arrow, which we'll likely do in future. Right now, this will do the job. With the chevrons applied, we draw the text, according to the x and y positions of the SelectWidget. As you can see, giving the SelectWidget its own x and y values allows use to render the options wherever we want. We could draw them beneath the label, for example. That's our widget logic and rendering done, so we can now look at how we're setting up the widgets, in demo.c. Since we're using some different widgets, we've updated initDemo:
As we've seen in our previous tutorials, we're looking up the widgets by name, before assigning their actions and positions. In the case of our SelectWidget, we're extracting the SelectWidget from the widget's data variable and updating its x value, so that the options lie to the right of the widgets' labels. The actions for each of the widgets are, again, quite simple in their nature. Starting with difficulty:
The function just grabs the text of the SelectWidget's current option and uses it in a formatted string, to show what the difficulty has been set to. We do a similar thing with the subtitles:
And again with language:
That's our select widget done! We now have the ability to offer buttons and cycle through a series of options. What we'll look at next is how to create a widget that allows for smooth value change, such as for use with volume levels of sound and music. 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 |