This article is also available on Medium.
These past few weeks, we’ve been working on the minimap and the fog of war systems for our RTS game. They allow us to get a bird’s-eye view of the terrain and to distinguish unexplored, already explored and in-sight areas. We have a little UI indicator to better materialize the part of the map currently in view of the main camera and some game parameters to easily change the settings of the fog of war.
In this final part, we’re going to wrap this up by adding some final touches:
- a mechanism for direct camera “teleportation” when we click on the minimap
- a small “fade-in” effect on units FOV so we transition a bit from dark to light when we place a building or create a character unit
- conditional rendering for the scene objects depending on their type and on whether they are currently in an in-sight area or simply in an already discovered area
Moving the camera when we click on the minimap
Instant camera teleportation is neat because it allows the player to quickly go to the other edge of the map without having to continuously press some arrow key or stick your mouse on a screen border during dozens of seconds.
So like in most RTS games, we’ll implement this “minimap click”: whenever the player clicks somewhere on the minimap (in an explored zone or not), the camera will move instantly so the matching point in the 3D scene is in the middle of the main camera screen. We will even make it so we can click and drag the minimap indicator and automatically update the main camera accordingly (watch the mouse cursor on the minimap in the bottom-left corner):
The idea is roughly to:
- add a script on the minimap UI object (the image that is fed the render texture and shoved in the bottom-left corner of the interface)
- in this script, handle dragging events and, whenever a movement is detected, send an event to update the (main) camera position
- in the
CameraManagerscript, catch this event and re-place the camera accordingly
Let’s start with our minimap dragging logic. Create a new C# script named
Minimap.cs and drag it on the minimap UI object in your scene hierarchy. Now, add the following content in it:
This script inherits from Unity’s built-in UI-event-handling classes (here
IPointerUpHandler) just like the
BuildingButton class did, several tutorials ago. Thanks to this inheritance, we can directly access two methods in our
OnPointerUp(). Those functions are called automatically by Unity when – you guessed it! – the mouse is on the object and the left button is pressed or released.
Note: because we want to be able to click and drag the indicator to continuously move the main camera, we can’t use a simple “click” event – we need to separate the down and up moments so we can track the potential dragging in the middle.
The class is basically watching those press/release events and setting a
true if the user is currently click-and-dragging (and
false if he/she is simply moving the cursor around and not pressing the left button). Then, if the player is dragging, we get the position of the cursor on the image. Finally, here’s the trick: we get the relative position on the image and re-apply it to the given terrain size. This way, we can compute easily the following:
- my minimap has a 100 pixels width, and my terrain has a 700 units width
- the player clicked 20 pixels from the left
- so it is a 20% ratio on the horizontal axis
- meaning 0.2 * 700 = 140 units to the right on the X-axis in the 3D world
Similarly, we can “transfer” the relative 2D Y-position to a corresponding 3D Z-position.
And in the end, we send this final computed 3D position as an event so our main camera can catch it.
Mathf.Epsilon constant is a predefined very small value that is a nice way to check for zero distances: because of floating point precision, requiring a distance of 0 is always “risky” – the approximation might create a super-small difference that will make the condition impossible to match. Instead, it’s better to require that the distance is below this tiny threshold – the human eye can’t see the difference but the computer will! 😉
On the camera side, we need to update our
CameraManager script a bit. First off, we’ll extract the logic to fix the altitude of the camera compared to the ground beneath into its own private function,
Now, we can easily add the event listener and its callback function – this method receives the 3D position we computed earlier, adds the orthographic view offset to it and re-adjusts the altitude:
And that’s it! You can now click around on your minimap to instantly teleport your camera to this location!
Adding a “fade-in” on units FOV
In the first part of the minimap/fog of war tutorials, we added some logic in our unit-related classes to take care of the FOV object – the “hidden” mesh that is just seeable by the fog of war cameras. Basically, we told the unit to set the scale of the object when it is first created (but it’s still disabled) and to then enable the game object when the unit is actually “placed”.
The problem is that it makes for quite a harsh change of light. When we place a building, the shadow just “pops out” suddenly with no transition whatsoever:
It would be better to feel like the terrain is “discovered” gradually. To do that, we’re going to start with a scaled-down FOV object and use an IEnumerator (yes, again!) to scale it up over a little timespan. To get the right “final” scale (i.e. the actual field of view of the unit), we’ll simply store it in the
Unit class and pass it on to the
There are a couple of things to note in this simple coroutine:
- we use a “do-while” loop: this statement is present in the programming languages of the C family and in C#; it’s similar to a while loop but it is guaranteed to run at least once, and the condition is evaluated at the end of each iteration
stepfloat variable sets how frequently the scale of the FOV object is updated – so it defines how smooth / precise our scale-up transition is, but also how heavy it is in terms of computation
- we use lerping (aka linear interpolation) to automatically compute the scale of the FOV: the
Vector3.Lerp()function allows us to parametrize the variation between two
- you can of course play around with the scale-up time to have our coroutine reveal the terrain more or less quickly
We now have a way better effect when we place a building where we quickly (but not instantaneously!) scale up the FOV so we see how a new part of the map has been explored 🙂
Rendering units and doodads based on the map explored areas
The RTS “fog of war” is great for getting the player thrilled with excitement as he/she discovers the map. But it’s also useful for hiding away some info about the current state of the game in the places this players hasn’t got units – even if you’ve discovered a zone, you shouldn’t have the same amount of information if it’s in sight of one of your units or if you walked through it half an hour ago!
A very common choice for RTS designers is:
- to hide everything in unexplored areas
- to only show the terrain and doodads (i.e. scenery assets) in the already explored (but not in-sight) areas; in particular, you don’t show enemy/NPC units
- to show everything in in-sight areas
We have taken care of the first and last points just by implementing our fog of war. But the second feature is not coded up yet – at the moment, we see everything in already explored areas, there’s no difference with the in-sight zones.
If you take a look at this Unity forum thread, you’ll see that JoshuaMcKenzie gave a very nice and very detailed answer to the question: “how to make a cool fog of war in Unity?”. It’s pretty similar to our current setup, and it has a script for toggling the renderers of game objects on and off based on their position on the map and the state of the fog of war at that position. The idea is to convert the object’s world position to a screen point for the fog of war camera; then, you can simply check what color the matching pixel is on the “unexplored areas” render texture: if it’s white, then the unit is currently in-sight for the player, else it’s either completely in the dark or in an already explored area. So if the pixel is white, the renderer should be enabled, else it should be disabled.
The rest of the script is mostly about getting references to the proper assets in our scene (I’ve slightly adapted the script to get the camera automatically on
Awake(), since the units using this component won’t exist before the game has started):
We can now add this component to all of our units. Remember to drag in the “MeshRenderer” of your prefab in the
myRenderer field of the script — this renderer might be on different parts of your prefab depending on how it’s constructed. In our case, the prefabs have a subchild called “Mesh” with the “MeshRenderer component on it.
So now any House, Tower or Soldier that is not owned by the player disappears as soon as it is not directly in-sight:
Note: we haven’t yet talked about unit ownernship – this will come in a later episode! In the end, we’ll need to conditionally toggle the FOV on or off depending on whether we own the unit or not. But for now, I’ve just placed some buildings on the ground before running the scene and removed their FOV logic entirely 😉
This part of the RTS tutorial was complex and a bit long, but we now have two crucial features for our RTS game: a nice and readable (although basic) minimap, and a simple fog of war system that updates as the player’s units move around, differentiates between unexplored, already explored and in-sight areas, and even toggles the rendering of some objects in the scene based on the state of this fog.
This minimap can still be improved in many ways; for example, we could show the position of players’ units with color spots (corresponding to each player color), or we could add specific icons to denote particular structures on the map (such as a mysterious cave, a treasure chest, etc.).