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

Android Games

DDDDD
Number Blocks
Match 3 Warriors

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
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Rogue tutorial
Wed, 29th September 2021

SDL2 Gunner tutorial
Thu, 26th August 2021

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (6)
games (37)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (8)
water-closet (3)

Books

« 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 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 here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.

Comments

Share your comments and thoughts below. All comments are anonymous and cannot be edited.

 

Mobile site