31 August 2024

How to implement a level map with two level of Fog Of War (Two level FOW) in Godot

Map with two level FOW
In my previous article, I explained how to implement a level map in Godot with fog of war. That map initially appeared black, but it gradually revealed itself as the character moved through it. It was a single-level map because, once the character traversed an area, that area remained fully uncovered, and all enemies passing through it were visible, even if the main character was far away.

Other games implement a two-level FOW, where the area immediately surrounding the character, within their line of sight, is fully uncovered, and enemies passing through it are entirely visible, while in areas left behind, only the static part of the map is visible, but not the enemies that might be there. Generally, maps with two-level FOW display distant areas with a dimmer tone, where only the static part is visible, to distinguish them from areas where enemies might appear.

It turns out that, when you have a map with one level of FOW, implementing a second level is incredibly easy. So I will pick up from where we left off at the end of the previous article. If you haven’t read it yet, it is essential that you do so, using the link at the beginning of this one if you like. To avoid repetition, I will assume that everything discussed in that article is understood.

You can find all the code for this article in its GitHub repository.

Starting from that map, we need to consider that the shader we used for the map (Assets/Shaders/Map.gdshader) checked each pixel of the map’s image to see if that same pixel was colored in the mask map rendered by SubViewportMask. If it wasn’t colored on the mask map, it assumed that the pixel was not visible and painted it black instead of painting it with the color coming from the map’s SubViewport (remember that the map's TextureRect was connected to the image rendered by SubViewportMap). Conceptually, the mask map corresponds to the areas already explored.

In this new map, we want to take it a step further by distinguishing the explored areas (the mask map) from the directly visible areas. The former will be shown in a dimmer tone, displaying only the static part, while the latter will continue to be rendered as before.

What are the directly visible areas? Those are on the shapes map. They are rendered in SubViewportShapes with a camera that only captures the white circle placed on the character. So far, we had used the shapes map for the mask shader (Assets/Shaders/MapMask.gdshader), but now we will also use it in the map shader to know which pixels of the map are in the player’s visible area.

Once we know how to distinguish, within the explored areas, which ones are in the visible zones and which ones are not, we need to render an image that only shows the visible part of the map. As in the other cases, this can be achieved with a SubViewport. In my case, I simply copied SubViewportMap and its child camera. I called the copy SubViewportStatic.

SubViewportStatic

To ensure that this SubViewport only shows the static part, the Cull Mask of its camera needs to be configured to capture only layer 1, where I placed all the static elements of the environment.

Screenshot of the camera inspector

Note that the same camera in SubViewportMap is configured to capture layers 1 and 4 (1 for the static objects in the environment and 4 for the character's identifying icons placed on them).

To make the image captured by the camera dimmer, you need to assign it an Environment resource (in the field with the same name). Once you’ve created this resource, you can click on it to configure it. In the Adjustments section, I enabled it and lowered the default brightness to half.

Environment configuration of the camera

Notice that the Environment resource has many more sections, so imagine the number of things you could do to the image captured by the camera.

With that, we now have a dimmer image of the environment, but without characters. Just the static part we needed. The map shader will receive this image through a Uniform (Static Map Texture), which we’ll configure through the inspector to point to SubViewportStatic.

Map shader inspector


Under the hood, the shader code is very similar to that of the previous article.

New shader code

The main novelty is the new uniform we mentioned earlier (line 12) to receive the image of the static elements map, and lines 16 to 18. If the code reaches this point, it means that line 14 concluded that since the pixel was marked with color in the mask map, it corresponds to an already explored area. In line 16, it checks if that pixel, besides being in an explored area, corresponds to a colored pixel in the shapes map (i.e., the directly visible areas). If not, the pixel receives the color of the equivalent pixel in the static map image (line 17). If the pixel was indeed colored in the shapes map (and its red channel was different from 0), that pixel would receive the default color coming from SubViewportMap (which shows the enemy icons).

The result is the image that opens this article, with the area surrounding the character showing the closest enemies, the more distant explored areas displaying only the static elements of the environment, and the unexplored areas colored in black.

27 August 2024

How to implement a level map with Fog Of War (FOW) in Godot

In games that offer a level map, it’s common to cover the areas that the player has not yet explored. As the player progresses through the terrain, those sections of the map are gradually revealed. This is known as the Fog of War (FOW), and starting from this simple concept, things can get quite complex. A common enhancement is to give the player a limited vision range, so the revealed areas of the map only show other players or NPCs if they are within the player’s vision range. Outside of this range, the map only displays static elements of the environment: buildings, rivers, forests, mountains, etc. Another refinement is to apply the fog of war not only to the map but also to the 3D level environment.

In this article, I will explain the simplest approach. We will uncover the map, and the revealed areas will retain full visibility even if the player moves away from them. Once we understand the basics, we’ll see that it’s not that difficult to apply more sophisticated techniques.

The elements I refer to in this article are available in my DungeonRPG repository. This is the code I developed while following GameDevTV’s course "Godot 4 C# Action Adventure: Build your own 2.5D RPG," which I highly recommend. The course doesn’t cover maps or FOW, but the mini-game it implements provides an excellent foundation for learning how to create them.

Map Creation

I won’t delve into this here because I already explained it in the article on how to create a minimap. Creating a complete map is similar. You just need to ensure that the orthographic camera is positioned at the center of the environment (from above) and has a size (parameter Camera3D > Size) large enough to cover the entire environment. The Subviewport that the Camera3D projects to must cover the entire screen.

You’ll only need a scene with the following structure:

When placing the scene in the environment, you’ll need to position the camera in the appropriate spot. The problem is that you won’t have direct access to it since it’s in its own scene. I solved this by having the root node’s script in the scene use the [Tool] attribute for the main class. This attribute allows the script to execute logic while being manipulated within the editor.

The script placed at the root of the scene is located at Scripts/General/Map.cs. Its source code is in Godot C# (as is all the code I develop), but I don’t think developers who prefer GDScript will have any trouble understanding it. Specifically, being marked with the [Tool] attribute, its _Process() method executes continuously in the editor as long as there’s a node with that script in the hierarchy. The content of that method is as follows:



The Engine.IsEditorHint() method returns true when the calling code is executed from the editor. It’s very useful for defining code that should run while working in the editor but not when running the game.

In this case, two things are done: it looks for a Marker3D to obtain its position, and that position is used to place the Camera3D node of the map.

The Marker3D should be placed as a child of the scene when instantiated in the environment. It’s similar to instantiating a CollisionShape as a child of a CharacterBody3D.


What the GetCameraPositionMarker() method does is check if the scene has any Marker3D as a child. If the user hasn’t configured a Marker3D as a child of the scene, Godot typically shows a warning with a yellow icon.

The decision whether to show that warning is made by the UpdateConfigurationWarnings() method, which is called on line 59 of the last code snippet. This method is built into Godot, and to make its decision, it relies on the information passed in the implementation of the _GetConfigurationWarnings() method, which is an abstract method that classes inheriting from Godot nodes can implement. In my case, I implemented it as follows:

This method is very simple. It returns an array of warning messages. If the array is empty, UpdateConfigurationWarnings() interprets that everything is fine and no warning messages need to be displayed. But if the array contains any strings, it shows the warning icon with the message included in the array.

In my case, I simply check if _cameraPosition is still null (line 87) after the GetNodeOrNull() call on line 58 of GetCameraPositionMarker(). If it turns out to be null (line 87), it indicates that the user hasn’t placed a Marker3D as a child of the scene, so an error message is added to the returned message.

A Marker3D is just a Node3D with a noticeable appearance. It’s great for marking places within the environment that your objects can use as references. The idea in this case is to place the Marker3D at the point in the environment where we want to position the map’s camera (usually the center of the level).

Once you have the Marker3D, the _Process() method calls the UpdateCameraConfiguration() method (from line 80) to configure the camera’s position.

That method updates the configuration of two cameras, the map camera and the visibility zone camera (shapes), which we’ll see shortly. The Marker3D position is used to configure the map camera’s position, while its size and aspect ratio (lines 66 and 67) are configured based on what you’ve set in the inspector via exported fields:

In my case, I created an InputAction so that when the "M" key is pressed, the screen is covered with the map, and it disappears as soon as the key is released:



The rest of the GUI elements subscribe to the MapShown (line 106) and MapHidden (line 110) signals to know when they should hide or reappear.

The Visibility Zones Map

The previous map is the full level map—the one we would offer the player if they could see it from the beginning of the game.

But we’ve decided that the player should not have infinite visibility, but rather be able to see up to a certain distance. This distance is usually modeled as a circle around the player, with the radius being the player’s maximum vision range.

What we’re going to do is hide the map behind a black layer, the fog of war (FOW), and only open holes in the fog where the player’s vision circle passes. To do this, we’ll use the mask concept we used in the minimap article. In that case, we used a circle image to define that the visible part of the minimap was circular. In this case, I used a similar technique by creating a dynamic image with a black background, where anything white defines the visible areas of the map.

To create this dynamic image, I used a similar approach to the character icons in the minimap. I placed a circular sprite above the main character and assigned that sprite to layer 5.


This layer is exclusively for FOW. Neither the game camera nor the minimap camera includes layer 5 in their culling mask, so the circle will be invisible to them. However, the camera I added to its own Subviewport in the FOW map scene does include this layer.


As shown in the figure, that camera only sees the white circles on layer 5, and the rest will be colored black, which is the background color I configured. The result is that the Subviewport will render a black screen with a white circle moving around.

Besides the mentioned layer and background color, it’s important that both the map Subviewport (SubViewPortMap in the figure) and the visibility zones Subviewport (SubViewPortShapes), as well as their respective cameras, have the same configuration so that they have the same scale and cover the same area of the environment, perfectly overlapping.

Configuring a Dynamic Mask

At this point, we have a map and a dynamic image with a white circle that moves with the character. If we wanted to reveal only the area around the player, we would already have most of the elements needed to take the final step. But we want to make things a bit more complex, as we want the character to leave a trail as they move, making the map visible along that trail. Therefore, it is necessary for the vision circle to leave a trace.

This function will be carried out by one more Subviewport (SubViewportMask in the last figure), which will house a fully black ColorRect. The image rendered by this Subviewport will be used to cover the map, acting as the FOW. The peculiarity is that this ColorRect has its own shader:




This shader is located in Assets/Shaders/MapMask.gdshader and is quite simple.


The topic of shaders could fill entire books, but I’ll try to give a very basic introduction. Using Godot’s terminology, this shader "exports" two variables by marking them with the term "uniform": shapesTexture and prev_frame_text. The value of the first one is set through the inspector, and the second is marked with a special attribute (hint_screen_texture), which marks that variable so that Godot automatically assigns it a value with the data from the last image rendered by the Subviewport that is  the parent of the shader node (in this case, SubViewPortMask).

The fragment() method of a shader runs for each pixel on the screen (you know which pixel thanks to the UV and SCREEN_UV variables), and depending on the pixel you’re at, you can modify the color that will eventually be rendered for that pixel using the COLOR variable. By default, the fragment() method runs on a screen with no previous data, so if you want to consider the previous image, you must mark a uniform variable with the attribute (hint_screen_texture) and ensure that the Subviewport of the shader does not clear itself each frame by setting its Clear Mode to Never.


I set the Update Mode to Always so that the calculations we’re about to see are performed even if the map is not visible, ensuring that when the player decides to view it, it is up to date.

If we continue with the MapMask shader, we see that I took the value of the pixel in the visibility zones map (shapesTexture, to which I assigned a reference to SubViewportShapes in the inspector), as well as the value of the pixel in the previous frame (previousColor).

The visibility zones map only has two colors, the black background and the white vision circles, so as soon as the visibility zone pixel is not black, we know it’s a map pixel that should be made visible, so we mark it as white by setting its COLOR to that value (line 10 of the shader). To check if the pixel is different from 0, I simply look at the red channel. Since the circles I used are white, I’m sure the red channel will also be affected as they pass, as that color has components in all three channels. If the visibility zone pixel is black, it means it’s a part of the map that is not within the vision range at this moment, but it could have been in previous moments, so since we want to leave a trail of visible areas, we leave that pixel with the value it had in the previous frame (line 12).

The effect of this shader will be that the character’s vision circle behaves like a brush, drawing white over a black background to mark the character’s trail.

The Final View

Now we need to combine the dynamic mask and the map to display the result somewhere. This will be the role of the TextureRect Map, which I placed as low as possible in the scene hierarchy to ensure it is drawn over all other elements, covering them.

For this TextureRect to read the map’s information, I made its Texture property reference the SubViewportMap. If we did nothing else, this would make the TextureRect faithfully reproduce what SubViewportMap renders.

But we want to incorporate the dynamic mask information, which is why this TextureRect has its own shader, which you can find in Assets/Shaders/MapMask.gdshader.

This shader achieves the desired effect in just a few lines:


In this case, two variables are exported, both configurable through the inspector. In maskTexture, I left a reference to SubViewportMask (the dynamic mask). Meanwhile, in fogColor, I left the color we want to use for areas covered by the FOW.

The shader checks the value of the pixel from the dynamic texture in the same position as the pixel being rendered. If the dynamic mask pixel (maskSample) is black, then I render the final image pixel in black. Here, I check only the red channel again since my masks are white, so I know they have presence in the red channel. If the mask pixel is not black, it means that pixel should be visible, so we don’t do anything and let it render the color from SubViewportMap.

Conclusion

The result is the image at the beginning of this article: a map with FOW that expands its visible areas as the player moves through the environment.

As I mentioned at the beginning of the article, this is the most basic case of FOW. I want to try in a future article the option of a two-level FOW map, where the completely unexplored areas are hidden, and the explored areas only show static elements outside the character’s vision range. This evolution should be straightforward to achieve, but I don’t want to include it in this article to keep it from getting too long.

As for applying FOW to the 3D environment and not just the map, that’s something I’m still trying to understand how to do. As soon as I figure it out, I’ll reflect it in an article here.

I hope you found this interesting.

08 August 2024

How to test multiplayer games in Unity

I am taking the GameDevTV course on multiplayer game development with Unity. When I finish it, I will share my opinion, as I have done with other courses. Until then, I wanted to tell you about a new trick I discovered in Unity: how to create multiple instances of a multiplayer game to test it on one PC.

Having this capability is essential during the development phase to ensure that we synchronize all game elements correctly between different participants. Godot natively includes the ability to run up to 4 independent instances of the game for testing. However, Unity has not had this feature, at least until now.

The course I am taking, for example, limits testing to compiling and running one instance of the game outside the editor (with the "Build and Run" option) and running a second instance within the Unity editor. It doesn’t explain how to test with more than two players, which has not been easy. Searching the internet, I concluded that the closest thing Unity had to Godot’s functionality was a third-party extension called ParrelSync.

The news is that Unity 6 will finally include the feature that Godot users have already enjoyed. Although there is no stable version of Unity 6 yet, you can already test everything in the Preview versions available for installation from Unity Hub.

The functionality is called Multiplayer Play Mode and allows you to simulate up to 4 players: one from the editor and three other virtual instances of the game.

To install it, we need to go to the Package Manager in the Unity 6 editor and install the Multiplayer Play Mode package from the Unity Registry.

To activate a virtual instance of the game, you need to go to Window > Multiplayer Play Mode and select how many instances you want to launch.

Those instances you select will start a boot process, and once they become active, their respective windows will appear. Don’t worry about the long startup time. It only takes that long the first time. After that, the information is cached, and subsequent startups are much faster.

From that moment on, every time we start the game from the editor, it will play in both the editor and the virtual instances, allowing us to simulate the behavior of independent players.

To operate as one of the players, simply select the window of its instance and interact with the game as the player would.

To stop the game, do so from the editor as you would with a single-player game. This will stop the game in all virtual instances.

To make the windows disappear, just deselect them in the Multiplayer Play Mode window.

With this, we have everything we need to test any multiplayer game.

I hope you found this interesting.