When developing my casual roguelike, Wild West Pigeon, I’ve encountered a need to close parts of the map with fog of war. My game is turn-based, meaning I need to recalculate fog of war once every turn and not dynamically.
A lot of solutions recommend shaders, but I could not find any articles if I want other, albeit less prettier, solution (You might need this, for example, because not all Android phones prior to Android 4.0 support shaders – check this stack overflow reply). Therefore, I’ll try to share my knowledge on how I addressed the issue.
Fog of War Tile GameObject
- There are lots of way to do fog of war sprite, but I made a simple black square (1×1). If you want your fog of war smoother and prettier – make a larger square with blurred sides.
- Add it as an asset to your game project.
- Create an empty object
- Add a sprite renderer to it – set the sprite as your “Fog of War” sprite
- Make a new sorting layer (call it “Fog of War” or something like that) – make it to be the last layer drawn (In my case, the order is following: Default -> Floor -> Floor_Destructibles -> Items -> Units -> Fog Of War) so that fog of war sprites would be drawn above all else.
- Move the object to prefab.
- Create an empty GameObject in your scene, name it “FOVPanel” or something like that, this will aggregate your ‘fog of war’ tiles.
Logics
What we need to do now – is to look at every player turn and check what tiles are revealed and need hiding. For this purpose – fog of war is regenerated after every move. Whenever new Fog of War tiles are created – their parent is assigned to FOVPanel object and on the next turn all of FOVPanel object’s current children are deleted and replaced by a newly-calculated and instantiated FOV tiles. Each game will be different, but in my game, when the level is generated, I have an array boolean revealedTiles[][]
that takes the width and height of my map whenever new level is initialized.
I’ll try to describe the logics behind it – but I’ll show you my code as well in case you are interested.
- You can attach your new script to “FOVPanel” object or whichever object you want, just make sure your player object will be able to access it. I’ve attacked it to my game manager (singleton static persistent object).
- When level loads, mark all tiles in revealedTiles as false
- Reveal some tiles around your player, i.e. make every tile around him visible (8 tiles around the player, 1 tile where player stands)
- After player moves – call recalculation function, it makes sure that the tile the player moved to is set as true in revealedTiles, then calls the recursive calls to simple ‘raycasting’ function that checks which tiles are open (note – I call it ‘Raycasting’, not sure how it’s called ‘scientifically’ – I just imagine that we are sending rays of light towards direction where player looks)
- The raycasting function goes forward, checks every tile row and sees if that’s an obstacle. If that’s an obstacle – reveal the tile, but don’t go forward. If there’s no obstacle – reveal the tile and go to next row.
- The ray expands gradually: i.e. at first we reveal one tile in front of our hero, on the next tile column we reveal up to 3 tiles, on the next tile column we reveal up to 5 tiles, etc.
- At first – imagine the simple ray that goes forward – you just reveal all tiles in front of it until you encounter an obstacle.
- After that – try to improve it by making a corner-case (pun intended) so that leftmost and rightmost revealed tile would go deeper one level and make sure that the next column is revealed a bit more. See image below. The hero (‘H’) expands his view depending on the distance of the tiles.
Full Fog of War module code here:
using UnityEngine; using System.Collections; using System.Collections.Generic; public class FOV : MonoBehaviour { [HideInInspector] public GameObject fovPanel; public GameObject fovTile; //That's your previously created Fog of War tile - assign in editor! void Awake() { fovPanel = GameObject.Find("FOVPanel"); } // max revealed tile depth (from start) public const int MAX_RANGE = 5; /* level - how far we've already gone with our raycasting x - current tile x position y - current tile y position directionX - make sure it's -1, 0 or 1 (0 if directionY is not 0) directionY - make sure it's -1, 0 or 1 (0 if directionX is not 0) */ public void RayCasting(int level, int x, int y, int directionX, int directionY, bool leftCorner, bool rightCorner) { // that's the board script: it handles all the board logics BoardManager board = GameManager.instance.boardScript; // continue only if we have not surpassed the max distance if (level < MAX_RANGE) { // if we are out of bounds if (x < 0 || y < 0 || x >= board.maxX || y >= board.maxY) { return; } else { // ok, we're here, mark the tile as revealed board.revealedGrid[x, y] = true; // but does the tile blocks visibility? if visibilityGrid[x, y] is true, then player can see through the tile // what's behind it if (board.visibilityGrid[x, y]) { // our ray expands gradually. therefore if we are located on the corner tiles // of the current ray level - we need to expand further on the next level if (leftCorner) { if (directionX != 0) { // if we are moving horizontally, expand ray below or above (that's why we adjust y by directionX) // the left corner tile ray would go something like that // level // 0 1 2 // . . * // . * * // * * . RayCasting(level + 1, x + directionX, y + directionX, directionX, directionY, leftCorner, false); } else { // else RayCasting(level + 1, x + directionY, y + directionY, directionX, directionY, leftCorner, false); } } // same as with left corner if (rightCorner) { if (directionX != 0) { RayCasting(level + 1, x + directionX, y - directionX, directionX, directionY, rightCorner, false); } else { RayCasting(level + 1, x - directionY, y + directionY, directionX, directionY, rightCorner, false); } } // we raycast forward in any case // level // 0 1 2 // . . . // * * * // . . . RayCasting(level + 1, x + directionX, y + directionY, directionX, directionY, leftCorner, false); } } } else { return; } } public void Recalculate(int x, int y) { BoardManager board = GameManager.instance.boardScript; // the player moves here board.revealedGrid[x, y] = true; // raycast to all directions RayCasting(0, x, y, 0, 1, true, true); RayCasting(0, x, y, 0, -1, true, true); RayCasting(0, x, y, 1, 0, true, true); RayCasting(0, x, y, -1, 0, true, true); } protected const int MAX_X_LOOKUP = 20; protected const int MAX_Y_LOOKUP = 10; // redraw based on position of camera // (currentX and currentY most often is the center of the screen, where player stands) public void Redraw(int currentX, int currentY) { // redraw the Fog of War tiles List<GameObject> children = new List<GameObject>(); // first we gather all children of FOVPanel foreach (Transform child in fovPanel.transform) { children.Add(child.gameObject); } // then we delete them foreach (GameObject child in children) { Destroy(child); } BoardManager board = GameManager.instance.boardScript; // then we draw new Fog of War tiles for (int x = -MAX_X_LOOKUP + currentX; x < MAX_X_LOOKUP + currentX + 1; x++) { for (int y = -MAX_Y_LOOKUP + currentY; y < MAX_Y_LOOKUP + currentY; y++) { if (x < 0 || y < 0 || x > board.maxX - 1 || y > board.maxY - 1) { continue; } if (board.revealedGrid[x, y] == false) { Vector3 pos = new Vector2(x, y); GameObject obj = Instantiate(fovTile, pos, Quaternion.identity) as GameObject; obj.transform.parent = fovPanel.transform; } } } } }
Let me know if you see any mistakes or know how to do it better! If you want to see how the final result works, you can check out my game on Google Play market: Wild West Pigeon.