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 3: Loading a widget set

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

Introduction

Before we delve deeper into creating widgets, we should look into loading widgets from a file. This will make certain aspects of our widget building process easier, as you will come to see. For this, we'll simply be creating a file called data/widgets/title.json, which will contain our important widget details.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./widgets03 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 Space or Return on the highlighted menu option to action it. When you're done, either select Exit or close the window.

Inspecting the code

We're now defining our widgets in a JSON file called title.json. We're going to be doing this for the remaining tutorials parts, so it's best to know what to expect. Looking at the JSON file, we don't see anything out of the ordinary:

[
	{
		"type" : "WT_BUTTON",
		"name" : "start",
		"x" : 0,
		"y" : 200,
		"label" : "Start"
	},
	{
		"type" : "WT_BUTTON",
		"name" : "load",
		"x" : 0,
		"y" : 250,
		"label" : "Load Game"
	},
	{
		"type" : "WT_BUTTON",
		"name" : "options",
		"x" : 0,
		"y" : 300,
		"label" : "Options"
	},
	{
		"type" : "WT_BUTTON",
		"name" : "credits",
		"x" : 0,
		"y" : 350,
		"label" : "Credits"
	},
	{
		"type" : "WT_BUTTON",
		"name" : "exit",
		"x" : 0,
		"y" : 400,
		"label" : "Exit"
	}
]

It's a JSON array, holding a bunch of objects. Those are the widgets we were originally setting up in the initDemo function. We'll be loading them in widgets.c from now on. Pay attention to the type field. It's a string. What we'll be doing is mapping the string to a number when we load the widgets. Let's look at defs.h, where we've set up an enum to handle our types:


enum {
	WT_BUTTON
};

WT stands for Widget Type. We could've called it just BUTTON, but it's a good idea to prefix these things, in case there is a conflict with another enum later on during development.

All our major changes have happened in widgets.c, so let's take a look, starting with initWidgets:


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

	loadWidgets("data/widgets/title.json");
}

We're now making a call to a new function called loadWidgets, passing in the name of the file we want to load our widgets from. loadWidgets itself is a simple function:


static void loadWidgets(char *filename)
{
	cJSON *root, *node;
	char *text;

	text = readFile(filename);

	root = cJSON_Parse(text);

	for (node = root->child ; node != NULL ; node = node->next)
	{
		createWidget(node);
	}

	cJSON_Delete(root);

	free(text);
}

We're loading our file data by calling our readFile function (defined in util.c) and passing in the filename. We're then using cJSON_Parse to convert the text into a JSON object, with which to work. With that done, we're looping through our JSON array, and calling createWidget on each object. Once we're done, we're deleting our JSON object and the text we loaded, to free the memory.

createWidget is an important new function, as it's essentially our widget factory. Note that this function replaces our original createWidget function, where we would pass in the name of the widget to create. It's also static within widgets.c, making it private. It does inherit some of the original code, however, as we'll see:


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;

			default:
				break;
		}
	}
}

The first thing we'll do is find out what type of widget we want to create. We do this by extracting the "type" field from our JSON object and passing it to a function called getWidgetType. If this function doesn't return -1, we'll know it's a valid type and move onto creating the widget. Looking at the JSON data at the top of the tutorial, we'll see that this is just WT_BUTTON right now. We'll see more on this function in a bit. Continuing with creating our widget: as before, we're setting up it's next and prev variables, to act as a double-linked list. After that, we want to extract our data from the JSON object we've passed into the function. Setting the name and label of the Widget is as simple as using our STRCPY macro to copy the char data. Next, we're setting the Widget's x and y values to those defined in the JSON object.

With the base widget data fetched, we can now move onto setting up the data for each widget type. Again, we've only got one widget type right now, we our switch statement for handling the type only deals with WT_BUTTON. This will be expanded our in future, as we add more widget types.

That's our createWidget function done, so we can move onto the other functions we've added. There's not a lot to getWidgetType, as we'll see:


static int getWidgetType(char *type)
{
	if (strcmp(type, "WT_BUTTON") == 0)
	{
		return WT_BUTTON;
	}

	SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Unknown widget type: '%s'", type);

	return -1;
}

We're just performing a strcmp on the type string that is passed into the function and returning a matching enum. Again, we're only supporting WT_BUTTON right now, so there isn't a lot more to it. If we don't recognise the widget, print a warning and return -1. This will cause our createWidget function to skip building the widget.

Our other function is createButtonWidget. This is where we'll do some extra work to setup additonally widget details:


static void createButtonWidget(Widget *w, cJSON *root)
{
	calcTextDimensions(w->label, &w->w, &w->h);
}

Not a lot going on - we're calling our calcTextDimensions function (defined in text.c) to calculate the size of the widget, using its label. Knowing the size of our widget can help us later with layout (as we'll see when we come to using it in demo.c).

The final function we should look at is getWidget. This function is important to us, as we'll need a way to find the widgets that were loaded:


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

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

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

	return NULL;
}

To find our Widget, we're just going to loop through our list of widgets and return the one with a matching name. If we don't find the widget, we'll print an error and return NULL.

We've also made a minor change to drawWidgets:


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

	for (w = widgetHead.next ; w != NULL ; w = w->next)
	{
		if (w == app.activeWidget)
		{
			c.g = 255;
			c.r = c.b = 0;

			h = w->h / 2;

			drawRect(w->x - (h * 2), w->y + (h / 2), h, h, 0, 255, 0, 255);
		}
		else
		{
			c.r = c.g = c.b = 255;
		}

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

Instead of drawing a right chevron, we're calling drawRect, and displaying a green square on the left-hand side of the active widget. The size of the square is being determined by the height of the widget itself.

That's all our changes to widgets.c done. We now only have changes to demo.c to consider, as the rest of our work was restricted to widgets.c. The only change to demo.c was in the initDemo function:


void initDemo(void)
{
	Widget *w;

	w = getWidget("start");
	w->x = (SCREEN_WIDTH - w->w) / 2;
	w->action = start;

	app.activeWidget = w;

	w = getWidget("load");
	w->x = (SCREEN_WIDTH - w->w) / 2;
	w->action = load;

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

	w = getWidget("credits");
	w->x = (SCREEN_WIDTH - w->w) / 2;
	w->action = credits;

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

	message = "Select a widget!";

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

Now, instead of calling createWidget (which is private in widgets.c) we're calling getWidget and passing in the name of the widget we want. For each of our widgets, we're adjusting the x value to centre it on screen. As our widget's width is known (it's w value, as setup in createButtonWidget), this is simple task of subtraction and division.

And that's all we needed to do to load our widgets. Other than the adjustments in demo.c, everything else was done in widgets.c and is transparent to even the init functions of the main application. In our next part, we'll do something more interesting and look at creating a new type of widget.

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