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 Honour of the Knights (First Edition) (The Battle for the Solar System)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and an Imperial nation's civil war than either the Confederation Stellar Navy or the government are willing to let on.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a basic widget system —
Part 5: Slider widget

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

Introduction

We're going to introduce another new widget now: a slider. This is the sort of widget that one might use for volumes. If you're offering the ability to change the volume of the sound, music, voices, etc. in games, then a slider widget will be most suitable.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./widgets05 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 change the value of the Slider widgets (sound and music). Note how you can hold the arrows for Sound, but must press it to adjust Music. When you're done, either select Exit or close the window.

Inspecting the code

We've added a slider widget JSON to our options.json file. The definition of the sound widget is like this:

{
	"type" : "WT_SLIDER",
	"name" : "sound",
	"x" : 0,
	"y" : 300,
	"label" : "Sound",
	"step" : 1,
	"waitOnChange" : 0
}

The type is a new type: WT_SLIDER. Most of the other keys will be familiar, though there are two that will come into play later on: step and waitOnChange. Let's get into the code to see how our Slider widget works, starting with defs.h:


enum {
	WT_BUTTON,
	WT_SELECT,
	WT_SLIDER
};

We've added the WT_SLIDER value to our enum, so it can be used throughout the code. Next, we've created the actual struct for our new widget in structs.h:


typedef struct {
	int x;
	int y;
	int w;
	int h;
	int value;
	int step;
	int waitOnChange;
} SliderWidget;

Our SliderWidget will be used as part of a main Widget's data. Like the SelectWidget, the SliderWidget has its own x and y values, so that it can be positions independently. The w and h values are its width and height, which will be used to draw the bar donating the value of the widget. Step will be used to determined how much the widget's value increases or decreased when we interact with it. Finally, waitOnChange will be used to specify whether keys must be pressed to change the value. We'll see how this works when we come to process the widget's logic.

Moving onto widgets.c, we've added a new global variable called sliderDelay:


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

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

	sliderDelay = 0;
}

We're setting the value to 0 in initWidgets. Next, we've made a change to createWidget to support the WT_SLIDER enum value:


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

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

	if (type != -1)
	{
		w = malloc(sizeof(Widget));
		memset(w, 0, sizeof(Widget));
		widgetTail->next = w;
		w->prev = widgetTail;
		widgetTail = w;

		STRCPY(w->name, cJSON_GetObjectItem(root, "name")->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;

		switch (w->type)
		{
			case WT_BUTTON:
				createButtonWidget(w, root);
				break;

			case WT_SELECT:
				createSelectWidget(w, root);
				break;

			case WT_SLIDER:
				createSliderWidget(w, root);
				break;

			default:
				break;
		}
	}
}

For all widgets of type WT_SLIDER, we'll call the new createSliderWidget function. This function is quite simple:


static void createSliderWidget(Widget *w, cJSON *root)
{
	SliderWidget *s;

	s = malloc(sizeof(SliderWidget));
	memset(s, 0, sizeof(SliderWidget));
	w->data = s;

	s->step = cJSON_GetObjectItem(root, "step")->valueint;
	s->waitOnChange = cJSON_GetObjectItem(root, "waitOnChange")->valueint;

	calcTextDimensions(w->label, &w->w, &w->h);
}

We're mallocing a SliderWidget and assigning it to the main Widget's data field. We're also extracting the step and waitOnChange values from the JSON configuration, before finally calculting the main widget's width and height.

With our SliderWidget setup, we can now look at the logic side of things. We've made a simple update to doWidgets:


void doWidgets(void)
{
	sliderDelay = MAX(sliderDelay - app.deltaTime, 0);

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

		app.activeWidget = app.activeWidget->prev;

		if (app.activeWidget == &widgetHead)
		{
			app.activeWidget = widgetTail;
		}
	}

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

		app.activeWidget = app.activeWidget->next;

		if (app.activeWidget == NULL)
		{
			app.activeWidget = widgetHead.next;
		}
	}

	if (app.keyboard[SDL_SCANCODE_LEFT])
	{
		changeWidgetValue(-1);
	}

	if (app.keyboard[SDL_SCANCODE_RIGHT])
	{
		changeWidgetValue(1);
	}

	if (app.keyboard[SDL_SCANCODE_SPACE] || app.keyboard[SDL_SCANCODE_RETURN])
	{
		if (app.activeWidget->action != NULL)
		{
			app.activeWidget->action();
		}
	}
}

We're decreasing the value of sliderDelay each time doWidgets is called, not allowing it to fall below zero. Otherwise, everything else remains the same. Our main logic support for our SliderWidget is done in changeWidgetValue:


static void changeWidgetValue(int dir)
{
	SelectWidget *select;
	SliderWidget *slider;

	switch (app.activeWidget->type)
	{
		case WT_SELECT:
			app.keyboard[SDL_SCANCODE_LEFT] = app.keyboard[SDL_SCANCODE_RIGHT] = 0;

			select = (SelectWidget*) app.activeWidget->data;

			select->value += dir;

			if (select->value < 0)
			{
				select->value = select->numOptions - 1;
			}

			if (select->value >= select->numOptions)
			{
				select->value = 0;
			}

			if (app.activeWidget->action != NULL)
			{
				app.activeWidget->action();
			}

			break;

		case WT_SLIDER:
			slider = (SliderWidget*) app.activeWidget->data;

			if (sliderDelay == 0 || slider->waitOnChange)
			{
				if (slider->waitOnChange)
				{
					app.keyboard[SDL_SCANCODE_LEFT] = app.keyboard[SDL_SCANCODE_RIGHT] = 0;
				}

				slider->value = MIN(MAX(slider->value + (slider->step * dir), 0), 100);

				sliderDelay = 1;

				if (app.activeWidget->action != NULL)
				{
					app.activeWidget->action();
				}
			}
			break;

		default:
			break;
	}
}

We're now handling the SliderWidget in the WT_SLIDER case statement. The first thing we do is extract the SliderWidget from the widget's data field. We then test to see if we're allowed to process the action, by checking if sliderDelay is 0 or the SliderWidget has waitOnChange set. The sliderDelay is important to stop the SliderWidget's value from changing too quickly when we adjust it; a high framerate would mean it would be near-impossble to update the widget with any good accuracy. If the SliderWidget has waitOnChange set, we'll 0 the left and right arrows, to force them to be pressed again. The waitOnChange is important for the same reason as sliderDelay. If we have a high step set on the SliderWidget, we'll struggle to adjust the value properly due to the speed of the change.

The next thing to do is to actually update the SliderWidget's value. We'll do this by adding the SliderWidget's step (multiplied by the dir, to make it negative or positive) to its value. We've decided that our SliderWidget will have a maximum value of 100 and a minimum value of 0, so we'll limit value to this range. In effect, our SliderWidget will represent a percentage from 0 to 100. This will make life easier when it comes to working with the value of the SliderWidget in future.

With our value updated, the only two things left to do is reset sliderDelay to 1, to slow speed of the next update, and also to call the Widget's action function pointer, if one has been set.

That's our logic done. The only thing left to deal with is drawing out SliderWidget. We'll start with updating drawWidgets:


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

	for (w = widgetHead.next ; w != NULL ; w = w->next)
	{
		switch (w->type)
		{
			case WT_BUTTON:
				drawButtonWidget(w);
				break;

			case WT_SELECT:
				drawSelectWidget(w);
				break;

			case WT_SLIDER:
				drawSliderWidget(w);
				break;

			default:
				break;
		}

		if (w == app.activeWidget)
		{
			h = w->h / 2;

			drawRect(w->x - (h * 2), w->y + (h / 2), h, h, 0, 255, 0, 255);
		}
	}
}

For SliderWidgets (WT_SLIDER), we're calling a new function: drawSliderWidget. It's defined below:


static void drawSliderWidget(Widget *w)
{
	SDL_Color c;
	SliderWidget *s;
	double width;

	s = (SliderWidget*) w->data;

	if (w == app.activeWidget)
	{
		c.g = 255;
		c.r = c.b = 0;
	}
	else
	{
		c.r = c.g = c.b = 255;
	}

	width = (1.0 * s->value) / 100;

	drawText(w->label, w->x, w->y, c.r, c.g, c.b, TEXT_ALIGN_LEFT, 0);

	drawRect(s->x + 2, s->y + 2, (s->w - 4) * width, s->h - 4, c.r, c.g, c.b, 255);

	drawOutlineRect(s->x, s->y, s->w, s->h, 255, 255, 255, 255);
}

Like our other widgets, we're drawing the widget in green if it's currently active. Otherwise, we'll render in white. The next thing we do is grab the value of the SelectWidget. We'll take its integer percent value and normalize it to between 0.0 and 1.0, by dividing by 100. After drawing the widget label, we'll draw two rectangles, one filled and one outline. Both rectangles will be drawn using the SliderWidget's x and y coordinates, as well as its width and height. For the filled rectangle, however, we'll multiply the SliderWidget's width by the normalized SliderWidget's value. So, if our SliderWidget's value is 100, the rectangle's width will be the full value of the SliderWidget's w. If it's 50, we'll draw half the value of w. If 28, we'll draw 28% of w, etc. Note how when we call drawRect, we'll adding some inner padding, by adding to the x and y values, but subtracting from the w and h. Our outlined rectangle (drawOutlineRect) is always drawn with the full values of the SliderWidget and always in white.

Our SliderWidget is complete! We can now use it in demo.c. As usual, we'll first be updating initDemo:


void initDemo(void)
{
	Widget *w;
	SliderWidget *s;

	w = getWidget("difficulty");
	w->action = difficulty;
	w->x = 400;
	((SelectWidget*) w->data)->x = 700;

	app.activeWidget = w;

	w = getWidget("subtitles");
	w->action = subtitles;
	w->x = 400;
	((SelectWidget*) w->data)->x = 700;

	w = getWidget("language");
	w->action = language;
	w->x = 400;
	((SelectWidget*) w->data)->x = 700;

	w = getWidget("sound");
	w->action = sound;
	w->x = 400;
	s = (SliderWidget*) w->data;
	s->x = 700;
	s->y = w->y + 16;
	s->w = 300;
	s->h = 32;
	s->value = 100;

	w = getWidget("music");
	w->action = music;
	w->x = 400;
	s = (SliderWidget*) w->data;
	s->x = 700;
	s->y = w->y + 16;
	s->w = 300;
	s->h = 32;
	s->value = 50;

	w = getWidget("exit");
	w->x = 400;
	w->action = quit;

	STRCPY(message, "Adjust options.");

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

We only have two SliderWidgets - sound and music. We're getting those by calling getWidget. We're then setting their function pointers (action), and their value attributes, on both the main Widget and the SliderWidget. For the SliderWidget, we're extracting from the main widget's data field, and setting up the x, y, w and h, to set the size of the horizontal bar. We're also setting initial values of 100 for sound (maximum) and 50 for music (halfway).

Our function pointers for sound and music references similiarly named functions. The sound function is simple:


static void sound(void)
{
	SliderWidget *s;

	s = (SliderWidget*) app.activeWidget->data;

	sprintf(message, "Sound volume: %d%%", s->value);
}

We're just grabbing the SliderWidget from the activeWidget's data, and updating the message with the value. We're doing the same thing in the music function:


static void music(void)
{
	SliderWidget *s;

	s = (SliderWidget*) app.activeWidget->data;

	sprintf(message, "Music volume: %d%%", s->value);
}

We now have three different widgets to work with, ones that can handle core functions in a game. Next, we'll look at creating an input widget, so that users can enter text.

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