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


H1NZ

Arriving on the back of a meteorite, an alien pathogen has spread rapidly around the world, infecting all living humans and animals, and killing off all insect life. Only a handful are immune, and these survivors cling desperately to life, searching for food, fresh water, and a means of escape, find rescue, and discover a way to rebuild.

Click here to learn more and read an extract!

« Back to tutorial listing

— Simple 2D map editor —
Part 4: Scrolling and picking entities

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

Introduction

We're almost feature-complete with our basic map editor. There are a few more things that will be nice for us to add, so in this part we'll be expanding the size of the map, to allow for scrolling, and also adding in the ability to "pick" objects and move them around, rather than deleting and recreating them.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./editor04 -edit example to run the editor. You will see a window open, displaying a scene like the one above. The controls from the previous tutorial apply. The view can now be scrolled around, by using the WASD control scheme. The scene will stop scrolling when at the limits of the map. Users can also press 3 on the keyboard to enter "Pick" mode. In this mode, an entity can be clicked on with the left mouse button, and moved to a new position. Clicking the left mouse button again will drop it in the new place. Once you're finished editing, press Space to save the map. If there are errors, these will be logged to the terminal. With your map saved, close the window to exit. As before, you can play the map by using ./editor04 -map example.

Inspecting the code

Once again, the focus of this update is mostly on editor.c (with a single visit to map.c). Adding in our entity picking and map scrolling is a very simple affair, since we've already done much of the ground work. Once again, we'll be snipping away blocks of logic we're not interested in, so we can focus on the changes and additions that are most relevant.

The first thing we've done is update our MODE enum:


enum
{
	MODE_TILES,
	MODE_ENTITIES,
	MODE_PICK
};

We've added a new entry: MODE_PICK. When in this mode, we'll be able to pick entities, and move them around the map.

Now onto initEditor:


void initEditor(void)
{
	app.dev.isEditor = 1;

	// snipped

	moveTimer = 0;

	currentTile = 1;

	mode = MODE_TILES;

	// snipped
}

We've added in a variable in editor.c called moveTimer. This is a variable that will control the speed of our scrolling, when moving around the map. We'll see this in use when we come to to keyboard controls.

First, let's look at the updates to `logic`:


static void logic(void)
{
	// snipped

	moveTimer = MAX(moveTimer - app.deltaTime, 0);

	doMouse();

	doKeyboard();

	activeObjectArrowBob += 0.1 * app.deltaTime;
}

Just one new line here - we're updating the value of moveTimer, reducing it and limiting the result to 0.

There's nothing more to discuss here, so look at the updates to doMouse:


static void doMouse(void)
{
	if (isInsideMap(mouseTile.x, mouseTile.y))
	{
		if (mode == MODE_TILES)
		{
			// snipped
		}
		else if (mode == MODE_ENTITIES)
		{
			// snipped
		}
		else
		{
			if (app.mouse.buttons[SDL_BUTTON_LEFT])
			{
				app.mouse.buttons[SDL_BUTTON_LEFT] = 0;

				if (currentEntity == NULL)
				{
					pickEntity();
				}
				else
				{
					currentEntity = NULL;
				}
			}
		}
	}
}

We've added in an else block to our mode testing. If we're not in MODE_TILES or MODE_ENTIITES, we're going to work with the picking mode. In this mode, we're only supporting the left mouse button; the right button and the scroll wheel will do nothing. When the left mouse button is pressed, we'll test if currentEntity is NULL. If so, this means that we currently don't have an entity picked, so we'll call pickEntity, a new function. We'll come to this one in a moment. Otherwise, we'll set currentEntity to NULL. As we've seen before, currentEntity's `x` and `y` values are updated with the mouse position, so the picked entity will follow the mouse around. Therefore, simply setting currentEntity to NULL will result in the entity being relocated.

Nice and easy. Now for pickEntity:


static void pickEntity(void)
{
	Entity *e;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		if (collision(app.mouse.x + stage.camera.x, app.mouse.y + stage.camera.y, 1, 1, e->x, e->y, e->w, e->h))
		{
			currentEntity = e;
			return;
		}
	}
}

There are probably no surprises here - when picking an entity, we simply loop through all the entities in the stage, looking for one that intersects the mouse pointer (with help from the `collision` function, camera position included). We then set currentEntity to `e`, the entity in our iteration, and return from the function. This means that if two entities are overlapping, we'll select the first one we find, so some micromanagement might be required by the user when it comes to grabbing the correct object (as with most other editors..!).

Nothing taxing so far! Now onto doKeyboard:


static void doKeyboard(void)
{
	int dx, dy;

	moveTimer = MAX(moveTimer - app.deltaTime, 0);

	if (moveTimer <= 0)
	{
		dx = dy = 0;

		if (app.keyboard[SDL_SCANCODE_A])
		{
			dx = -MAP_TILE_SIZE;
		}

		if (app.keyboard[SDL_SCANCODE_D])
		{
			dx = MAP_TILE_SIZE;
		}

		if (app.keyboard[SDL_SCANCODE_W])
		{
			dy = -MAP_TILE_SIZE;
		}

		if (app.keyboard[SDL_SCANCODE_S])
		{
			dy = MAP_TILE_SIZE;
		}

		if (dx != 0 || dy != 0)
		{
			stage.camera.x = MIN(MAX(stage.camera.x + dx, -SCROLL_OVERSCAN), (MAP_WIDTH * MAP_TILE_SIZE) - SCROLL_OVERSCAN);
			stage.camera.y = MIN(MAX(stage.camera.y + dy, -SCROLL_OVERSCAN), (MAP_HEIGHT * MAP_TILE_SIZE) - SCROLL_OVERSCAN);

			moveTimer = 3;
		}
	}

	// snipped

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

		mode = MODE_TILES;

		currentEntity = NULL;
	}

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

		mode = MODE_ENTITIES;

		currentEntity = entities[currentEntityIndex];
	}

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

		mode = MODE_PICK;

		currentEntity = NULL;
	}
}

Okay, so there's a few new things happening here, since we're handling the scrolling and the option to enter picking mode. First off, we're testing whether moveTimer has now hit 0, and, if so, we'll start testing our movement keys. We set two variables called `dx` and `dy` to 0, as our control variables. We then test our WASD control scheme. If A or D are pressed, we're going to set `dx` to -MAP_TILE_SIZE or MAP_TILE_SIZE, depending on the key. We next test W and S, and set `dy` to -MAP_TILE_SIZE or MAP_TILE_SIZE, again depending on the key.

Next, we test if `dx` or `dy` is a non-zero value. If so, we're going to add `dx` to Stage's camera's `x` value, limiting the resulting value to the bounds of our map, albeit with some overscan (SCROLL_OVERSCAN). Adding in this overscan allows us to move beyond the bounds of the map by a certain amount (SCROLL_OVERSCAN is defined as MAP_TILE_SIZE * 8). This makes for a more comfortable editing experience, as we don't need to reach into the corners and edges, just to update the tiles at the extremities of our stage. We do the same for `dy`, adding it to Stage's camera's `y` value, and limiting the result to stay within the map bounds. With that done, we set moveTimer to 3, to prevent us from scrolling again too soon; we don't want the screen to scroll too fast (and it'll be faster at high frame rates!).

That's our movement done. Next, we've added in a test for pressing 3 on the keyboard. If so, we're going to clear the key, and set `mode` to MODE_PICK. We're also setting currentEntity to NULL, to ensure we pick a fresh entity (we've also updated the other mode selections with similar logic).

That's all there is to the keyboard handling updates. Now we just need to have a look at the updates to the rendering. These are simply tweaks and won't take long.

Starting with drawTopBar:


static void drawTopBar(void)
{
	char text[MAX_LINE_LENGTH];

	drawRect(0, 0, SCREEN_WIDTH, 48, 0, 0, 0, 192);

	if (mode == MODE_TILES)
	{
		sprintf(text, "Pos: %d,%d", mouseTile.x, mouseTile.y);
	}
	else if (currentEntity != NULL)
	{
		sprintf(text, "Pos: %d,%d", (int)currentEntity->x, (int)currentEntity->y);
	}
	else
	{
		sprintf(text, "Pos: -,-");
	}

	// snipped
}

We've added an else clause to our existing if-checks when it comes to rendering the position ("Pos"). Now, if currentEntity is NULL, we're rendering a string to indicate an empty or unknown position. This will happen in picking mode when we're not handling an entity. The rest of the function remains the same.

The last function we need to update is drawBottomBar:


static void drawBottomBar(void)
{
	int x, x2, i, j;

	drawRect(0, SCREEN_HEIGHT - 54, SCREEN_WIDTH, 54, 32, 32, 32, 255);

	// snipped

	if (mode != MODE_PICK)
	{
		x = (SCREEN_WIDTH - MAP_TILE_SIZE) / 2;

		drawOutlineRect(x, SCREEN_HEIGHT - MAP_TILE_SIZE - 2, MAP_TILE_SIZE, MAP_TILE_SIZE, 255, 255, 0, 255);

		blitAtlasImage(activeObjectArrowTexture, x + (MAP_TILE_SIZE / 2), SCREEN_HEIGHT - 64 + (sin(activeObjectArrowBob) * 8), 1, SDL_FLIP_NONE);
	}
}

The only change we've made here is to test if we're not in picking mode. If we're not, we'll be rendering the currently selected tile / entity rectangle and arrow. When in pick mode, we don't want to do this, as tiles and entity aren't being selected.

That's everything for editor.c. Before we close, let's look at map.c quickly. You will have noticed while scrolling around that when outside the bounds of our map (in the overscan areas) a checkerboard pattern appears. This is done in map.c. We'll quickly look at how we're doing this.

Starting with initMap:


void initMap(void)
{
	loadTiles();

	checkerboard = getAtlasImage("gfx/editor/checkerboard.png", 1);

	loadMap();
}

We're loading an AtlasImage here called checkerboard.png, and assigning it to a variable called `checkerboard`.

Now, to see how it's used, let's look at drawMap:


void drawMap(int layer)
{
	int x, y, n, x1, x2, y1, y2, mx, my;

	// snipped

	for (y = y1; y < y2; y += MAP_TILE_SIZE)
	{
		for (x = x1; x < x2; x += MAP_TILE_SIZE)
		{
			if (isInsideMap(mx, my))
			{
				n = stage.map[mx][my];

				if ((n > 0) && ((layer == LAYER_BACKGROUND && n < 100) || (layer == LAYER_FOREGROUND && n >= 100)))
				{
					blitAtlasImage(tiles[n], x, y, 0, SDL_FLIP_NONE);
				}
			}
			else if (app.dev.isEditor)
			{
				blitAtlasImage(checkerboard, x, y, 0, SDL_FLIP_NONE);
			}

			mx++;
		}

		mx = stage.camera.x / MAP_TILE_SIZE;

		my++;
	}
}

When drawing our tiles, we're first calling isInsideMap before calling blitAtlasImage. Notice the else-if clause that follows. We're testing to see if we're in editor mode (isEditor). If so, we're calling blitAtlasImage, using the `checkboard` image. So, whenever we're in editor mode and move outside the bounds of our map, we'll see that pattern being rendered.

And that's it! Hurrah! Another part down! We've very nearly finished our little editor. A few more nice-to-haves and we'll be done. So, in the final part, we'll add in a mini map, that will display the overall look of the map we're working on, and also update the UI to display a message when we save.

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