Minimaps_Personal_Research

Research about Minimaps Theory & Minimap Practice

View on GitHub

Minimaps Research

I am Alejandro Gamarra Niño, student of the Bachelor’s Degree in Video Games by UPC at CITM. This content is generated for the second year’s subject Project 2, under supervision of lecturer Ricard Pillosu.

Theory about minimaps

What is a minimap?

Minimap is an UI element of the HUD usually located in a corner of the screen. It shows an abstract view of the playfield where the gameplay is being played on a smaller scale. Its main objective is to help the player orientate and help them successfully navigate the environment to reach points of interest and objectives.

enter image description here

When is a minimap necessary? / Introduction to the problem

This question has a different answer depending on the genre and the pillars of the video game but in general the main problems that make the minimap a necessity are:

enter image description here

In order to solve these problems one of the several options is to implement a minimap. Other options are in-game elements like arrows or the famous compas buts this elements are not compatible with most genres.

Minimap types / Different approaches

Mini maps can be classified by many of their attributes as their perspective (Orthogonal, Isometric, 3D) or their shape (Rectangular, Circular) but the two most common types are:

enter image description here

enter image description here

Features

Interactions

Where should we place the minimap? / Different approaches

You may be thinking, does the position of the minimap matter? The truth is that yes, it affects above all in fast-paced games such as First Person Shooters where you usually put the minimap on the left. This is because our western culture tends to pay more attention to the left part of the screen.

enter image description here

A misplaced minimap can not only lead to prolonged reaction times but also to erroneous clicks and misperceptions (depending on where you place other HUD elements). Therefore it is not uncommon to find that the same game released in the West has the minimap or other elements of HUD different position than in the East. Ideally, we would give the possibility to configure its position and size to make the game more accessible for everyone.

As we can verify each video game genre has a distinctive position on the screen:

enter image description here

Selected approach

After seeing the theory, let’s start with the practical part. I’m going to teach you how to program an isometric mini-map from 0 using only SDL with a renderer initialized with Opengles2 . The final result will be a mini-map that can:

Here you can see the final result:

enter image description here

So, we have two classes that will contain everything we need:

Class Minimap Lifecycle

enter image description here

Class Minimap Indicators Lifecycle

Leaving the textures aside, we first need to integrate the indicators. These have a life cycle that only consists of a Constructor, an Update and a Destroy. These can use either a target to update their position or stay at a fixed point. Your position is in map coordinates. This class is a class Minimap friend since it only will treat its private variables.

class Minimap_Indicator
{
public:

	Minimap_Indicator(const fPoint map_pos, const SDL_Rect icon_rect = { 0,0,0,0 }, const SDL_Color color = { 0,0,0,0 }, Object*  target = nullptr);

	void Destroy();

private:

	bool UpdateFromTargetPosition();

private:

	fPoint     map_pos = { 0, 0 };        // - Map position in map units 
	SDL_Rect   icon_rect = { 0,0,0,0 };   // - Icon sprite rect , {0,0,0,0} = No icon
	SDL_Color  color = { 0,0,0,0 };       // - Point color, {0,0,0,0} = No point 
	Object*    target = nullptr;          // - Target used to update map_pos, nullptr = static map_pos
	bool       to_destroy = false;        // - Indicator used to known when is ready to be destroied

private:

	friend Minimap;
};

Load Minimap Info

The first step is to generate the necessary information for the subsequent generation of the texture. In order not to distort the image, we will generate the texture from the width value. The information we have from the map will be obtained from a map daata already loaded in the Map module (class M_Map m_map on app). This is the responsibility of the LoadMinimapInfo() method.

enter image description here

bool Minimap::LoadMinimapInfo()
{
	if (app->map->MapLoaded() == false) { return false; }  // If there isn't a map loaded break load

	// Information from map data ==================================================================

	float tile_width = app->map->data.tile_width;
	float tile_height = app->map->data.tile_height;
	float half_tile_width = (float)app->map->data.tile_width * 0.5f;
	float half_tile_height = (float)app->map->data.tile_height * 0.5f;

	// Number of tiles that fit on one side of the map =============================================

	float tiles_amount = (float)(app->map->data.columns + app->map->data.rows)* 0.5f;

	// Map Pixel Mesures ==========================================================================

	float map_width = tile_width * tiles_amount;
	float map_height = tile_height * tiles_amount;

	// We found texture height from the width with a rule of 3  ===================================

	texture_height = (texture_width * map_height) / map_width;

	// Now we have enough information to know the size of minimap tiles ===========================

	minimap_tile_width = texture_width / tiles_amount;
	minimap_tile_height = texture_height / tiles_amount;

	// We also find a constant to transform from pixels in the world to pixels in the minimap  ====

	aspect_ratio_x = texture_width / map_width;
	aspect_ratio_y = texture_height / map_height;

	// Finally, the blit x offset ===============================================================

	x_offset = (float)app->map->data.rows *minimap_tile_width * 0.5f;

	return true;
}

Unit transformation methods

These functions are the core of our class. They allow us to transform units and thus be able to interact between the map and the minimap with its respective offset x_offset. These formulas are related to the isometric world. You can get more information about isometric maps in this link.

Once we have the transformation methods of units we can generate the texture of the minimap. To do this, iterate throught all the layers tiles and draw a tileset tile sprite in a scaled size corresponding to the width and height of minimap tiles. This is the responsibility of the GenerateMinimapTexture() method.

This method creates the final texture and the minimap texture. These textures will be used later in the drawing of the minimap. What you have to keep in mind is that we change the SDL_Render target in order to draw in a texture (in this case minimap_texture) instead of the default texture that is the texture that the SDL_Window has internally. For more information on this topic I leave the SDL_SetRenderTarget () API.

bool Minimap::GenerateMinimapTexture()
{
	// Create Target Textures ====================================

	final_texture = app->tex->CreateTargetTexture(minimap_rect.w, minimap_rect.h);
	minimap_texture = app->tex->CreateTargetTexture(texture_width, texture_height);

	SDL_SetRenderTarget(app->render->renderer, minimap_texture);
	SDL_SetTextureBlendMode(minimap_texture, SDL_BLENDMODE_BLEND);

	// Blit scaled map to minimap texture =========================

	SDL_Rect sprite_rect = { 0,0,0,0 };
	SDL_Rect section_to_print = { 0,0,0,0 };
	fPoint minimap_tile_pos = { 0,0 };

	for (std::list<MapLayer*>::iterator iter = app->map->data.map_layers.begin(); iter != app->map->data.map_layers.end(); ++iter)
	{
		for (int y = 0; y < (*iter)->rows; ++y)
		{
			for (int x = 0; x < (*iter)->columns; ++x)
			{
				Tileset* tileset = app->map->GetTilesetFromTileId((*iter)->Get(x, y));
				sprite_rect = tileset->GetTileRect((*iter)->Get(x, y));

				minimap_tile_pos = MapToMinimap(x, y);

				if (minimap_tile_width > 1.f && minimap_tile_height > 1.f)
				{
					section_to_print = { (int)minimap_tile_pos.x, (int)minimap_tile_pos.y, (int)minimap_tile_width, (int)minimap_tile_height };
				}
				else
				{
					section_to_print = { (int)minimap_tile_pos.x, (int)minimap_tile_pos.y, 1, 1 };
				}

				SDL_RenderCopy(app->render->renderer, tileset->texture, &sprite_rect, &section_to_print);
			}
		}
	}

	// Reset target texture ==================================================

	SDL_SetRenderTarget(app->render->renderer, NULL);

	return true;
}

Update Final Texture

In UpdateFinalTexture() method we draw all the necessary textures in the final_texture using the render target as we have done before. We will do the drawing in this order ( like photoshop layers ):

enter image description here

void Minimap::UpdateFinalTexture()
{
	// Set render target =========================================================

	SDL_SetRenderTarget(app->render->renderer, final_texture);
	SDL_SetTextureBlendMode(final_texture, SDL_BLENDMODE_BLEND);

	// Clear texture =============================================================

	SDL_SetRenderDrawColor(app->render->renderer, 0, 0, 0, 255);
	SDL_RenderClear(app->render->renderer);

	// Draw minimap texture  =====================================================

	app->render->BlitUI(minimap_texture, texture_pos.x, texture_pos.y, NULL, camera);

	// Draw minimap indicators ==================================================

	SDL_Rect section_to_print = { 0,0,0,0 };
	fPoint pos = { 0,0 };

	for (std::list<Minimap_Indicator*>::iterator iter = indicators_list.begin(); iter != indicators_list.end(); ++iter)
	{
		pos = texture_pos +  MapToMinimap((*iter)->map_pos.x, (*iter)->map_pos.y);

		if ((*iter)->icon_rect.w != 0 && (*iter)->icon_rect.h != 0)
		{
			section_to_print = { (int) (pos.x - (*iter)->icon_rect.w* 0.5f)  ,  (int)(pos.y - (*iter)->icon_rect.h* 0.5f), (*iter)->icon_rect.w, (*iter)->icon_rect.h };
			SDL_RenderCopy(app->render->renderer, icons_texture, &(*iter)->icon_rect, &section_to_print);
		}
		else
		{
			section_to_print = { (int)(pos.x - 2), (int) (pos.y - 2), 4, 4 };
			app->render->DrawQuad(section_to_print, 255, 0, 0, 255, true, false);
		}
	}

	// Draw minimap camera rect =================================================

	pos = texture_pos + WorldToMinimap(camera->camera_pos.x, camera->camera_pos.y) ;

	SDL_Rect camera_rect = { pos.x , pos.y, camera->screen_section.w * aspect_ratio_x ,  camera->screen_section.h * aspect_ratio_y };
	app->render->DrawQuad(camera_rect, 255, 255, 255, 255, false, false);

	// Draw alpha mask texture  =================================================
	if (shape_type == SHAPE_TYPE::CIRCLE)
	{
		SDL_RenderCopy(app->render->renderer, alpha_mask_texture, NULL, NULL);
	}

	// Reset render target ======================================================

	SDL_SetRenderTarget(app->render->renderer, NULL);
}

As you can see, we draw an alpha mask. This is possible thanks to the custom blend mode that has the texture. This blend mode is achieved using the SDL_ComposeCustomBlendMode function. The blend mode that alpha masks need is only available with render that certain drivers like OpenGLES 2.0 or DirectX11 use. In our case, we will use OpenGL ES 2.0, which is already integrated with SDL 2.0. To be able to use it we must:

Uint32 flags = SDL_WINDOW_SHOWN;
flags |= SDL_WINDOW_OPENGL;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
window = SDL_CreateWindow(app->GetTitle(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, flags);

for (int i = 0; i < SDL_GetNumRenderDrivers(); ++i) { SDL_GetRenderDriverInfo(i, &info);

if (info.name == std::string("opengles2"))
{
	index = i;
} } renderer = SDL_CreateRenderer(app->win->window, index, flags); ```
SDL_BlendMode blend_mode = SDL_ComposeCustomBlendMode(SDL_BLENDFACTOR_ZERO, SDL_BLENDFACTOR_ONE, SDL_BLENDOPERATION_ADD, SDL_BLENDFACTOR_ONE, SDL_BLENDFACTOR_ONE, SDL_BLENDOPERATION_REV_SUBTRACT);
SDL_SetTextureBlendMode(alpha_mask_texture, blend_mode); // This belnd mode become transaparent all pixels under the painted area of texture

TODO’S & Solutions

TODO 1: Complete MapToMinimap()

Explication

Transform Map Coordinates to Minimap Pixel Coordinates. Very similar to the MapToWorldF() function on M_Map.

Test

The texture of the minimap will be well generated and you will be able to see it but it will be static. You will also see the position indicator of the player and you can even add alerts by clicking on the map.

enter image description here

Solution

fPoint Minimap::MapToMinimap(const float x, const float y)
{
	return fPoint((x - y) * minimap_tile_width * 0.5f + x_offset, (x + y) * minimap_tile_height * 0.5f);
}

TODO 2: Complete MinimapToMap()

Explication

Knowing how to transform Map coordinates to Minimap Coordinates, complete the inverse function.

Solution

fPoint Minimap::MinimapToMap(const float x, const float y) 
{
	float half_width = minimap_tile_width * .5f;
	float half_height = minimap_tile_height * .5f;

	float x_mod = x - x_offset;

	return fPoint((x_mod / half_width + y / half_height) * .5f, (y / half_height - x_mod / half_width) * .5f);
}

TODO 3: Add an alert (Ping) by clicking over minimap

Explication

When you right click on the minimap, you must add an alert. Use the AddIndicator () function & GetTextureScreenPos() function. The alert sprite sheet rectangle is {32, 32, 32, 32}.

Test TODO 2&3

You can add indicators directly on the minimap with the right click like this:

enter image description here

Solution

if (app->input->GetMouseButton(3) == KEY_DOWN)
{
	if (SDL_PointInRect(&mouse_point, &minimap_rect))
	{
		AddIndicator(MinimapToMap(mouse_point.x - GetTextureScreenPos().x, mouse_point.y - GetTextureScreenPos().y), { 32, 32, 32, 32 });
	}
	else
	{
		AddIndicator(app->map->ScreenToMapF(mouse_point.x + camera->rect.x, mouse_point.y + camera->rect.y), { 32, 32, 32, 32 });
	}
}

TODO 4: Transform the Minimap into a Focused on Player Minimap.

Explication

When interaction_type == NO_TYPE, update the variable texture_pos so that the player stays in the center of the minimap as in the “Focused on the Player” minimap type. The pointer that contains the player is target_to_follow.

Test

You will see the player focused on the minimap and the texture will move:

enter image description here

Solution

switch (interaction_type)
	{
	case INTERACTION_TYPE::NO_TYPE:
         // here
        texture_pos = fPoint(minimap_rect.w * .5f, minimap_rect.h * .5f) - MapToMinimap(target_to_follow->map_pos.x, target_to_follow->map_pos.y); 
		camera_target_pos = camera->camera_pos; 
		camera->MoveToObject(dt, target_to_follow);         // Caemra movement ----------
		break;

	case INTERACTION_TYPE::MOUSE_DRAG:

		MouseDragInput(dt);
		camera->MoveToScreenPoint(dt, camera_target_pos);	// Caemra movement ----------

		break;
	}

TODO 5: Draw Camera Area Borders & Apply Alpha Mask

Explication

Within UpdateFinalTexture() you must find the correct location to draw the camera area borders on minimap. Also placed correctly the alpha mask drawing.

Test

You will see that your minimap now has a circular shape! Oh, and the rectangle of the camera will also have appeared. You can use debug mode F1 and F2 without any problem.

enter image description here

Solution

// After minimap indicators
// Draw minimap camera rect =================================================

	pos = texture_pos + WorldToMinimap(camera->camera_pos.x, camera->camera_pos.y) ;

	SDL_Rect camera_rect = { pos.x , pos.y, camera->screen_section.w * aspect_ratio_x ,  camera->screen_section.h * aspect_ratio_y };
	app->render->DrawQuad(camera_rect, 255, 255, 255, 255, false, false);

	// Draw alpha mask texture  =================================================
	if (shape_type == SHAPE_TYPE::CIRCLE)
	{
		SDL_RenderCopy(app->render->renderer, alpha_mask_texture, NULL, NULL);
	}

	// Reset render target ======================================================

	SDL_SetRenderTarget(app->render->renderer, NULL);

Improvements