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 Red Road

For Joe Crosthwaite, surviving school was about to become more than just a case of passing his exams ...

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a basic widget system —
Part 9: Widget groups

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

Introduction

For the final part of this tutorial, we'll look at grouping our widgets, so that we only show those widgets that matter at that moment. Here, we'll group our widgets into a pause section, an options section, and a control configuration section.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./widgets09 to run the code. You will see a window open like the one above. Press Escape to bring up the pause menu. From here, selecting "Options" will take you to the options menu. And from there, selecting "Controls ..." will take you through to the control configuration section. Press Escape to back out of each menu section and back to the game. Once you're done, either select Exit from the pause menu or close the window.

Inspecting the code

In order to support widget groups, our widgets will need a group name, alongside their own names. Doing this is incredibly simple. Starting with structs.h:


struct Widget {
	int type;
	char name[MAX_NAME_LENGTH];
	char groupName[MAX_NAME_LENGTH];
	int x;
	int y;
	int w;
	int h;
	char label[MAX_NAME_LENGTH];
	Widget *prev;
	Widget *next;
	void (*action)(void);
	void (*data);
};

We've added an extra field to Widget, naming it groupName. That's literally the only change we need to make in structs.h, so we can now move onto widgets.c, to see what changes are needed there. Note: we've snipped away the irrelevant code in this part of the tutorial, as some of the changes were only one or two lines and can be hard to see in longer functions.

To make life a bit easier, we've put our widget groups into separate files, named pause.json, options.json, and controls.json. We'll load each of these files in our initWidgets call:


void initWidgets(void)
{
	memset(&widgetHead, 0, sizeof(Widget));
	widgetTail = &widgetHead;

	loadWidgets("data/widgets/pause.json");
	loadWidgets("data/widgets/options.json");
	loadWidgets("data/widgets/controls.json");

	sliderDelay = 0;

	cursorBlink = 0;

	handleInputWidget = 0;

	handleControlWidget = 0;
}

Since our widgets are a linked list, we can load as many as we like, and they will be appended to the existing list. Our loadWidgets function has changed, but our createWidget function has:


static void createWidget(cJSON *root)
{
	Widget *w;
	int type;

	type = getWidgetType(cJSON_GetObjectItem(root, "type")->valuestring);

	if (type != -1)
	{
		// snipped

		STRCPY(w->name, cJSON_GetObjectItem(root, "name")->valuestring);
		STRCPY(w->groupName, cJSON_GetObjectItem(root, "groupName")->valuestring);
		STRCPY(w->label, cJSON_GetObjectItem(root, "label")->valuestring);
		w->type = getWidgetType(cJSON_GetObjectItem(root, "type")->valuestring);
		w->x = cJSON_GetObjectItem(root, "x")->valueint;
		w->y = cJSON_GetObjectItem(root, "y")->valueint;

		// snipped
	}
}

We're now copying our groupName into the Widget we've created. All our JSON objects will have a groupName, alongside the name, label, etc. Again, this is all we needed to do in createWidget, so we can look at the logic and rendering of the widgets, which is a bit more interesting.

The first thing you'll notice about our doWidgets function is that it now takes the name of the group as a parameter:


void doWidgets(char *groupName)
{
	// snipped

		if (app.keyboard[SDL_SCANCODE_UP])
		{
			app.keyboard[SDL_SCANCODE_UP] = 0;

			do
			{
				app.activeWidget = app.activeWidget->prev;

				if (app.activeWidget == &widgetHead)
				{
					app.activeWidget = widgetTail;
				}
			}
			while (strcmp(app.activeWidget->groupName, groupName) != 0);

			playSound(SND_GUI, 0);
		}

		if (app.keyboard[SDL_SCANCODE_DOWN])
		{
			app.keyboard[SDL_SCANCODE_DOWN] = 0;

			do
			{
				app.activeWidget = app.activeWidget->next;

				if (app.activeWidget == NULL)
				{
					app.activeWidget = widgetHead.next;
				}
			}
			while (strcmp(app.activeWidget->groupName, groupName) != 0);

			playSound(SND_GUI, 0);
		}

	// snipped
}

That's the first change. The next change is what happens when we push up and down on the keyboard, to move through our list of widgets. Before, we used to simply move to the previous or next widget in our list, looping around as needed. However, we're now only interested in processing those widgets that match the name of the group that we've passed in. As such, we need to keep moving through our list until we find the next widget in the group. For this reason, we're now using a do-while loop.

Let's take pushing Up as an example. When we push up, we set our activeWidget to the activeWidget's prev (to move backwards through the list). If we've come to the top of the list, we'll move to the buttom of it. However, our while condition will then check to see if the new widget's groupName matches that which was passed into the function. If so, our do-while loop will exit. If not, the loop will continue to move backwards through the list (looping around as needed) until it finds a widget matching the group name that we are currently processing. In effect, this means that we will only ever process and handle widgets whose groupName matches that passed into the function. The same is true of moving forward through the list.

Note that it is possible for us to get into a endless loop here, if we pass in the name of a widget group that doesn't exist. One would need to be mindful of typos (or perhaps implement a sanity counter, to exist if it didn't find a widget after a great many attempts). This isn't something that's likely to happen too often to you, however, if ever, so it's not that big a deal.

Now, let's look at drawWidgets, which now also takes the name of the widget group as a parameter. However, the further changes we've made are much simpler than doWidgets, for sure:


void drawWidgets(char *groupName)
{
	Widget *w;
	int h;

	for (w = widgetHead.next ; w != NULL ; w = w->next)
	{
		if (strcmp(w->groupName, groupName) == 0)
		{
			// snipped
		}
	}
}

We're still looping through all our widgets, but instead of rendering all of them, we're now only drawing those whose groupName matches that of the parameter we've passed into the function.

A word on this strcmp usage. strcmp is a very well optimised function, that is incredibly fast. Even so, I tend to avoid using strcmp during main processing loops, in case it drags down performance. However, we're safe here. Our game doesn't actually process any gameplay logic while the widgets are showing, so we won't see a performance hit. If we needed to, we could create separate linked lists for all our widget groups, so we didn't need to test each widget, or even create an array of all the widgets we wanted to work with ahead of time, and pass those into the function. These two approaches are quite unnecessary here, though, given the small number of widgets we're working with (fewer than 20).

Another function that has needed to change is getWidget. It is now required that we pass over the groupName of the widget:


Widget *getWidget(char *name, char *groupName)
{
	Widget *w;

	for (w = widgetHead.next ; w != NULL ; w = w->next)
	{
		if (strcmp(w->name, name) == 0 && strcmp(w->groupName, groupName) == 0)
		{
			return w;
		}
	}

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "No such widget: name='%s', groupName='%s'", name, groupName);

	return NULL;
}

Now, when looking up the widget, we'll test the name and group name of each widget, looking for a match. Doing this allows us to have more than one widget with the same name. For example, our options and controls widget groups both have a widget named "back", so this allows us to distinguish between them.

That's the changes to widgets.c handled, so we can now turn to the updates we've made to demo.c. We're not going to cover every single change, as we'll find that we'll end up just repeating ourselves. We'll cover all the most important aspects, though. Starting with initDemo:


void initDemo(void)
{
	// snipped

	setupOptionWidgets();

	setupControlWidgets();

	setupPauseWidgets();

	show = SHOW_GAME;

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

Our initDemo function was becoming quite large with all the widgets being setup, so we've split it into three functions: setupOptionWidgets, setupControlWidgets, and setupPauseWidgets, for each of the widget groups. Something else new to notice is a variable called show. We've ditched the pause variable, as we want to support four different display types. The show variable can be one of SHOW_GAME, SHOW_PAUSE, SHOW_OPTIONS, SHOW_CONTROLS. We'll see how these come into play as we proceed through the code.

As an example of what our separate widget setup functions look like, below is the setupPauseWidgets function:


static void setupPauseWidgets()
{
	Widget *w;

	w = getWidget("options", "pause");
	w->action = options;
	w->x = (SCREEN_WIDTH - w->w) / 2;

	w = getWidget("exit", "pause");
	w->action = quit;
	w->x = (SCREEN_WIDTH - w->w) / 2;

	w = getWidget("resume", "pause");
	w->action = resume;
	w->x = (SCREEN_WIDTH - w->w) / 2;

	app.activeWidget = w;
}

The getWidget function should be quite familiar to you by now, but notice how we're also passing over the required groupName, to conform to the new getWidget parameter requirements. Our setupControlWidgets and setupOptionWidgets function are largely the same as this.

Our logic function has been reworked, now that we no longer have the pause variable and are instead using the show variable:


static void logic(void)
{
	switch (show)
	{
		case SHOW_GAME:
			doPlayer();
			doStars();
			doBullets();
			break;

		case SHOW_PAUSE:
			doWidgets("pause");
			break;

		case SHOW_OPTIONS:
			doWidgets("options");
			break;

		case SHOW_CONTROLS:
			doWidgets("controls");
			break;

		default:
			break;
	}

	if (app.keyboard[SDL_SCANCODE_ESCAPE])
	{
		app.keyboard[SDL_SCANCODE_ESCAPE] = 0;

		if (show == SHOW_GAME)
		{
			show = SHOW_PAUSE;
		}
		else
		{
			back();
		}
	}
}

We now calling switch on our show variable, to see what it is set to. If it is set to SHOW_GAME, we'll be processing the game logic as normal. However, if it's SHOW_PAUSE, SHOW_OPTIONS, or SHOW_CONTROLS, we'll be processing the appropriate widget groups, by calling doWidgets and passing in the name of the widget group. We're again always checking if Escape has been pressed. If so, we'll do one of two things. If show is set to SHOW_GAME, we'll set show to SHOW_PAUSE to open the pause menu. Otherwise, we'll be calling a function called back:


static void back(void)
{
	switch (show)
	{
		case SHOW_CONTROLS:
			show = SHOW_OPTIONS;
			app.activeWidget = getWidget("controls", "options");
			break;

		case SHOW_OPTIONS:
			show = SHOW_PAUSE;
			app.activeWidget = getWidget("options", "pause");
			break;

		case SHOW_PAUSE:
			show = SHOW_GAME;
			break;

		default:
			break;
	}
}

The back function tests our show, to see what we're currently looking at. This function is assigned to the "back" widgets of the options and controls sections. The function will basically cause us to return to the previous menu (or return to the game, if we're paused). If we're in the control menu, we'll return to the options menu. We'll also set the activeWidget to the "controls" widget in the options menu. This step is important, as otherwise our activeWidget would remain as one in the controls menu. As this is no longer displayed, none of the options widgets will be highlighted and it will look odd. The same thing happens when we're in the options menu - we return to the pause menu, highlighting the "options" widget.

In effect, this allows us to press Escape to jump back up the menu hierarchy, selecting the menu option that would've taken us there in the first place (rather than selecting the first widget in the sub menu, which could lead to cognitive dissonance).

Our main draw function also makes use of the new show variable. It's not too different from the previous version that used the pause variable:


static void draw(void)
{
	drawStars();

	drawPlayer();

	drawBullets();

	if (show == SHOW_GAME)
	{
		drawHud();
	}
	else
	{
		drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0, 128);

		switch (show)
		{
			case SHOW_PAUSE:
				drawWidgets("pause");
				break;

			case SHOW_OPTIONS:
				drawWidgets("options");
				break;

			case SHOW_CONTROLS:
				drawWidgets("controls");
				break;
		}
	}
}

At all times, we want to draw the stars, player, and bullets. We'll only draw the hud if show is set to SHOW_GAME. Otherwise, we're still darkening the background, but then we're testing the value of show, and drawing the widget group appropriate for the value; if SHOW_PAUSE, we're rendering the pause widgets, etc.

Not too complex some far, at all, and thankfully it remains that way. We're almost done with the tutorial, so before we wrap up, let's look at some of the function pointers to see how they've changed. Starting with resume:


static void resume(void)
{
	show = SHOW_GAME;
}

This shouldn't be a surprise. Instead of flipping the old paused variable to 0, we're now setting show to SHOW_GAME, to return to the game. What about when we select "Options" or "Controls ..."? The function used by options is below:


static void options(void)
{
	show = SHOW_OPTIONS;

	app.activeWidget = getWidget("name", "options");
}

We're updating the value of show, but we're also setting the activeWidget to the widget at the top of the menu. In the case of the options menu, this is "name". Again, this is important to ensure that we're highlighting a widget when moving around menus.

Finally, let's look at the what happens when we change our controls. All of our control widgets's function pointers make use of a function called updateControls. Whenever we change a control, this function will be called:


static void updateControls(void)
{
	controls[CONTROL_LEFT] = ((ControlWidget*) getWidget("left", "controls")->data)->value;

	controls[CONTROL_RIGHT] = ((ControlWidget*) getWidget("right", "controls")->data)->value;

	controls[CONTROL_UP] = ((ControlWidget*) getWidget("up", "controls")->data)->value;

	controls[CONTROL_DOWN] = ((ControlWidget*) getWidget("down", "controls")->data)->value;

	controls[CONTROL_FIRE] = ((ControlWidget*) getWidget("fire", "controls")->data)->value;
}

updateControls is somewhat crude, but it gets the job done. For each of our controls (a static array of ints, in demo.c), we're looking up the appropriate widget that is storing the value, and extracting it. CONTROL_LEFT will grab the value of the "left" widget from "controls", and so on. I say this is crude, as the code is looking up the widgets each time, instead of grabbing the reference once and storing it. Again, however, our performance hit will be negligible, and changing the controls isn't something users will be doing a great deal.

There are some other things here that could be improved. For example, we're not ensuring that each control is unique. It is therefore possible, for example, to set Up as the control for everything! We'll look into how we can overcome this in another tutorial.

Finally, let's look at how our customized controls are used. Our controls variable is an array of ints, allowing us to store numbers at an index. We can therefore store SDL_SCANCODE values at these indexes. So, when it comes to testing if a key has been pressed, we take the value from the controls array at a specified index, then use that value with our app.keyboard array, to find if that key is pressed. This is how doPlayer now functions:


static void doPlayer(void)
{
	player.reload = MAX(player.reload - app.deltaTime, 0);

	if (app.keyboard[controls[CONTROL_LEFT]])
	{
		player.x -= (PLAYER_SPEED * app.deltaTime);
	}

	if (app.keyboard[controls[CONTROL_RIGHT]])
	{
		player.x += (PLAYER_SPEED * app.deltaTime);
	}

	if (app.keyboard[controls[CONTROL_UP]])
	{
		player.y -= (PLAYER_SPEED * app.deltaTime);
	}

	if (app.keyboard[controls[CONTROL_DOWN]])
	{
		player.y += (PLAYER_SPEED * app.deltaTime);
	}

	if (app.keyboard[controls[CONTROL_FIRE]] && player.reload == 0)
	{
		fireBullet();

		player.reload = 8;
	}

	player.x = MIN(MAX(0, player.x), SCREEN_WIDTH - player.texture->rect.w);
	player.y = MIN(MAX(0, player.y), SCREEN_HEIGHT - player.texture->rect.h);
}

So, instead of testing app.keyboard[SDL_SCANCODE_UP] to see if the up control is pressed, we're now using app.keyboard[controls[CONTROL_LEFT]]. By default, controls[CONTROL_LEFT] is set to the value of SDL_SCANCODE_UP, but it can be any key we want, and our ControlWidget allows us to set this.

That's it for the widget tutorial. As you can see, it does actually get a bit more involved than might at first be thought, but not overly complex either.

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