Daily free asset available! Did you claim yours today?

Crafting a Raycasting Field of View (FOV) System in Unity for Roguelikes

Posted by Gemma Ellison
./
July 16, 2025

In the shadowy depths of roguelike games, vision is power. Forget clunky, pre-baked visibility solutions. We’re diving headfirst into crafting a finely-tuned Field of View (FOV) system using raycasting in Unity. This isn’t just about seeing what’s around the corner; it’s about tactical awareness, strategic decision-making, and ultimately, survival. This is about building an FOV system that feels right.

Setting Up the Stage: Unity Environment Prep

First, let’s establish our battlefield. Create a new 2D Unity project. Import the Tilemap package via the Package Manager. This provides the foundation for our grid-based world.

Create a new Tilemap in your scene. This will be the stage upon which our roguelike drama unfolds. Populate the Tilemap with tiles representing walls and floors. Use different tiles for clear differentiation. Walls will block vision; floors will allow it.

Next, create a Player GameObject. Attach a script called PlayerFOV to it. This script will house our raycasting logic. Position this GameObject within your Tilemap.

Raycasting: The Core of Our Vision

Raycasting is the heart of our FOV system. It works by sending out “rays” from the player’s position. These rays travel in various directions. If a ray hits a wall, it stops. The space the ray travels through is considered within the player’s field of view.

Here’s the basic raycasting logic in C#:

using UnityEngine;
using UnityEngine.Tilemaps;

public class PlayerFOV : MonoBehaviour
{
    public int viewDistance = 10;
    public int numberOfRays = 360;
    public Tilemap visibilityMap;
    public TileBase visibleTile;
    public TileBase invisibleTile;

    private Vector3Int playerCell;

    void Update()
    {
        UpdateFOV();
    }

    void UpdateFOV()
    {
        playerCell = visibilityMap.WorldToCell(transform.position);

        // Set all tiles to invisible initially
        BoundsInt bounds = visibilityMap.cellBounds;
        foreach (var position in bounds.allPositionsWithin)
        {
            TileBase tile = visibilityMap.GetTile(position);
            if (tile != null)
            {
                 visibilityMap.SetTileFlags(position, TileFlags.None); // Allow changing tile
                 visibilityMap.SetTile(position, invisibleTile);
            }
        }

        for (int i = 0; i < numberOfRays; i++)
        {
            float angle = i * (360f / numberOfRays);
            Vector2 direction = new Vector2(Mathf.Cos(angle * Mathf.Deg2Rad), Mathf.Sin(angle * Mathf.Deg2Rad));
            RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, viewDistance);

            if (hit.collider != null)
            {
                //Debug.DrawRay(transform.position, direction * hit.distance, Color.red);
                Vector3Int hitCell = visibilityMap.WorldToCell(hit.point);
                DrawLine(playerCell, hitCell);
            } else {
                Vector3 rayEnd = transform.position + (Vector3)(direction * viewDistance);
                Vector3Int endCell = visibilityMap.WorldToCell(rayEnd);
                DrawLine(playerCell, endCell);
            }
        }
    }

    void DrawLine(Vector3Int start, Vector3Int end)
    {
        int x0 = start.x;
        int y0 = start.y;
        int x1 = end.x;
        int y1 = end.y;

        int dx = Mathf.Abs(x1 - x0);
        int dy = Mathf.Abs(y1 - y0);

        int sx = (x0 < x1) ? 1 : -1;
        int sy = (y0 < y1) ? 1 : -1;

        int err = dx - dy;

        while (true)
        {
            visibilityMap.SetTileFlags(new Vector3Int(x0, y0, 0), TileFlags.None); // Allow changing tile
            visibilityMap.SetTile(new Vector3Int(x0, y0, 0), visibleTile);

            if ((x0 == x1) && (y0 == y1)) break;
            int e2 = 2 * err;
            if (e2 > -dy)
            {
                err -= dy;
                x0 += sx;
            }
            if (e2 < dx)
            {
                err += dx;
                y0 += sy;
            }
        }
    }
}

Explanation:

  • viewDistance: Determines how far the player can see.
  • numberOfRays: Controls the resolution of the FOV. More rays mean a smoother, more accurate FOV, but also higher computational cost.
  • visibilityMap: A separate Tilemap dedicated to displaying the FOV.
  • visibleTile: The tile used to represent visible areas.
  • invisibleTile: The tile used to represent invisible areas.

Visualizing the Vision: Tilemap Manipulation

The UpdateFOV function iterates through a set number of angles. It sends out a raycast in each direction. If the ray hits a collider (a wall), it marks the tiles along the ray’s path as visible. Otherwise it calculates the endpoint of the ray and marks the tiles as visible. The Bresenham line algorithm is used to efficiently draw a line between two points on a grid.

To see this in action, create two new Tile Palettes. Name one “Visible” and the other "Invisible". Create a simple, bright tile in the “Visible” palette. Create a dark or black tile in the “Invisible” palette. Create new Tilemap called "Visibility". Assign the visible and invisible tiles and the Visibility Tilemap in the Inspector of the Player GameObject.

Common Pitfalls and How to Dodge Them

One major performance killer is casting too many rays. Start with a lower numberOfRays value (e.g., 180) and increase it gradually, monitoring performance. Implement a tile caching system. Store the visibility status of each tile and only update tiles that have changed. This can drastically reduce the number of tilemap updates.

Another challenge is dealing with tilemap colliders. Ensure your wall tiles have colliders attached. If your floor tiles also have colliders, Physics2D.Raycast may return unexpected results, therefore they should not have colliders.

Level Up: Optimization and Refinements

Explore different raycasting methods for optimization. Consider using Physics2D.OverlapCircle to detect obstacles within a radius. For a smoother FOV, implement anti-aliasing techniques. This could involve blending visible and invisible tiles. You could also look into precomputing the FOV for static environments. Store this data and only recalculate when the player moves.

Putting It All Together: A Real-World Scenario

Imagine a dungeon crawler. The player navigates dark corridors. An enemy lurks around the corner. The FOV system dynamically reveals the enemy just as the player sees them. This creates tension and excitement. The player can use the limited vision to strategically plan their next move. Or maybe, trigger a trap!

Conclusion: Beyond Simple Sight

Building a raycasting-based FOV system in Unity is more than just making things visible. It’s about crafting a dynamic and engaging gameplay experience. By understanding the core principles of raycasting, tilemap manipulation, and optimization, you can create a truly immersive world where vision is a valuable resource. Take these techniques and build upon them. Transform your roguelike games into captivating adventures!