Today, we’ll continue working on our RTS and fix our minimap!
This article is also available on Medium.
A while ago, we worked on our minimap and we even implemented some logic to move around a little indicator that shows our current main camera view zone.
Although it was a nice opportunity to discuss raycasting and dynamic mesh instantiating/updating, the thing is that this technique is pretty fragile and not the most efficient. If you start to have more “rocky” terrains or, just a hill somewhere, the difference between the flat FOV plane and the terrain itself will completely mess up the computation.
To fix this, today, we are going to completely refactor our minimap management system and instead rely on GL rendering to show the indicator on the UI minimap image. Then, we’ll see how to properly clamp the camera to the map and how to handle the “click and drag” camera re-positioning feature.
Switching to GL rendering
Note: the inspiration for this refactor comes from this nice and short tutorial by Alessandro Valcepina 🙂
GL is a low-level graphics library that is included in Unity and allows you to easily add graphics onto a camera render, in a post-processing image effect and so on by stacking new info in the render buffer.
Here, we’ll use it to draw the camera view zone on the render texture in a very efficient and clean way 😉
The basic idea of this refactor is that:
- we’ll add a new
MinimapManagerscript on our “MinimapCamera” object that computes the current main camera view zone and adds it on top of our minimap render texture (before it is drawn in the UI) using GL utilities
- we’ll remove all the code related to creating/moving the minimap indicator mesh in the
As explained in Alessandro’s tutorial, remember that we shouldn’t raycast directly on our terrain because the ground irregularities could lead to some strange results. That’s why, so far, we’ve used a “MinimapFOVPlane” object with a flat BoxCollider as reference. However this will fail if there is too much difference between the terrain and the flat collider: the indicator will have a wrong offset!
To fix this, the better idea is to turn our “plane” into a “box wrapper” (I’ve renamed the object just to keep it clear) and have it encompass the terrain completely:
We’ll see very soon how to have this collider size adapt to the terrain size (in the next article). For now, just make sure it matches the X and Z dimensions of your Unity terrain object, and that its height is at least equal to the highest of the peaks on the terrain.
Let’s add a reference to this map collider in our
GameManager script so that we can easily access it from anywhere using the Singleton instance:
Of course, don’t forget to drag the BoxCollider in the newly created slot, in the Inspector!
Now, we can use this
mapWrapperCollider in a new util method, in our
Utils script, to compute the current bounds of the camera view zone (i.e. the world position-equivalent of the bottom-left and top-right corners of the screen):
As usual, implementing this as a static method in the
Utils class is nice because it makes it easier to use from various locations in the codebase. Also, I’m caching/pre-defining some variables to optimise the computations a bit (in particular I try and avoid accessing the
Camera.main too often because it requires some heavy lifting from Unity – that is unnecessary to repeat!).
For example, our
GetCameraWorldBounds() method is used in the
OnPostRender() entrypoint is a Unity built-in that is called after the camera using this script is done rendering the frame and it lets us push additional graphics onto the rendered frame using GL.
Just add this script on the “MinimapCamera” object and fill in the line width you want (after some tests, I went for 0.01):
And now, all that’s left to do is to clean up our
CameraManager class and remove all things about creating or moving the minimap indicator mesh! 🙂
If you run the game again, you should see that you get the exact same result as before: you have a little square on your minimap that moves along with your camera and zooms in and out… except it is far more efficient and robust!
Clamping the camera movement
But there is an issue: if you move your camera out of the bounds of the terrain, the indicator will start to freak out because it won’t be able to hit the collider anymore and default everything to zero.
What I want to do is to prevent the camera from moving this far – after all: there’s nothing more to see over there! 😉
We’ll see in the next part how to handle the “click and drag” camera re-positioning that “teleports” the camera when we click on the minimap in the UI; so, in this section, we’ll focus on the translation (with the arrow keys, or when you place your mouse on the screen borders) and zoom features.
A naive approach would be to just block the camera if we try and translate it over some boundaries; so we could define a bunch of min/max thresholds for the X and Z positions and check those when we translate the camera, like this:
But if you try that out, you’ll quickly see that it doesn’t really solve the problem: the camera still overshoots the borders of the minimap! And that’s because, actually, the camera position in itself isn’t enough for the check…
Remember how our camera is rotated towards the ground? This means that if the zone we are looking at is near the border of the terrain, then the camera actually needs to be outside it:
Similarly, if we allow the camera to go up to the edge of the terrain, we’ll be looking in the dark void outside the terrain at one point:
This means that, in fact, we should rather be looking at the point the camera is looking at… and “wrap” it with the current camera view zone to compute the actual limits!
To avoid heavy computation, we can compute the offset between the camera and the world point it is pointing at once at the beginning and then re-apply it to the camera position over and over again. We can compute the camera view zone, too. We’ll only need to re-update these when we zoom in or out:
Now that we have this info, we can change our checks to incorporate these “paddings”:
And because there can be some approximation imprecisions when we translate the camera, we should add a bit of hardcoded buffer distance to avoid overshooting the borders and getting weird minimap displays – I’ll just remove an additional 5 units from the allowed travel zone so I’m sure the camera stays within bounds:
There is one last fix we need to add, for the zoom feature. For now, if we zoom out near the borders of the minimap, the indicator will get crazy just like it did before we implemented thresholds. To avoid this, we need to optionally recompute a best position for the camera after we’ve updated the zoom value and recomputed the current view zone, in the
Now, you see that if I zoom out, the camera is also dragged towards the center of the terrain so that it doesn’t overshoot the limits of the terrain!
Fixing the “click and drag” camera re-positioning
Finally, let’s take care of properly controlling the camera position with our UI. At the moment, the logic we have in our
Minimap script is sort of broken.
Let’s say you have a map with varying heights; then, if you try and move the camera by clicking on the minimap, you’ll see that there is a little delta between the position you aimed for and the actual location your camera teleports to look at when the terrain is lower!
To fix this, we should re-project the world position matching the 2D UI position onto the map wrapper in the
Second, we also need to make sure the camera teleports to a position within the proper bounds. Once again, we’ll use our min/max thresholds as well as the camera view zone size, in the
Now you can see that when you drag the indicator on the minimap, the camera moves around but doesn’t overshoot the limits of the terrain:
Today, we’ve done various fixes to our minimap, both in terms of optimisation and accuracy 🙂
Next time, we’ll continue working on our level maps and see how to store some additional metadata about them. We’ll also see how to handle different terrain sizes to have mini, small, medium or large terrains depending on the level the player picks!