Making a RTS game #13: Adding a minimap and fog of war 1/3 (Unity/C#)

Today, let’s keep working on our RTS project – we’ll start implementing our minimap and fog of war features!

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:

  1. get the input stream from the camera
  2. re-paint it on an image
  3. 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 ARGB32 or 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 GameManager script, 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 BuildingPlacer class, we’ll add some logic in a Start() function so that it initially spawns our building at the start point :

Conclusion

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.

12 thoughts on “Making a RTS game #13: Adding a minimap and fog of war 1/3 (Unity/C#)”

  1. Hi,
    unfortunately I got a Shader warning for the Alpha Projection stating
    “Custom/Alpha Projection shader is not supported on this GPU (none of subshaders/fallbacks are suitable)”

    Do you have any idea, how I can circumvent this issue?

    1. Hi – is it just a warning, or do you actually have an issue with the visuals and the game? 😉
      Because Unity’s rendering pipelines have changed quite a lot recently (meaning from 2019 to 2021), you often have warnings if you’re not on the exact same version; but the code might work properly nonetheless. (Basically because, as you can see, the shader does not work on GPU but works on CPU, I think)

      So do you have an error, or is it just a warning? 🙂

      Note: for more discussion about shaders issues in this project, you can check out the end of the README on the Github: another reader and I are trying to debug some other shader issues as well ^^

      1. Hi,
        unfortunately I did see this approach, but it won’t change anything… 🙁
        At the moment I can’t assign a Texture to the FogOfWar material using the shader. Therefore the material is of a nice pink color.
        I already loaded the complete project from git, but if I press start there I have basically the same issue of a pink screen.

        1. Hi, what version of Unity are you using?
          When you say you can’t assign it: does the field not appear? Is it that when you drag it it “doesn’t stick”?
          (Note: I’m surprised because I tried the project on various versions of Unity and, apart from the 2019.4 that I refer to in the README, the others seemed to work well… :/)

          I find it weird that a warning results in an error like this… but I’m not a pro of Unity shaders, so I’m probably missing something! :p

          1. I’m using 2020.1.26f1.

            The fields are there and I am allowed to drop my Texture there, but after droping it (or selecting it via selector) the texture is not applied and the field still says ‘none’.

            I do not have a clue, I have not worked with shaders for now and I think I’ll prefer shadergraph somehow, so I’m not really of much help here. 🙁

          2. Arg, I don’t have this version installed and, since it’s not long-term supported (LTS), it’s going to be a pain to find… The weird thing is that I just upgraded the project to 2020.3.18 (which is LTS) – it was long overdue!! – and I don’t have any issues.

            I guess you’ve checked the settings of the RenderTextures extensively and they match the ones in the tutorial?

            Sorry, I’m a bit at a loss here, not sure how I can help… :/

  2. Wow… after weeks of trying to solve the shader problem… here is the solution…

    First, the custom alpha projection was not supported by my GPU (which is the reason for not being able to select the textures).
    To solve this I had to remove the check box for “Auto Graphics API for Windows”. This setting has selected the Direct3D11 api automatically, but to use those shaders it seems like I need the OPENGLES3 api.
    After deselcting the checkbox I was able to add OPENGLES3 to the top of the list and now it looks like it works. We will see what the rest of the tutorial is saying. 😉

    1. Hi! Well done, wow 🙂
      I knew it was something with the shader-hardware compatibility, but I had no idea how to fix it – great job on finding this option!
      I’ll add this info to the Readme when I have the time, perhaps it can help other people too.

      Thanks for sharing!
      (And I think the rest of the tutorial won’t be as hardware-specific… but one can only hope ^^)

  3. A couple of quick ‘gotchas’ for anyone following along…

    Firstly, you need to uncheck the FOV layer from the main camera and minimap camera’s culling mask to prevent the FOV mesh rendering on these cameras; and secondly, you need to add a line to SkillData.cs to enable the FOV on the soldier when instantiated.

    1. Hi,
      thanks for continuously following the series and for pointing these out – it might indeed help other readers! I’ve included your acute comment about the culling masks 😉

      For the FOV, we’ll see in the next episodes how to have it “spread” when the unit is first instantiated but in the meantime, yeah, you can either do it by script or simply toggle on the FOV object in the prefab, I think? Not sure, it’s been a long time ^^

      Anyway, thanks again,
      cheers!

  4. Hello! All is work perfect, but not in Universal Render Pipeline (URP), because Projectors components are not working with this pipeline!
    I was trying to make fog through URP Decal system, but I am working with shaders only couple months, and don’t reached well result. Plus if Main camera go through the Decal box all fog is disappeared, when Projector component have no this problem.
    Can you help any solution?

Leave a Reply

Your email address will not be published.