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.
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.
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.
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.
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);
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.
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.
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.
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;
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.
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.
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.
18.118.20.231