Stealth Game Field of View Issue

Hi everyone, I’m trying to make a stealth game that utilizes visualized sightcones. I used this fantastic tutorial here to get the sightcone to work and it works like a charm. But there are some instances where my enemies need to look up and down, instead of just left and right. I tried to change the rotation value in the inspector of the mesh but it wouldn’t move. The mesh only responds to rotation along the Y axis (left and right). Here’s the code for the drawn field of view:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class FieldOfView : MonoBehaviour
{

    public float viewRadius;
    [Range(0, 360)]
    public float viewAngle;

    public LayerMask targetMask;
    public LayerMask obstacleMask;

    //[HideInInspector]
    public List<Transform> visibleTargets = new List<Transform>();

    public float meshResolution;
    public int edgeResolveIterations;
    public float edgeDstThreshold;

    public float maskCutawayDst = .1f;

    public MeshFilter viewMeshFilter;
    Mesh viewMesh;

    void Start()
    {
        viewMesh = new Mesh();
        viewMesh.name = "View Mesh";
        viewMeshFilter.mesh = viewMesh;
    }

    IEnumerator FindTargetsWithDelay(float delay)
    {
        while (true)
        {
            yield return new WaitForSeconds(delay);
        }
    }

    public void Update()
    {

    }

    void LateUpdate()
    {
        DrawFieldOfView();
    }

    void DrawFieldOfView()
    {
        int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
        float stepAngleSize = viewAngle / stepCount;
        List<Vector3> viewPoints = new List<Vector3>();
        ViewCastInfo oldViewCast = new ViewCastInfo();
        for (int i = 0; i <= stepCount; i++)
        {
            float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
            ViewCastInfo newViewCast = ViewCast(angle);

            if (i > 0)
            {
                bool edgeDstThresholdExceeded = Mathf.Abs(oldViewCast.dst - newViewCast.dst) > edgeDstThreshold;
                if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDstThresholdExceeded))
                {
                    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;
        }

        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] = transform.InverseTransformPoint(viewPoints[i]) + Vector3.forward * maskCutawayDst;

            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();
    }


    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 edgeDstThresholdExceeded = Mathf.Abs(minViewCast.dst - newViewCast.dst) > edgeDstThreshold;
            if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceeded)
            {
                minAngle = angle;
                minPoint = newViewCast.point;
            }
            else
            {
                maxAngle = angle;
                maxPoint = newViewCast.point;
            }
        }

        return new EdgeInfo(minPoint, maxPoint);
    }


    ViewCastInfo ViewCast(float globalAngle)
    {
        Vector3 dir = DirFromAngle(globalAngle, true);
        RaycastHit hit;

        if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask))
        {
            return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
        }
        else
        {
            return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);
        }
    }

    public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
    {
        if (!angleIsGlobal)
        {
            angleInDegrees += transform.eulerAngles.y;
        }
        return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
    }

    public struct ViewCastInfo
    {
        public bool hit;
        public Vector3 point;
        public float dst;
        public float angle;

        public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle)
        {
            hit = _hit;
            point = _point;
            dst = _dst;
            angle = _angle;
        }
    }

    public struct EdgeInfo
    {
        public Vector3 pointA;
        public Vector3 pointB;

        public EdgeInfo(Vector3 _pointA, Vector3 _pointB)
        {
            pointA = _pointA;
            pointB = _pointB;
        }
    }

}

Any help here would be greatly appreciated. Thanks!

Hey guys,

I’ve been doing a lot of tests and I still haven’t fixed the issue, but I have created a test scene to better explain the issue. In the test scene attached, you’ll notice that changing the X rotation of the Capsule will not change the rotation of the field of view.

You can see in FOV-0-degrees.png that at 0 X rotation, the FOV points straight. You can see in FOV-90-degrees.png that at 90 degrees at the X rotation, the FOV still points straight. I’d like toe FOV be able to rotate as well.

I realize that whatever the script is attached to will never be able to rotate and properly change the FOV. I’m truly stumped, but I feel like the answer is right in front of me. Thanks again everyone.

3401618–267779–FieldOfView-Example.unitypackage (4.59 KB)


Could you post EnemyFieldOfView again, this time with indentation? Really hard to read!

Sorry! It has been updated.

The author of the tutorial takes an approach that seems pretty common - think about angles, use angles, and transform those into directions when you need to.

So, to find raycast increments, he takes angle increments, and then turns them into directions pretty deep in the algorithm - in the DirFromAngle function.

I prefer to work with directions, and then rotate those a certain angle. In this case, I want the forward direction rotated around the local Y-axis.

Here’s the important change:

//old version, take increments of angles:
for (int i = 0; i <= stepCount; i++)
{
    float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;
    ViewCastInfo newViewCast = ViewCast(angle);
    ...

//new version, take increments of directions:
for (int i = 0; i <= stepCount; i++)
{
    float stepAngle = -(viewAngle / 2) + (stepAngleSize * i); // same angle increment
    var stepRotation = Quaternion.Euler(0f, stepAngle, 0f); // turn it into a rotation to rotate the direction around
    var globalDirection = stepRotation * Vector3.forward; // rotate the global forward direction by the step's rotation
    Vector3 stepRayDirection = transform.TransformDirection(globalDirection); // turn that relative, to respect x- and z- axis rotation

    ViewCastInfo newViewCast = ViewCast(stepRayDirection); //Use the direction directly in the ray

The code’s a bit longer, but in return the whole DirFromAngle method is removed. An additional bonus with that is that all instances of Sine and Cosine are gone from the function - I really never feel comfortable working with those outside of tweening :stuck_out_tongue:

Here’s the whole re-written code. It’s a bit less chopped up than the above part, I added a bunch of comments for explanation up there. I’ve taken out some unused methods and variables, too.

Code

using UnityEngine;
using System.Collections.Generic;

public class FieldOfView : MonoBehaviour
{

    public float viewRadius;
    [Range(0, 360)]
    public float viewAngle;

    public LayerMask obstacleMask;

    [Range(0.01f, 2f)]
    public float meshResolution;
    public int edgeResolveIterations;
    public float edgeDstThreshold;

    public float maskCutawayDst = .1f;

    public MeshFilter viewMeshFilter;
    public MeshCollider viewMeshCollider;
    private Mesh viewMesh;

    void Start()
    {
        viewMesh = new Mesh();
        viewMesh.name = "View Mesh";
        viewMeshFilter.sharedMesh = viewMesh;
        viewMeshCollider.sharedMesh = viewMesh;
    }

    void LateUpdate()
    {
        DrawFieldOfView();
    }

    void DrawFieldOfView()
    {
        int stepCount = Mathf.RoundToInt(viewAngle * meshResolution);
        float stepAngleSize = viewAngle / stepCount;
        List<Vector3> viewPoints = new List<Vector3>();
        ViewCastInfo oldViewCast = new ViewCastInfo();
        for (int i = 0; i <= stepCount; i++)
        {
            float stepAngle = -(viewAngle / 2) + (stepAngleSize * i);
            var stepRotation = Quaternion.Euler(0f, stepAngle, 0f);
            var globalDirection = stepRotation * Vector3.forward;
            Vector3 stepRayDirection = transform.TransformDirection(globalDirection);
           
            ViewCastInfo newViewCast = ViewCast(stepRayDirection);

            if (i > 0)
            {
                bool edgeDstThresholdExceeded = Mathf.Abs(oldViewCast.distance - newViewCast.distance) > edgeDstThreshold;
                if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDstThresholdExceeded))
                {
                    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;
        }

        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] = transform.InverseTransformPoint(viewPoints[i]) + Vector3.forward * maskCutawayDst;

            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();
    }


    EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast) {
        Vector3 minDir = minViewCast.rayDir;
        Vector3 maxDir = maxViewCast.rayDir;
        Vector3 minPoint = Vector3.zero;
        Vector3 maxPoint = Vector3.zero;

        for (int i = 0; i < edgeResolveIterations; i++)
        {
            Vector3 dir = (minDir + maxDir) / 2;
            ViewCastInfo newViewCast = ViewCast(dir);

            bool edgeDstThresholdExceeded = Mathf.Abs(minViewCast.distance - newViewCast.distance) > edgeDstThreshold;
            if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceeded)
            {
                minDir = dir;
                minPoint = newViewCast.point;
            }
            else
            {
                maxDir = dir;
                maxPoint = newViewCast.point;
            }
        }

        return new EdgeInfo(minPoint, maxPoint);
    }


    ViewCastInfo ViewCast(Vector3 dir)
    {
        RaycastHit hit;

        if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask))
        {
            return new ViewCastInfo(true, hit.point, hit.distance, dir);
        }
        else
        {
            return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, dir);
        }
    }

    public struct ViewCastInfo
    {
        public bool hit;
        public Vector3 point;
        public float distance;
        public Vector3 rayDir;

        public ViewCastInfo(bool _hit, Vector3 _point, float _distance, Vector3 _rayDir)
        {
            hit = _hit;
            point = _point;
            distance = _distance;
            rayDir = _rayDir;
        }
    }

    public struct EdgeInfo
    {
        public Vector3 pointA;
        public Vector3 pointB;

        public EdgeInfo(Vector3 _pointA, Vector3 _pointB)
        {
            pointA = _pointA;
            pointB = _pointB;
        }
    }
}

Now, you could probably have stuck with the author’s original approach and made it work. But then you’ll have to get help from someone else - I don’t have the patience for figuring out how to preserve your x-rotation in a sin/cos-based setup.

1 Like

Thank you @Baste , it works like a charm! It now functions exactly how I would like it to. I hope this will be useful to others that have the same issue I did. Thanks again!