Implementing advanced features for minimaps

In comparison to the simple minimap that we created in the last recipe, a more complex minimap features more detailed attributes, such as the shape of a minimap. In this recipe, we will make the minimap circular using masks. Also, you will learn how to add layers to hide various objects so that they don't feature inside of the minimap. This may be particularly useful if you want to hide specific objects and characters and even some locations throughout your game. Finally, we will look at how to add icons to the minimap, also through the use of layers.

Furthermore, in the There's more… section of this recipe, you can find other advanced features to implement in your minimap.

How to do it...

  1. Since this recipe will teach you how to implement some advanced features in a minimap, it is assumed that the previous recipe for creating a minimap has been completed. However, you don't have to follow the entire recipe. You can also take ideas on how to improve your minimap with these advanced features. So, let's start by shaping it.
  2. In order to transform the minimap's shape into a circle, we have to use masks. You learned about them in Chapter 1, UI Essentials in the Adding a circular mask to an image recipe. We can take the mask that we created in that recipe and use it again here. Thus, we can create a new image, rename it MinimapMask, and set the white circle to its Source Image.

    Tip

    It is possible to use any other shape that we want. Just keep in mind that if you create any shape, the white sections of the mask texture will be the parts that are visible.

  3. We place MinimapMask over our Minimap and parent the latter with the first one.
  4. The next step is to add the Mask component to MinimapMask. To do this, go to Add Component | UI | Mask. Since we don't want to see the original graphic of our mask, we need to make sure that we uncheck Show Mask Graphic.
  5. Now, our minimap should have a circular shape, as shown in the following screenshot:
    How to do it...
  6. Another interesting feature that we can add to the interface of the minimap is a compass. To do this, create another image element and rename it Compass. Finally, attach a sprite to it, like this:
    How to do it...
  7. Now, we have only to place it behind the minimap, and ensure that the part of the interface we want is visible. We should end up with something similar to the following:
    How to do it...

    Note

    In the There's more… section of this recipe, you can find out more about the compass.

  8. Furthermore, minimaps often use icons or symbols within themselves as a way of indicating to the player various objects and even characters. For instance, the player could be represented as a little white arrow and the enemies as red dots. To implement this feature on the minimap, we have to use layers. To edit layers, go to the top-right corner of Unity, click on Layer, and then click on Edit Layers..., as shown here:
    How to do it...
  9. As we can see, a menu now appears in the Inspector, showing all the layers. Some of the layers, from 0 to 7, are built-in layers and cannot be modified. In contrast, all other layers are user layers, which we are going to modify. If you have followed the Making UI elements affected by different lights recipe contained in Chapter 4, Creating Panels for Menus you should see that some of the user layers have already been set. If they are, we can just use the other ones:
    How to do it...
  10. Now, let's add another couple of layers and call them HideMinimap and ShowMinimap. We do this so that all the objects that belong to the first layer are not displayed on the map but in the main camera. Thus, all the objects that belong to the second layer are shown in the minimap but not in the main camera. Ultimately, all the objects that belong to another layer, including nothing/default, are shown in both the cameras (such as the terrain).

    Tip

    According to the design of our game, different objects in the world could already have a layer assigned to them. This may have been done in order to implement other functions of the game. Therefore, we have to extend the concepts that we are covering in this recipe to multiply layers, since we cannot change the layer of an object that is used by other scripts in the game.

    Therefore, instead of creating these two layers, we have to imagine them as a set of other layers that are already implemented — the ones we want to show on the minimap and the ones we don't want to. So in the next steps, every time we perform an operation with one of the two layers, we have to perform that action on all the layers that belong to the set of layers that we want to show in the minimap.

  11. Next, we have to assign these layers to our objects in the scene. Let's start assigning the HideMinimap layer to the Player and to all other objects that we don't want to show in the minimap. This can be because they may be replaced by an icon, or simply because we don't want them to be displayed at all in the minimap (for example, other characters in a scene, treasure to find, and coins to collect). To assign a layer, select the object and change the layer from Default to HideMinimap. This option can be found just under the name of the object in the Inspector, as shown in this screenshot:
    How to do it...
  12. We have to hide this entire set of objects from MinimapCamera. We can do this by selecting it and, then in the Inspector, changing Culling Mask in order to uncheck the HideMinimap layer. In fact, the camera will not render these objects anymore, and as a result, they will not appear on the Render Texture that is attached to our Minimap:
    How to do it...
  13. Now, we have to create the icons that will be displayed in the minimap. In this recipe, for the sake of simplicity, we will use only spheres, which will be rendered as dots from the top-down view of the camera. But feel free to attach your own icon as a texture on a quad. However, keep in mind that you should rotate it along the y axis only, because it is supposed to rotate like an icon. This means that it can only rotate left or right and not in all directions like a 3D object.

    Tip

    By using monochromatic spheres, we don't have to consider rotation, since it will always be rendered as a circle of their color, independent of the rotation they have.

  14. So, let's create a new sphere and a new material for it. We can change the Albedo value of Texture to a color that we want to give to the dot in the minimap that will be our player (for example, cyan). Rename the sphere PlayerMinimapIcon. Put it under the MinimapCamera. If needed, depending on the Size you have set previously, increase or decrease the scale of the sphere so that it appears bigger or smaller in the minimap. Next, place the sphere at the same position of the player and then attach it to the player object.
  15. The main camera is still able to render PlayerMinimapIcon. However, we don't want this. In order to stop the camera from rendering this, we change the PlayerMinimapIcon layer to ShowMinimap, as we did in step 12. Next, cut this layer from the rendering of the main camera by unchecking the Showminimap layer from its Culling Mask.
  16. Repeat these three last steps for all the objects that you want to represent as icons. Also, we don't have to change the cameras this time, since their Culling Masks are already set properly.
  17. Finally, we have created a nice minimap that is ready to be used. Especially if we have already built our 3D world, it will be a pleasure to navigate through it with the minimap. Furthermore, take a look at the There's more… section to implement some even more advanced features.
    How to do it...

How it works...

In this recipe, we added new features to the minimap that we had developed in the first recipe of this chapter.

First, we gave our minimap a shape using the mask component. In fact, as you learned in Chapter 1, UI Essentials, we can use another picture to shape our UI elements.

Then we added a new Image element to our minimap so that we can use a compass in the interface. This works as both a decorative element and also a way to indicate north to the player.

Finally, we used layers. By using them, it is possible for us to tell elements that are rendered by our cameras from elements that are not. This is useful for both hiding elements on the minimap and showing something only in the minimap, such as icons.

There's more...

If we have implemented all the features so far, we already came up with a very good minimap. However, if you want to push your skills and learn more about how to improve your minimap, the following sections will give you the right tools to achieve this.

Limiting the boundaries of the minimap camera

It is good practice to design well-structured boundaries for our level, ideally so that when the player gets closer to the boundaries, the minimap shouldn't display areas of the game environment that the player cannot access. However, in spite of our best efforts, a player may still go close to the boundaries, and since the minimap is centered on the player, it could display these areas, which are outside the bounds. Therefore, we want to make the minimap stop being centered on the player when he is close to the boundaries, and return to tracking the player when he goes back inside the area where the minimap could follow him without showing inaccessible parts of the world.

In general, the shape of the map could be anything, and in this case, we have to set the script to properly distinguish between whether or not the player is close to the edge of this area. Here, for the sake of simplicity, we will implement rectangular edges.

Let's set two variables for our edge. The first variable is a Vector2, where the first value is for the min and max movement along the x axis that the minimap can reach. The second one is also a Vector2, but this time for min and max along the z axis:

public Vector2 xBoundaries;
public Vector2 zBoundaries;

In the Update() function, we want to find out whether the player is inside this area or not. If so, we can just set the position as we usually do. Otherwise, we have to set the minimap to the closest position that it can reach to best track the player when he is outside the area. If the player still manages to view past the boundaries of the map, it is likely that there are issues with the way in which the map is designed, or that there is a bad setting of the boundaries' vectors. Thus, during the design of the map, designers need to ensure that its boundaries are kept adequately constrained to the dimensions of the minimap. Therefore, we can replace the line of code that updates the position with these lines:

float newXPosition = playerTransform.position.x;
float newZPosition = playerTransform.position.z;

if (newXPosition < xBoundaries.x)
  newXPosition = xBoundaries.x;
if (newXPosition > xBoundaries.y)
  newXPosition = xBoundaries.y;

if (newZPosition < zBoundaries.x)
  newZPosition = zBoundaries.x;
if (newZPosition > zBoundaries.y)
  newZPosition = zBoundaries.y;

transform.position = new Vector3 (newXPosition, transform.position.y, newZPosition);

Now let's set the vectors in the Inspector and test it.

Rotating the minimap according to where the player is facing

In the same game, the minimap doesn't always face the same direction, but changes according to where the player is facing. This is so that everything that is displayed is done in a way that is relative to the player's perspective. This is often used in first-person games. In order to implement it, we have to change the rotation of the minimap time after time.

So, at the end of the Update() function, we not only have to move the camera but also have to rotate the minimap according to the player's direction. Therefore, we want to rotate the y axis of MinimapCamera so that it matches the rotation along the y axis of the player. The other two axes are not touched, so set them equal as before. To do this, we can add the following to our script:

transform.rotation = Quaternion.Euler(new Vector3(transform.rotation.eulerAngles.x,playerTransform.rotation.eulerAngles.y,transform.rotation.eulerAngles.z));

As the camera rotates, the view is reflected inside the minimap, which also assumes the same rotation as the player.

Smoothly rotating the minimap compass to point towards the relative north of the game environment

A compass in the minimap is a nice element that complements it, not only as an additional aesthetical element, but also for actually indicating the location of north within our game to the player. Especially if our minimap rotates according to the direction that the player is facing, we cannot use a static compass, and thus we have to move it. However, just as in reality, the compass should not be instantaneous. Therefore, we need to add a slight delay in rotating the compass so that it feels realistic.

We have to store the transform value of the compass. To do this, we need to create a variable for it:

public Transform compass;

Since it is a public variable, we can assign it in the Inspector. It is also worth keeping in mind that we should provide designers with the ability to tweak the velocity of rotation of the compass according to the design of the game. To do this, we again need to add a variable:

public float compassRotationSpeed = 1f;

Because the north of our game could be located anywhere, we need to provide designers with the option of changing the direction of the compass through an offset from the standard north, which can often be the immediate direction that the player is facing when the scene is loaded. Therefore, we should create a variable for this as well:

public float compassOrientationOffset = 0f;

At the end of the Update() function, we have to set the rotation of the compass. This is so that it can accurately point toward north relative to the game environment. Now, in order to make this rotation smooth, we have to perform a Lerp from the current rotation of the compass to the desired one, which assumes the same direction that the player is facing. Furthermore, at this rotation, we can add compassOrientationOffset in order to reposition the location of north so that it reflects the in-game bearing of north. As a final parameter to control the lerp, we take the time from the last frame and multiply it by compassRotationSpeed:

compass.rotation = Quaternion.Lerp (compass.rotation, Quaternion.Euler (0, 0, playerTransform.rotation.eulerAngles.y+compassOrientationOffset), Time.deltaTime * compassRotationSpeed);

We have used the Quaternion.Lerp() function because it works very well with rotations. In fact, when the player changes rotation from +160 degrees to -160 degrees, we would want the compass to traverse the shortest path of rotation possible to reach the final orientation. As a result, in this example, we would want the rotation path to be only 40 degrees, and not 320 degrees, as it would have happened if we had used the normal lerp between angles. By using Quaternions, we can avoid this problem and thus find the shortest path to rotate our compass.

Improving the lighting within the minimap

Sometimes, within the game world, lighting can add to the game experience. However, the minimap does not require the same amount of detailed lighting. In fact, it should be clear and easy for the player to follow while he is traversing the game's environment. As such, we need to take into consideration the fact that the lighting that we have for the minimap allows it to be viewed easily.

If we try to modify the lighting by setting a layer to some lights and then excluding that layer from the Culling Mask of the minimap camera, we won't see any change. This is because the light, along with the shadows that it casts, is independent from the layer and from the Culling Mask of the camera. Therefore, we have to use some advanced features of Unity.

Similar to Start() and Update(), there are other special functions that Unity itself calls at certain moments during all the processes to render the final frame on the screen. In particular, we will implement the OnPreCull() function, which is called before a frame is rendered with a specific camera, and also the OnPostRender() function, which is called when the frame for that specific camera is already rendered.

Tip

Keep in mind that these two functions are called by Unity only if they are implemented in a script attached to an active camera.

These two functions allow us to change the world's state before the camera renders a frame, and then put the world back to its original state after the frame has been rendered. We will see how to use these functions at the end of this section.

In order to avoid having shadows, we can turn the shadow casting of the lights off in our world and then turn it on again after the rendering. Keep in mind that even if we turn off the shadows, there might be some parts of the game that are not illuminated and thus appear like soft shadows. This is because it also depends on which shader the object has, and this affects how it is seen in the 3D world.

Since we have used the Terrain object in Unity to build this test scene, we can change its shader. We can achieve this by going to its settings (select it in the Hierarchy panel and then click on the cog in the Inspector). Then we go to Material and change to Built in Legacy Diffuse. Using a directional light with Shadow Type set to No Shadow and a rotation of 90 degrees on the x axis and zero on the other two axes, it is possible to render a Terrain that appears to have no shadows of any kind.

Shadows are not the only issue that we have to face. For instance, some lights may feel right within the 3D world but not in the minimap. An example of this would be if we have a fire in the game. While a fire may look appealing and contribute to the atmosphere, it is probably not ideal to have it in a minimap. So, we may want to remove it. Furthermore, including something such as a fire inside the minimap as well could be computationally expensive. This is because it needs to be rendered twice, once with the main camera and also for MinimapCamera. However, even if we remove the fire from the minimap through layers, the light of the fire will still be present when the minimap is rendered. Therefore, we have to remove this light as well when we render the minimap.

Additionally, imagine that our game implements a day-night cycle. Ideally, we would like our minimap to always appear the same and not reflect the changes throughout the cycle. Therefore, we have to maintain the same lighting for our minimap. This means that we have to render different settings for the lighting on each of the two cameras.

We can deal with all of this by using the two aforementioned functions. In order to do this, let's create some variables. The first is an array for all the lights that we want to disable when the minimap is rendered. By setting it as public, we can just drag all the lights that we don't want on the minimap in this variable, within the Inspector. So, let's add this to our script:

public Light[] minimapLightsVisible;

Tip

If we have a lot of lights that are useful for the game but not for the minimap, we should put all of them here. Furthermore, by doing this, we can also improve performance.

Now, we need another array for all the lights whose shadows we don't want to cast in our minimap. Even this time, we make it public so that we can set it in the Inspector:

public Light[] minimapLightsNoShadows;

Finally, an array with all the lights we want to render only in the minimap. We can keep these public for the same reason:

public Light[] minimapLightsNotVisible;

Now we can write the special functions that we mentioned earlier. Let's start with OnPreCull(). Here, we have to disable all the lights in minimapLightsNotVisible, make all the lights in minimapLightsNoShadows stop casting shadows, and turn all the lights in minimapLightsVisible on. Therefore, we use these lines:

void OnPreCull (){
  foreach(Light l in minimapLightsNotVisible)
    l.enabled = false;

  foreach(Light l in minimapLightsNoShadows)
    l.shadows = LightShadows.None;

  foreach(Light l in minimapLightsVisible)
    l.enabled = true;
}

Finally, we have to do the opposite process in the OnPostRender() function, as follows:

void OnPostRender(){
  foreach(Light l in minimapLightsNotVisible)
    l.enabled = true;
  
  foreach(Light l in minimapLightsNoShadows)
    l.shadows = LightShadows.Soft;
  
  foreach(Light l in minimapLightsVisible)
    l.enabled = false;
}

However, there is still more about lighting for us to know. In this example, every time we put a light in one of the three arrays, it will be enabled and disabled — every time in the same way. For instance, suppose that we have a light that is turned on or off during runtime in the game environment. If we don't want to render it on the minimap, we include it in the minimapLightsNotVisible array. However, when OnPostRender() is called, it is turned on irrespective of what its state was before the rendering of the minimap. Therefore, a more sophisticated implementation of this technique would include to store the original lighting of the scene in the PreCull() function, and then to restore it back into the PostRender() function.

Ideas for implementing the minimap in closed environments

In a closed environment, this minimap might not work. For instance, there is a multi-storeyed building, and ideally we wish to render on the minimap the floor where the player currently is. Otherwise, our icon could also be hidden by other elements. Therefore, we have to use layers to hide different parts of the building and change the Culling Mask of MinimapCamera at runtime. Another solution is to use different cameras and then switch between them every time the player goes to a new floor. In this case, it's preferable to keep all the other cameras that are not in use disabled, for performance reasons.

Other techniques for minimaps

Of course, the techniques that are explained here are not the only ways to implement a minimap. This section is aimed at giving you an idea about other ways to implement minimaps.

If our graphics team has drawn the levels' schematics, we can use them to implement a minimap. Here, instead of using another camera, we can use these level schematics along with a mask, as we did before in this recipe by shaping the map as a circle. The main issue here is to move and rotate the picture according to where the player is. However, the limitation here is that this is hard when the map keeps changing over time. For instance, if we want to display moving platforms, we have to link them with other pictures or icons in the minimap and move them with respect to the main map schematics. The advantage in this case is performance, since we don't have to render what appears in the minimap frame by frame. Furthermore, the aesthetic aspect of the level schematics as a minimap could have a nicer result than the realistic look of a map with a top view. In addition, if we implement this system in Unity, we don't need Render Textures, and therefore we don't necessarily require Unity Pro.

Another technique is a mix of the following two: one technique that you learned in this chapter, using another camera for a minimap; and the previous one, using level schematics. The basic idea here is to render the top view of the map only once, in order to keep the top view of the map and make gains in performance. So, we can take pictures of the map from the top view by setting all the layers, merge them into a unique picture, and use that one as level schematics with the previous technique.

In both of these implementations, we have to consider how to properly include icons on the minimap. This is because their positions depend on the locations of objects or characters in the real world that they are representing. Again, if we use UI elements as icons, we have to properly position and rotate them on the minimap.

This helps us understand that there isn't a single technique that is best; all of them have their own advantages and disadvantages, which have to been taken into consideration when we design our game. Thus, at this stage, we need to carefully choose the technique that better suits our needs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.20.231