Field of view
overview
This utility project implements a dynamic Field of View (FOV) system in Unity 3D, designed to detect and visualize what an entity can “see” within a 3D environment. The system calculates angular vision cones, applies customizable radius and angle parameters, and uses raycasting to detect obstacles and line-of-sight for interactive or AI-driven elements. Perfect for stealth mechanics, visibility checks, or surveillance tools, this modular utility is easy to integrate into larger Unity projects and can be extended for real-time gameplay logic or editor-based simulation.
Year
2024
Genre
Utility
Platform
PC
Look around
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Handles the player's Field of View (FOV), detecting visible targets and rendering the FOV mesh.
/// </summary>
public class FieldOfView : MonoBehaviour
{
[Header("FOV settings")]
[SerializeField] private float viewRadius; // Radius of the FOV circle
[Range(0, 360)]
[SerializeField] private float viewAngle; // Angle of the visible arc
[Header("Player Settings")]
[SerializeField] private Transform playerVisualTransform; // Reference to where the player is "looking" from
[Header("LayerMasks")]
[SerializeField] private LayerMask targetMask; // What can be detected as a target
[SerializeField] private LayerMask obstacleMask; // What can block line of sight
[Header("Mesh Settings")]
[SerializeField] private float meshResolution = 1f; // Number of rays per degree
[SerializeField] private MeshFilter viewMeshFilter; // Mesh filter that displays the FOV cone
[SerializeField] private int edgeResolveIterations = 4; // How many times we try to find accurate edge points
[SerializeField] private float edgeDistanceThreshold = 0.5f;// When to trigger edge smoothing
private Mesh viewMesh; // Mesh that visualizes the FOV
[SerializeField] private List<Transform> visibleTargets = new List<Transform>(); // Targets currently in view
private void Start()
{
// Initialize mesh and assign it to the mesh filter
viewMesh = new Mesh { name = "ViewMesh" };
viewMeshFilter.mesh = viewMesh;
// Start scanning for visible targets periodically
StartCoroutine(FindTargetsWithDelay(0.2f));
}
private void LateUpdate()
{
// Update the FOV mesh every frame after movement
DrawFOV();
}
/// <summary>
/// Repeats FindVisibleTargets every X seconds.
/// </summary>
private IEnumerator FindTargetsWithDelay(float delay)
{
while (true)
{
yield return new WaitForSeconds(delay);
FindVisableTargets();
}
}
/// <summary>
/// Detects all targets inside the view radius and within view angle, without obstacles.
/// </summary>
private void FindVisableTargets()
{
// Unmark previously targeted objects
foreach (Transform oldT in visibleTargets)
{
var oldTarget = oldT.GetComponentInParent<Target>();
if (oldTarget != null)
oldTarget.NotTargeted();
}
visibleTargets.Clear();
// Get all possible targets within the radius
Collider[] targetInViewRadius = Physics.OverlapSphere(playerVisualTransform.position, viewRadius, targetMask);
for (int i = 0; i < targetInViewRadius.Length; i++)
{
Transform target = targetInViewRadius[i].transform;
Vector3 dirToTarget = (target.position - transform.position).normalized;
float angleToTarget = Vector3.Angle(playerVisualTransform.forward, dirToTarget);
// If the target is within the view cone
if (angleToTarget < viewAngle / 2)
{
float distToTarget = Vector3.Distance(playerVisualTransform.position, target.position);
// Check if an obstacle is between player and target
if (!Physics.Raycast(playerVisualTransform.position, dirToTarget, distToTarget, obstacleMask))
{
visibleTargets.Add(target);
var t = target.GetComponentInParent<Target>();
if (t != null)
t.Targeted();
}
}
}
}
/// <summary>
/// Returns a direction vector from an angle, relative to player's current forward.
/// </summary>
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{
if (!angleIsGlobal)
{
angleInDegrees += playerVisualTransform.eulerAngles.y;
}
return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}
/// <summary>
/// Creates and updates the FOV mesh based on raycasting outward.
/// </summary>
private void DrawFOV()
{
int rayCount = Mathf.RoundToInt(viewAngle * meshResolution);
float rayAngleSize = viewAngle / rayCount;
List<Vector3> viewPoints = new List<Vector3>();
ViewCastInfo oldViewCast = new ViewCastInfo();
for (int i = 0; i <= rayCount; i++)
{
float angle = playerVisualTransform.eulerAngles.y - viewAngle / 2 + rayAngleSize * i;
ViewCastInfo newViewCast = ViewCast(angle);
if (i > 0)
{
bool thresholdExceeded = Mathf.Abs(oldViewCast.distance - newViewCast.distance) > edgeDistanceThreshold;
// Smooth out jagged edges between rays hitting/not hitting
if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && thresholdExceeded))
{
EdgeInfo edge = FindEdge(oldViewCast, newViewCast);
if (edge.pointA != Vector3.zero) viewPoints.Add(edge.pointA);
if (edge.pointB != Vector3.zero) viewPoints.Add(edge.pointB);
}
}
viewPoints.Add(newViewCast.point);
oldViewCast = newViewCast;
}
// Build the mesh vertices and triangles
int vertexCount = viewPoints.Count + 1;
Vector3[] vertices = new Vector3[vertexCount];
int[] triangles = new int[(vertexCount - 2) * 3];
vertices[0] = Vector3.zero;
for (int i = 0; i < vertexCount - 1; i++)
{
vertices[i + 1] = playerVisualTransform.InverseTransformPoint(viewPoints[i]);
if (i < vertexCount - 2)
{
triangles[i * 3] = 0;
triangles[i * 3 + 1] = i + 1;
triangles[i * 3 + 2] = i + 2;
}
}
viewMesh.Clear();
viewMesh.vertices = vertices;
viewMesh.triangles = triangles;
viewMesh.RecalculateNormals();
}
/// <summary>
/// Finds the midpoint edge between two view rays for better mesh accuracy.
/// </summary>
private EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast)
{
float minAngle = minViewCast.angle;
float maxAngle = maxViewCast.angle;
Vector3 minPoint = Vector3.zero;
Vector3 maxPoint = Vector3.zero;
for (int i = 0; i < edgeResolveIterations; i++)
{
float angle = (minAngle + maxAngle) / 2;
ViewCastInfo newViewCast = ViewCast(angle);
bool thresholdExceeded = Mathf.Abs(minViewCast.distance - newViewCast.distance) > edgeDistanceThreshold;
if (newViewCast.hit == minViewCast.hit && !thresholdExceeded)
{
minAngle = angle;
minPoint = newViewCast.point;
}
else
{
maxAngle = angle;
maxPoint = newViewCast.point;
}
}
return new EdgeInfo(minPoint, maxPoint);
}
/// <summary>
/// Casts a ray outward at the given angle and returns hit info.
/// </summary>
private ViewCastInfo ViewCast(float globalAngle)
{
Vector3 dir = DirFromAngle(globalAngle, true);
RaycastHit hit;
if (Physics.Raycast(playerVisualTransform.position, dir, out hit, viewRadius, obstacleMask))
{
return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
}
else
{
return new ViewCastInfo(false, playerVisualTransform.position + dir * viewRadius, viewRadius, globalAngle);
}
}
// Getters for external access
public List<Transform> GetVisibleTargets() => visibleTargets;
public float GetFOVRadius() => viewRadius;
public float GetFOVAngle() => viewAngle;
/// <summary>
/// Struct representing a single raycast hit for the FOV.
/// </summary>
public struct ViewCastInfo
{
public bool hit;
public Vector3 point;
public float distance;
public float angle;
public ViewCastInfo(bool _hit, Vector3 _point, float _distance, float _angle)
{
hit = _hit;
point = _point;
distance = _distance;
angle = _angle;
}
}
/// <summary>
/// Struct representing an interpolated edge between two rays for mesh smoothing.
/// </summary>
public struct EdgeInfo
{
public Vector3 pointA;
public Vector3 pointB;
public EdgeInfo(Vector3 _pointA, Vector3 _pointB)
{
pointA = _pointA;
pointB = _pointB;
}
}
}
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(FieldOfView))]
/// <summary>
/// Custom Editor script for visualizing the Field of View (FOV) in the Unity Editor Scene view.
/// </summary>
public class FOVEditor : Editor
{
/// <summary>
/// Draws the FOV visualization in the Scene view.
/// </summary>
private void OnSceneGUI()
{
FieldOfView fov = (FieldOfView)target;
Handles.color = Color.white;
Handles.DrawWireArc(fov.transform.position, Vector3.up, Vector3.forward, 360, fov.GetFOVRadius());
Vector3 viewAngleA = fov.DirFromAngle(-fov.GetFOVAngle() / 2, false);
Vector3 viewAngleB = fov.DirFromAngle(fov.GetFOVAngle() / 2, false);
Handles.DrawLine(fov.transform.position, fov.transform.position + viewAngleA * fov.GetFOVRadius());
Handles.DrawLine(fov.transform.position, fov.transform.position + viewAngleB * fov.GetFOVRadius());
// Draws lines to visible targets
Handles.color = Color.red;
foreach (Transform visibleTargets in fov.GetVisibleTargets())
{
Handles.DrawLine(fov.transform.position, visibleTargets.position);
}
}
}
Used assets