« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 17: Intermission: Shop (1 / 2)

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

Introduction

It's time to introduce our shop, where we can upgrade the KIT-E, our fighter, using cash earned from missions. Our shop is quite a complex beast, and therefore the coding on this bit has been split into two parts. The first part will deal with all the setting up, while the second part will focus on the purchasing logic.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-17 to run the code. You will see a window open like the one above. Use the mouse to control the cursor. Click on the various shop items (in grey) to view information about them. There's not much else to do right now, so once you're finished, close the window to exit.

Inspecting the code

As already stated, the shop is complex, due to the logic involved in buying items, which will come next. For now, let's see what goes into the main guts of the shop.

Starting with defs.h:


enum
{
	SI_REPAIR,
	SI_REPAIR_FULL,
	SI_AMMO,
	SI_MAX_HEALTH,
	SI_DAMAGE,
	SI_RATE,
	SI_OUTPUT,
	SI_ROCKET_LAUNCHER,
	SI_HOMING_MISSILE,
	SI_BEAM,
	SI_MINE,
	SI_SHIELD,
	SI_MAX
};

We've created a new enum set, to represent our shop items. SI stands for Shop Item. We have here a full set of the available items to purchase.

Next, over to structs.h, where we've introduced a new struct called ShopItem:


typedef struct
{
	int      id;
	char     name[MAX_NAME_LENGTH];
	char     description[MAX_DESCRIPTION_LENGTH];
	int      price;
	SDL_Rect rect;
} ShopItem;

As the name implies, this will be used to represent an item for sale in our shop. `id` is the id of the item (one of the SI enums), `name` and `description` are the name and description of the item, `price` is how much catnip it will cost to buy, and `rect` is the rectangular position on screen, used to help with collision detection when the mouse is clicked on one of the items.

Now for shop.c. This is where all our shop logic is handled. It's a large file, as you can no doubt already tell! Let's start with initShop:


void initShop(void)
{
	chatTextId = rand() % numChatTexts;

	buyWidget = getWidget("buy", "shop");
	buyWidget->x = 200;
	buyWidget->y = 320;
	buyWidget->disabled = 1;

	equipWidget = getWidget("equip", "shop");
	equipWidget->x = buyWidget->x + buyWidget->w + 20;
	equipWidget->y = buyWidget->y;
	equipWidget->disabled = 1;

	shopItemBackgroundTexture = getAtlasImage("gfx/intermission/shopItem.png", 1);
	shopItemBackgroundHoverTexture = getAtlasImage("gfx/intermission/shopItemHover.png", 1);
	shopItemBackgroundSelectedTexture = getAtlasImage("gfx/intermission/shopItemSelected.png", 1);
	shopItemBackgroundEquippedTexture = getAtlasImage("gfx/intermission/shopItemEquipped.png", 1);

	shopItemTextures[SI_REPAIR] = getAtlasImage("gfx/intermission/repairIcon.png", 1);
	shopItemTextures[SI_REPAIR_FULL] = getAtlasImage("gfx/intermission/repairFullIcon.png", 1);
	shopItemTextures[SI_AMMO] = getAtlasImage("gfx/intermission/ammoIcon.png", 1);
	shopItemTextures[SI_MAX_HEALTH] = getAtlasImage("gfx/intermission/healthUpIcon.png", 1);
	shopItemTextures[SI_DAMAGE] = getAtlasImage("gfx/intermission/damageUpIcon.png", 1);
	shopItemTextures[SI_RATE] = getAtlasImage("gfx/intermission/rateIcon.png", 1);
	shopItemTextures[SI_OUTPUT] = getAtlasImage("gfx/intermission/outputIcon.png", 1);
	shopItemTextures[SI_ROCKET_LAUNCHER] = getAtlasImage("gfx/intermission/rocketIcon.png", 1);
	shopItemTextures[SI_HOMING_MISSILE] = getAtlasImage("gfx/intermission/missileIcon.png", 1);
	shopItemTextures[SI_BEAM] = getAtlasImage("gfx/intermission/redBeamIcon.png", 1);
	shopItemTextures[SI_MINE] = getAtlasImage("gfx/intermission/minesIcon.png", 1);
	shopItemTextures[SI_SHIELD] = getAtlasImage("gfx/intermission/shieldIcon.png", 1);

	mouseBrosPortraitTexture = getAtlasImage("gfx/avatars/mouseBros.png", 1);

	textTimer = 0;

	selectedItem = NULL;
}

We're first setting a variable called chatTextId to a random value of numChatTexts. This is used to display the Mouse Bros intro text, which will be randomly picked when we enter the intermission screen (the latter two variables are setup in loadShopChat). Next, we're setting up our buy and equip widgets (both disabled for now), grabbing some textures for use with drawing the backgrounds of our shop item icons (shopItemBackground*), loading the shop item textures themselves (shopItemTextures), and grabbing the image used for the Mouse Bros avatar (mouseBrosPortraitTexture). We're setting a variable called textTimer to 0, which will be used to control printing out the item descriptions (just like our comms screen). Finally, we're setting selectedItem to NULL, so nothing is selected when we first enter the shop screen.

That's our setup done, so now we can start looking into the logic. The doShop function is where we drive this:


void doShop(void)
{
	doItems();

	doShopButtons();

	textTimer += 2 * app.deltaTime;
}

We're calling doItems and doShopButtons, and also increasing the value of textTimer, to make our text type out (we'll see this in a bit).

doItems is the next function to consider.


static void doItems(void)
{
	int       i;
	SDL_Rect *r;

	hoverItem = NULL;

	for (i = 0; i < SI_MAX; i++)
	{
		r = &shopItems[i].rect;

		if (collision(app.mouse.x, app.mouse.y, 1, 1, r->x, r->y, r->w, r->h))
		{
			hoverItem = &shopItems[i];

			if (app.mouse.buttons[SDL_BUTTON_LEFT])
			{
				app.mouse.buttons[SDL_BUTTON_LEFT] = 0;

				if (selectedItem != &shopItems[i])
				{
					selectedItem = &shopItems[i];

					textTimer = 0;
				}
			}
		}
	}
}

An easy function to understand - we're merely looping through all our shop items, testing if the mouse is hovering over any, and assigning hoverItem to the shop item, before then testing if the left mouse button has been pressed. If so, and this item isn't already the one selected, we'll set selectedItem as the current shopItem, and reset textTimer to 0. This will mean that our text description typing will start over.

Next up is doShopButtons:


static void doShopButtons(void)
{
	doWidgets("shop");
}

We're just calling doWidgets here. Again, our widgets are disabled currently, so this won't do anything.

That's our logic done, for now. We'll handle the actual purchasing in our next part. Time to look into our rendering phase. Starting with drawShop:


void drawShop(void)
{
	drawItems();

	drawShopButtons();

	drawItemDescription();

	drawKITEStats();
}

We're just calling a number of other functions here, that we'll get to one at a time. Starting with drawItems:


static void drawItems(void)
{
	int         i;
	SDL_Rect   *r;
	AtlasImage *texture;

	for (i = 0; i < SI_MAX; i++)
	{
		r = &shopItems[i].rect;

		if (&shopItems[i] == selectedItem)
		{
			texture = shopItemBackgroundSelectedTexture;
		}
		else if (&shopItems[i] == hoverItem)
		{
			texture = shopItemBackgroundHoverTexture;
		}
		else
		{
			texture = shopItemBackgroundTexture;
		}

		blitAtlasImage(texture, r->x, r->y, 0, SDL_FLIP_NONE);

		blitAtlasImage(shopItemTextures[i], r->x + 16, r->y + 16, 0, SDL_FLIP_NONE);
	}
}

This function draws the shop items, and works very much like the section icons, in that it is a composite drawing routine. We first draw a background for our item, changing it depending on if it is the item the mouse is hovering over, or if it's selected, and then we draw both the background and the icon itself.

drawShopButtons is next:


static void drawShopButtons(void)
{
	char text[16];

	drawWidgets("shop");

	app.fontScale = 2;

	sprintf(text, "CN: %d", game.catnip);
	drawText(text, SCREEN_WIDTH - 785, equipWidget->y - 5, 255, 255, 255, TEXT_ALIGN_RIGHT, 0);

	app.fontScale = 1;
}

This code actually does two things: it calls drawWidgets to draw the shop buttons, but also displays the player's catnip (their cash) to the right of the buy and equip buttons, so that as we buy items we can see our remaining cash update.

drawItemDescription follows:


static void drawItemDescription(void)
{
	SDL_Rect r;
	char    *title, *body, *footer, text[16], out[MAX_LINE_LENGTH];

	r.w = 450;
	r.h = 625;
	r.x = SCREEN_WIDTH - (r.w + 200);
	r.y = 100;

	drawRect(r.x, r.y, r.w, r.h, 0, 64, 0, 128);
	drawOutlineRect(r.x, r.y, r.w, r.h, 128, 128, 128, 255);

	blitAtlasImage(mouseBrosPortraitTexture, r.x + r.w - mouseBrosPortraitTexture->rect.w, r.y + r.h - mouseBrosPortraitTexture->rect.h, 0, SDL_FLIP_NONE);

	if (selectedItem != NULL)
	{
		title = selectedItem->name;

		if (textTimer >= strlen(selectedItem->description))
		{
			body = selectedItem->description;
		}
		else
		{
			STRNCPY(out, selectedItem->description, (int)textTimer);
			strcat(out, "*");
			body = out;
		}

		sprintf(text, "Price: %d CN", selectedItem->price);

		footer = text;
	}
	else
	{
		title = "Connection Established";

		body = chatTexts[chatTextId];

		footer = "MouseBros App v1.61";
	}

	app.fontScale = 1.5;

	drawText(title, r.x + 10, r.y + 5, 0, 255, 0, TEXT_ALIGN_LEFT, r.w - 10);

	drawText(body, r.x + 10, r.y + 60, 200, 255, 200, TEXT_ALIGN_LEFT, r.w - 10);

	drawText(footer, r.x + 10, r.y + r.h - 50, 0, 255, 0, TEXT_ALIGN_LEFT, 0);

	app.fontScale = 1;
}

A much longer function now, but actually nothing complex. This renders the big green rectangle on the right-hand side of the screen, that displays information about the item we're interested in, and also draws the Mouse Bros avatar. We start by defined our rectangle as an SDL_Rect called `r`, draw it filled and also with an outline, and then add the Mouse Bros avatar (mouseBrosPortraitTexture) to the bottom right.

Next, we test if an item is selected, and if so we prepare three variable: `title`, `body`, and `footer`. `title` will be the name of the item, while `footer` will display its price. `body` will be the item's `description`. As with our comms section, we'll be typing out the text based on the value of textTimer. We'll be adding a * (star) to the text while we've not typed out the entire `description`. If we don't have an item selected, we're going to set `title` and `footer` to hardcoded strings. The body will be selected from a random string chosen from an array called chatTexts (that we'll see later), using the value of chatTextId that we set in initShop.

With `title`, `body`, and `footer` all determined, we're rendering the text to the screen.

Not an overly complex function, then. Just a little bit too it. Next up is drawKITEStats:


static void drawKITEStats(void)
{
	char text[32];
	int  x, y;

	x = 200;
	y = 405;

	app.fontScale = 2;

	sprintf(text, "Health: %d / %d", game.kite.health, game.kite.maxHealth);
	drawText(text, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y += STAT_SPACING;

	sprintf(text, "Damage: %d", game.kite.damage);
	drawText(text, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y += STAT_SPACING;

	sprintf(text, "Output: %d", game.kite.output);
	drawText(text, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y += STAT_SPACING;

	sprintf(text, "Rate: %d", 1 + (MIN_KITE_RELOAD - game.kite.reload) / KITE_RATE_STEP);
	drawText(text, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y += STAT_SPACING;

	sprintf(text, "Ammo: %d", game.kite.ammo);
	drawText(text, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y += STAT_SPACING;

	sprintf(text, "Secondary: %s", SECONDARY_WEAPON_NAMES[game.kite.secondaryWeapon]);
	drawText(text, x, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y += STAT_SPACING;

	app.fontScale = 1;
}

This function merely displays the KIT-E's stats. The Rate stat uses a calculation to convert Game's kite's `reload` value into a 1 - 5 rating, rather than show the internal timer. Our secondary weapon text looks up the name of the weapon in an array called SECONDARY_WEAPON_NAMES.

That's it for our rendering and logic at the time. We'll be expanding this in part 2 of our shop implementation. We have a few more function to cover now, that deal with setting up the shop's data. The loadShop function is the entry point for this:


void loadShop(void)
{
	loadShopItems();

	loadShopChat();
}

It's calling loadShopItems and loadShopChat. We'll start with loadShopItems:


static void loadShopItems(void)
{
	char     *data;
	cJSON    *root, *node;
	int       id, i, x, y, startX;
	SDL_Rect *r;
	ShopItem *shopItem;

	startX = 200;

	x = startX;
	y = 100;

	data = readFile("data/intermission/shopItems.json");

	root = cJSON_Parse(data);

	for (node = root->child; node != NULL; node = node->next)
	{
		id = lookup(cJSON_GetObjectItem(node, "id")->valuestring);

		shopItem = &shopItems[id];
		memset(shopItem, 0, sizeof(ShopItem));

		shopItem->id = id;
		STRCPY(shopItem->name, cJSON_GetObjectItem(node, "name")->valuestring);
		STRCPY(shopItem->description, cJSON_GetObjectItem(node, "description")->valuestring);
		shopItem->price = cJSON_GetObjectItem(node, "price")->valueint;
	}

	for (i = 0; i < SI_MAX; i++)
	{
		r = &shopItems[i].rect;

		r->x = x;
		r->y = y;
		r->w = r->h = 96;

		x += r->w + 8;

		if (x >= SCREEN_WIDTH - 800)
		{
			x = startX;
			y += r->h + 8;
		}
	}

	cJSON_Delete(root);

	free(data);
}

We load our shop items from a JSON file called shopItems.json, setting up a ShopItem for each entry. We're assuming here that the JSON array contains no more than the number of shop items in our array (IS_MAX). If we want to add more items, we'll need to increase the size of that array. With the data loaded into the items, we next loop through all our ShopItems and setup their positions. We'll align each item to the right of the previous, plus a bit of padding, until we reach a certain point on the screen, at which point we'll reset the `x` position and increase the `y` position, so that the icons appear on the next row down.

So, we're just loading our shop item data, and positioning the icons in rows.

loadShopChat comes last:


static void loadShopChat(void)
{
	char  *data;
	cJSON *root, *node;
	int    i, l;

	data = readFile("data/intermission/shopChat.json");

	root = cJSON_Parse(data);

	numChatTexts = cJSON_GetArraySize(root);

	chatTexts = malloc(sizeof(char *) * numChatTexts);

	i = 0;

	for (node = root->child; node != NULL; node = node->next)
	{
		l = strlen(node->valuestring) + 1;

		chatTexts[i] = malloc(l);
		STRNCPY(chatTexts[i], node->valuestring, l);

		i++;
	}

	cJSON_Delete(root);

	free(data);
}

Our random introduction texts for our shop are loaded from a JSON file. We first count how many items are in the JSON array, and malloc an array of char pointers. We then malloc and copy in a char array for each of the lines. So, in short, we're just loading in our text chat strings.

That's it for shop.c. We'll now move onto incorporating the functions into the main intermission screen. Turning now to intermission.c, we've updated initIntermission:


void initIntermission(void)
{
	int i, x;

	initStarfield();

	initPlanets();

	initComms();

	initShop();

	// snipped

	section = IS_SHOP;

	starfieldTimer = 0;

	// snipped
}

We're calling initShop and also setting `section` to IS_SHOP, to jump straight into it (just for now, by default we'll be viewing the planets).

Next up is the update to `logic`:


static void logic(void)
{
	// snipped

	doSectionIcons();

	switch (section)
	{
		case IS_PLANETS:
			doPlanets();
			break;

		case IS_COMMS:
			doComms();
			break;

		case IS_SHOP:
			doShop();
			break;

		default:
			break;
	}
}

We've added IS_SHOP to our case statement, in order to call doShop.

Finally, the update to `draw`:


static void draw(void)
{
	// snipped

	switch (section)
	{
		case IS_PLANETS:
			drawPlanets();
			break;

		case IS_COMMS:
			drawComms();
			break;

		case IS_SHOP:
			drawShop();
			break;

		default:
			break;
	}

	drawSectionIcons();
}

Likewise, we've added IS_SHOP to the switch statement, in order to call drawShop.

That's part 1 of the shop done. As you can see, this is mostly composed for loading data and displaying it. We intentionally left the purchasing logic out so that we don't drown in this part; it's long enough already!

Ready to buy and equip things? Good, in the next part we'll look into how that all works. There's a little more to it than one might think.

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