This article is also available on Medium.
In a previous tutorial, we worked on our main camera and we implemented a translation-and-zoom system so we can move around our game view. In most RTS games, we also have a top view of the scene shown in a corner of the UI as a “minimap“. Another common feature in those games (present for example in Blizzard’s Starcraft or Warcraft series) is a mechanism called the “fog of war“: the idea is to limit the player’s vision of the terrain by only lighting up the areas within the range of vision of the player’s units.
Today, we’re going to start implementing these two features to our game – since it’s a bit complex and not as quick to setup as previous systems, I’ve separated this tutorial in three parts and we’ll continue working on it in the weeks to come 😉
In this first part, we’re going to setup the minimap in the UI and implement a fog of war that determines where our scene is lit and where it is kept in the dark.
Creating the minimap
For now, let’s forget about the fog of war and implement the minimap. So if we suppose everything on the map is lit up, here is a preview of the final UI display of our minimap:
To create this minimap, we want to paint a top-view of our terrain on an image, in our UI. To get this view, we can use another orthographic camera – this time, we give it a 90° degrees rotation on the X axis so that it is looking at the ground directly, and we increase its orthographic size so it sees the entire terrain (remember that Unity’s terrains size can be configured on the “Terrain” component, in the settings tab – by default, those terrains are 1000 x 1000 wide). At this point, if you select the camera, you’ll get a preview that looks a lot like we want (see in the bottom-right corner of the “scene” tab)!
But the question is: how do we “transfer” this view to our UI? The process is the following:
- get the input stream from the camera
- re-paint it on an image
- place this image in our UI
We could do it “by-hand”, by getting the camera feed and creating a texture ourselves pixel by pixel… but this would be very inefficient, and way too complicated! Instead, let’s take advantage of Unity’s render textures. As explained in the docs, “a Render Texture is a type of Texture
that Unity creates and updates at run time” – so, basically, it’s going to enable us to automatically and continuously inject the top-view camera’s stream into this texture. And this texture can then be used directly in an image, just like any other 😉
To set this texture up, we need to first create a new “Render Texture” asset, then assign it to our minimap top-view camera as the target texture. To be honest, I haven’t tweaked the settings a lot to try things out, so far it seems the base settings are alright:
Note: we could, however, reduce the size of the texture because, in the end, it won’t be displayed with a large image in the UI. But if you do, it’s better to keep a size with a power of 2 (like 64 or 128), and of course we want a square texture so we need to keep the width equal to the height.
Alright, we’re now ready to integrate this texture to our UI! It’s quite easy to do – you simply need to be careful to add a “Raw Image” UI component rather than an “Image”; and then, you can just drag the minimap render texture into its texture slot:
Tadaa! We now have a minimap displayed in our UI 😉
Adding a field of view to units and a fog of war
Disclaimer: this part of the tutorial is based on Andrew Hung’s “Implementing Attractive Fog Of War In Unity” tutorial. I’ve started from his idea of having two cameras render to two render textures, and then use them for direct projection; but I’ve reduced the number of asset and changed his shader a bit to directly blend the render textures together.
It’s now time to add our second feature: the fog of war. It is described for example in Warcraft 3’s wikipedia as “any area or section of the map that is covered by a grayed area”. It relies on our units having a given field of view (FOV), i.e. a range of vision that can be materialised by a disk around their position, that determines the zones on the map currently “in sight” for the player. We usually distinguish between three types of areas:
- the “unexplored areas”: the zones on the map none of your units ever had in their field of view – it’s completely black and opaque
- the “already explored areas”: the zones on the map your units once had but don’t currently have in their field of view – it’s darkened but semi-visible
- the “in-sight areas”: the zones on the map your units currently have in their field of view – it’s lit up normally and shown without additional light filters
Creating the fog of war
First of, in his post, Andrew implements the field of vision (FOV) of the units. This is done by adding a custom mesh near/on the unit that is somewhat circular and sets the zone that should be in-sight around the unit. For us, the FOV mesh (that is put on a new “FOV” layer) can be added into our units prefab. We need to deactivate it at first so our units (especially the buildings) don’t have an active FOV yet before being placed on the ground. (Otherwise, the player could drag an in-construction building to the edge of the currently discovered areas and see more of the map)
Also, the FOV should probably have a different size depending on the unit – we can set this as a new variable in the
UnitData class, and use it during the initialisation of the
Unit class. To get the best control on our FOV, we’ll also have a reference to our FOV sub-object in the
UnitManager class, and we’ll create an
EnableFOV() method in it. So let’s update our three scripts to add this logic:
Don’t forget to drag and drop the “FOV” game object in your prefab to its “BuildingManager” or “CharacterManager” newly created “Fov” slot 🙂
Then, as Andrew explains in his post, we can reuse render textures for the fog of war feature, just like we did earlier for the minimap. This time, we are going to use those textures not for direct viewing but instead to generate alpha masks that define the unexplored/already explored/in-sight areas on the map. The alpha masks will then be fed to a projector so it can cast light on the proper areas on the map.
So what are these alpha masks exactly? Well, they are just square images with black and white pixels: black means no light and white means full light. Basically, our units are going to mark those masks with their field of view so we now where the projector should cast light, and by definition this will automatically keep the rest of the map in the dark.
We need two render textures (and therefore two cameras, one rendering to each texture) so we can differentiate between “unexplored” and “already explored” areas:
- the “explored areas” texture contains in white the zones that are currently in-sight while the rest is completely black
- the “already explored areas” texture is a bit more tricky: the white corresponds to the areas currently and previously in-sight, which means that we need to store the camera render at each frame without clearing the previous frames
Andrew tells us how to do this in his tutorial – it’s possible thanks to the “Dont’ Clear” flag on the camera dedicated to feeding the “already explored” areas texture (the one called “Shroud” in Andrew’s post). So, to recap, we need to create two render textures with the default options:
Note: Unity’s dropdown for render texture formats depends on the version you have. The default one should be the right format but if in doubt, check that it is set to
R8G8B8A8_UNORM (which is equivalent since it has 8 bits for each of the 4 channels).
I decided to go for a 256 x 256 pixel size because I don’t need too much detail and it provides a dirty but quick blur on the edges. But if you’re really into it (and especially if you’re better than me at shaders!), you’ll be able to improve the shader we’re going to see very soon to incorporate some Gaussian convolution into it and improve the anti-aliasing. No matter the size you choose, remember that it should be a power of 2, it should be a square (so it has the same aspect ratio as our square minimap and our square map) and that the larger the texture, the heavier it is to handle for your computer at runtime…
And we also need two new cameras that only consider the FOV layer we created and output their results to those render textures (with the famous “Don’t Clear” flag for the “already explored areas” computation):
Both cameras are using an orthographic projection, and they have the same size as the entire terrain.
Note: also, as pointed out by Glenn in the comments, remember to uncheck the FOV layer from the main camera culling mask (the one capturing the main image on the screen, and not the minimap) to avoid showing our minimap elements in the main view. For the minimap camera, it is up to you to decide whether you want the FOV to render on the minimap or not 🙂
Now, contrary to Andrew, I won’t be using his texture switch trick because I find I already get pretty ok results without it and it reduces the required computation power, so I haven’t reintegrated his
FogProjection C# script. However, this means that when I place a new building that suddenly reveals a new part of the map, I won’t get a smooth transition. I’ll show you next time how to add some transitioning on this – or you can check out his tutorial if you want the exact same effect as him in your game!
I’ve decided to modify his projection material shader a bit so it doesn’t take in a “previous” and “current” texture but instead the “unexplored” and “already explored” render textures as inputs. The shader then merges those alpha masks into a single output. Finally, this output is applied on a single projector to actually use the mask for our scene lighting.
Here’s the updated shader script:
And this shader is then used on a “FogOfWar” material which is, in turn, fed to the “FogOfWarProjector” object, in his “Projector” component:
For the “FogOfWar” material, make sure that you drag the “unexplored areas” render texture in the “Full Texture” (so it uses the solid black) and the “explored areas” render texture in the “Semi Texture” (so it uses the transparent black with the given “Semi Opacity”). Also, the projector must be set in orthographic mode and it has to match the terrain size.
Yay, we’re now done re-implementing Andrew’s method for a nice fog of war in Unity! 🙂
And if you play your scene, it’s gonna be… pitch black?!
Fixing the lights!
Wait, what? That’s a shame! But that’s because, when we start the game, we don’t have any unit yet, so there is no FOV mesh to use in the fog of war system… and therefore no light in the scene.
If you’ve ever played a RTS game like Starcraft, you’ll notice that there’s always an initial “headquarters” building of some sort. It’s usually the thing that you need to protect and that will be a condition for defeat if it’s ever destroyed. Well, we now have a good reason to add one! Because another advantage is that, by definition, it solves our unlit scene problem.
For now, we’re going to do something quite simple: we’re going to place a “House” building in the middle of the screen when we start the game. Note that, in the end, we will need to have a more advanced logic to compute this start position because we’ll need to make sure it’s on somewhat flat ground and it doesn’t collide with anything (so that the placement is valid). With this first test setup, we’ll just have to make sure that we move our main camera so that it points to an empty enough area. Our basic idea will require steps:
- we’ll add a util method to compute the world position matching the point in the middle of the screen:
The override allows us to call this transform method with another camera than the main one, if we ever need it. Else, we simply use a raycast onto our terrain layer and get the hit point position. The default value we return if we did not get a hit point,
Vector3.Zero, corresponds to a corner of the map. In the final game, it will never be a valid point because we will limit our camera movements so it doesn’t go out of bounds, therefore its middle point will never be a corner of the map.
- then in the
GameManagerscript, we’ll use it to compute the start position :
Note: I’ve stored the
startPosition in the
GameManager rather than the
BuildingPlacer directly because it may be interesting for other game systems in the future…
We also take this opportunity to introduce another Singleton into the game, by adding a Singleton for the
GameManager script. Since there is only one of those per scene, it makes sense – and it will make it easier to call functions on this object from other scripts without having to pass the value around, or calling lots of events. I think that in our precise init case, it’s less of an overload to call the method on the Singleton than to setup additional events, plus it lets us play with other concepts. The Singleton is a simple public static variable that is setup in the
Start() method of the class.
- and finally, in our
BuildingPlacerclass, we’ll add some logic in a
Start()function so that it initially spawns our building at the start point :
At this point, we can move our units around the map to gradually discover unexplored areas. This demo has some additional UI raw images that show the current state of the “unexplored areas” and “already explored areas” render textures in the bottom right corner; note how the “explored” one doesn’t clear and stacks the frames continuously, and how this directly impacts the lighting of the scene:
Next week, part two of this (sub?)tutorial will add some additional UI info to the minimap and a few game parameters to make our fog of war system easier to configure.