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

In this new Unity RTS programming tutorial, we’ll continue working on our minimap and fog of war features!

This article is also available on Medium.

Last time, we implemented a basic minimap and a fog of war system that allows us to change the lighting of the scene depending on the field of vision of the player’s current units. Today, we are going to improve both these features by adding:

  • some game parameters to make the fog of war easier to configure
  • a UI indicator that shows what part of the map the player is currently looking at through the main camera

Updating our game parameters

At the moment, we use our first building reference as our “headquarters” (it’s the “House” prefab)… but perhaps this could be added to our game parameters to change it more easily?

In a previous tutorial, we created a new Scriptable Object to hold the parameters for our current game. Today, we’ll add two new variables in it:

  • the reference to the initial building to place
  • a new boolean to enable or disable the FOV computation and the fog of war – this will make it way easier for plenty of tests, when we’re not actually working on this system!

To add this, it’s pretty simple:

  • in our GameParameters class, we implement the new fields:
  • and in our GameManager, we use the FOV boolean at the very beginning of the game to know if we should set our “FogOfWar” object to active or inactive:

The initial building reference will be accessible by our BuildingPlacer directly on the Singleton instance, so there’s no need to worry about it. We simply need to update the BuildingPlacer‘s Start() method:

And that’s it! We can now easily toggle the FOV/Fog of war mechanic in our editor so that, next time we run the prototype, the system is on or off – and we automatically instantiate a building of the chosen type when the game starts 🙂

Note: be careful, it doesn’t directly change the scene if you are currently running the game — it will only work when you restart it. We’ll see in a couple of tutorials how to have some our parameters update dynamically at runtime.

Since we now have a few parameters on different topic, a quick improvement we can do to our game parameters (just for our own comfort as designers) is to add headers to separate those variables in different groups. This is done in the script, with Unity’s “Header” utility:

Thanks to these headers, it’s way easier to spot our variables!

Adding a “current-view” UI indicator on the minimap

For now, it’s kind of hard to relate the main camera view with the minimap. In particular, we don’t know exactly what part of the map we are currently looking at. Although in the early game it will be easy because only a small part of the terrain will be revealed, as we discover more and more areas it will become significantly harder to distinguish one from the other and precisely pinpoint where the camera is looking.

RTS games often show on the minimap some additional UI to indicate the current field of view of the main camera. In our case, this field of view is a simple rectangle that grows bigger when we zoom out, shrinks down when we zoom in and moves around the minimap when we translate the main camera. Remember how, a couple of episodes ago, we decided to simulate the isometric view but keep our camera parallel to the borders of the terrain? This is going to pay off now!

To show the current field of view of the main camera, we’ll need to do the following:

  1. get the four corner points on the main camera screen
  2. project them on the ground to get their real world positions
  3. create some basic square shape to link those points and show them in our minimap camera
  4. place the object on a specific layer so it is only visible from the minimap camera

Here is a side-view of the setup with the main camera in red and the minimap camera in green – the red dots are the “corners” of the main camera screen that are then re-filmed by the minimap top-view camera:

In other words, we are going to create a “real” 3D object in our scene to materialize the main camera field of view that will be re-filmed by the minimap camera. We will need to update this object when we zoom in/out and when we move the main camera. Also, we don’t want this indicator to be affected by the projector, so we’ll give it an “unlit” material, i.e. a material that has a solid color, independent of the light it receives.

An important note: rather than projecting the camera “corners” directly on our terrain, we will be using a flat plane – otherwise, mountains and holes will create big deformations in the field of view visualization!

This plane will be put on a specific layer so we can optimize our raycasting by only considering this layer in the calculations. The minimap indicator itself will need to be on its own layer so we can hide it in the main camera view and show it in the minimap camera view.

So, first things first: let’s prepare the new layers for our indicator. Just like we did for the FOV layer in the previous tutorial, we are going to create two new layers: “FlatTerrain” and “Minimap”.

Now, let’s add the new projection plane where will get our camera FOV points from. Go to the “GameObject > 3D > Plane” menu to instantiate a new primitive of type plane. Then, remove the “MeshFilter” and “MeshRenderer” components: we want our projection plane to be invisible so it doesn’t block the view of the real terrain. Also, replace the “Mesh Collider” with a “Box Collider”, and scale it up to fit the size of your terrain:

  • in our case, the shape is just a big flat cube, anyway
  • it is way more optimized since the “Mesh Collider” is heavier in computation (it requires math calculations for each vertex on the collider, whereas the “Box Collider” assumes the mesh is just a basic cube)

And finally, put the object on the newly created “FlatTerrain” layer. In the end, we get a very simple (invisible) object like this one:

Note: I’ve reduced the size of my terrain a bit to a 700 x 700 size so that the minimap indicator is bigger and it’s clearer on my screenshots. If you keep the default 1000 x 1000 size, the indicator will be smaller because the zone of the map you’re seeing is comparatively smaller.

We have to move this object just below the real terrain so it doesn’t collide with our building-placing raycasts – I placed it at Y = -1.

Before we jump into the C# scripts, here is the unlit material we’ll apply to our minimap indicator – it uses a very basic built-in Unity shader, and you can choose the color you prefer for the indicator:

Now, time to code! We are going to create a mesh in C# and update it dynamically so it maps to the projected camera “corner” points.

Step 1: we need to create a new function in our Utils class to get the world positions matching the main camera screen “corners”, ScreenCornersToWorldPoints() – we’ll also be re-using the MiddleOfScreenToWorldPoint() very soon, so I’ll recall its prototype:

Step 2: we need to initialize the FOV visualization game object and its associated mesh. I won’t go extensively into the details of 3D meshes – if you’d like to learn more, I recommend you take a look at some articles, like Unity’s doc on the subject as a starting point. Roughly put, 3D meshes are a collection of 3D points, the vertices, and of relationships between them to define the faces of the mesh. For optimal 3D rendering, faces are triangles – they are defined as a trio of 3 vertex indices. In Unity, you can create a Mesh variable and give it an array of Vector3 for its vertices and an array of integers for its triangles.

Note: you can also pass in data for the UVs and additional rendering info, but I won’t go into this here. Though if you’re keen on learning more about this, I really encourage you to take a look at all the reading material available on the net with books, video courses and various tutorials 😉

If you’re not familiar with 3D modeling and/or don’t want to spend time on this right now, you can simply copy the following snippet of code and skip the next paragraphs.

The mesh we want to create is very basic: it is a square ribbon with each of its angle positioned at the projected main camera screen “corners”. Ignoring the actual length of the sides and the thickness of the ribbon, and just focusing on its actual structure, we can represent the mesh in top-view as follows:

Here are a few things to note:

  • we have 8 vertices: the vertex 0 is in the bottom-left corner, vertex 1 is one unit on the X-axis, vertex 2 is one unit on the Z-axis, etc.
  • we have 8 faces: for example, face A is the one that joins vertices 0, 4 and 1
  • to avoid having weirdly flipped normals, we need to always go clockwise when we pick our vertex indices to define triangles

Our code is going to start with the 8 vertices “un-initialized” (all stacked at the world origin (x, y, z) = (0, 0, 0)), but with all of its 8 faces already set up. We’ll then update the vertices position whenever we zoom in or out. To avoid moving the vertices and recomputing the mesh bounds at each camera translation, we will center the mesh around the world-projected middle of the main camera screen; then, we’ll be able to simply move the indicator object around, and only modify the mesh itself when we zoom.

Here is the corresponding code to add to our CameraManager.cs C# file – take a deep breath, it’s a bit long even though it shouldn’t be that hard to understand after those explanations 😉

Don’t forget to drag the unlit material we prepared for the minimap indicator onto the “Minimap Indicator Material” slot!

We now have a nice indicator for the current main camera field of view on our minimap (I’ve increased the size of the minimap so it’s easier to see, but the final UI should not take up as much on the screen ;)):

Warning: this is not a perfect minimap indicator… in particular, if you look “over a mountain” (meaning that you are on high ground compared to the zone you are pointing in the middle of your screen), you might get a sudden “teleport” effect. It’s not too much of a pain if you don’t plan on having very different altitude levels. But if you’re thinking of having big peaks and ravines you might need to switch gears: instead of adapting the altitude to the ground level, keep a constant altitude and pull the camera back so there is no issue with the far clip plane.

Conclusion

Next week, the final part of this minimap/fog of war tutorials will add a direct camera jump system when we click on the minimap, a smoother transition when we discover new areas and even hide some objects in the scene depending on whether they are in-sight or just in a previously explored area.

Leave a Reply

Your email address will not be published. Required fields are marked *