Fog of War in Flutter Game

Search for a command to run...

thanks! Glad you liked it :)
This is the series related to my experience building game with Flutter and Dart. It is basically a diary. At first I implement feature and then add a post about it. https://locadederta.com/sloboda
In my 4th game in the Loca Deserta Game Universe I am using InteractiveViewer widget in order to implement a map. It allows me to pan, zoom in/out, basically everything you expect from the map in a game: When the game starts I want to animate fly-ov...
Build. Craft. Explore.

Заселяємо південь України!

Hey! We just published the second content update for our game Sloboda 2. This update is free for anyone who already purchased our game. If you want to purchase it, then pick your platforms: Apple App Store: https://apps.apple.com/us/app/loca-deserta-...

Привіт, компана! Ось і настав час Січневого оновлення для нашої гри Слобода 2! Це оновлення безкоштовне для всіх, хто придбав гру. Якщо ж ще не придбали, то можете це зробити по цих посиланнях: Apple App Store: https://apps.apple.com/ua/app/loca-dese...

Статистика запуску гри Трохи більше двох тижнів тому ми випустили гру на всіх магазинах. І одразу ж попали в Топ платних ігор 🇺🇦українського App Store! Купити можна на iOS по посиланню: https://apps.apple.com/ua/app/loca-deserta-sloboda-2/id164250...

In this article I will show how to use CustomClipper widget in order to implement Fog of War for the map.
The next version of my city building game will include a map. On it you have to capture different houses, spots, resources. But the game limits your vision by applying Fog of War.
It looks like this:

Only small part of the map is interactive. You cannot tap on a 'grey' area of the map as the player has not yet opened it.
The map without the fog of war is a Stack widget that has houses. All these houses are interactive: player can select them and do some actions:

If we apply Fog of War, then each occupied house should clear fog of war in a given radius AND widgets inside this hole must be tappable!

First naive implementation is to use a Stack widget. The first child is a interactive map, then next children are Containers with white color as a background.
Stack applies following logic: if two widgets overlap then the last child specified in children list is rendered on top of all the previous. Once we know it we can write something like this:
Stack(
children: [
buildMap(),
generateGrid(),
]
)
generateGrid creates a Container with background transparent color:

The 'holes' in the grid are quite simple: if the cell of the grid touches with at least one corner the opened spot then it should not be drawn. In this way we just don't render cells:

The formula used to determine whether the corner of a cell is inside the given circle is a Pythagoras Theorem :)
The map looks like this:

To make it more antialiased we can reduce the size of the cell but this will lead to even more cells to be drawn.
Pros of this method:
Cons:
Actually the performance was so bad that even my Snapdragon 865 on Galaxy Tab S7 could not handle scrolling on that map. Say you wanted to have 100 cells of fog of war per row. This leads to total number 100*100 = 10000 widgets...
We can also use ColorFiltered widget in order to make some parts of the Fog of War widget invisible but it does not satisfy our need: the player should be able to tap on the controls located under the hole. With ColoredFilter it is not possible as the fog of war widget still occupies the hole and the events don't go through (IgnorePointer widget will also not help here). More about this you can read at this StackOverflow question: Create Widget with hole inside .
The best way is to use CustomClipper widget. It allows to draw the shape of the widget of any form, even with REAL holes inside itself!
The initial solution was taken and adjusted from this article: How to cut a hole in an overlay. You can read this article in order to understand how CustomClipper works. TLDR: it does not render itself in areas where two shapes overlap (this leads us to a bad issue which is solved at the bottom :) ).
Here is an adjusted solution for game's needs.

The CustomClipper accepts list of Spots on the map. Then draws ovals at the spot locations and creates holes via
path.fillType = PathFillType.evenOdd;
But what happens if two circles overlap each other? Then the evenOdd logic will not work as we now have three shapes overlapping each other. This leads to such funny issues:

The solution to the bug is to use a little bit different approach. Instead of relying on evenOdd fill type it is better to have two shapes:
Then we combine two paths with the difference algorithm:

And now the game renders holes in the fog of war correctly:
