Covert Cop

Unity

Project Overview

  • Project Type: Unity 2D Game
  • Team Size: 4
  • Role: Game Programmer
  • Software: Unity & VS Code
  • Languages: C#
  • Download link: Play Game
  • Programming Reel: Watch

Project Brief

Covert Cop is a 2D side-scrolling stealth platformer game where you play as an undercover agent trying to expose an evil organization by exploring their compound and hack into their systems without being seen.
Being the only programmer in the team I took charge of translating design specs into a functional game. My tasks and achievements include:


Responsibilities/Achievements

  • - Simulate enemies' field of view by dynamically create meshes to mimic where they could spot the player
  • - Develop tools that allow the design team to create levels and environmental hazards through the game
  • - Program player and enemies movement mechanics
  • - Code user interfaces for the menus and heads-up display
  • - Implement camera behaviour, music and SFX
  • - Implemented 2D animations for the player and enemies using Unity's animation state machine

Code Samples


/*
* Carlos Adan Cortes De la Fuente
* All rights reserved. Copyright (c)
* Email: dev@carlosadan.com
*/

public class FieldOfView : MonoBehaviour 
{

    #region Global Variables

    private IEnumerator currentState;
    private LayerMask playerMask;
    private LayerMask obstaclesMask;

    [SerializeField]
    private bool debugMode = true;
    [SerializeField]
    private float viewRadius;
    [SerializeField]
    [Range(0, 360)]
    private float viewAngle;
    [SerializeField]
    private float meshResolution = 0.5f;

    // Used a mesh to join all the points in space that form the FOV
    private struct MeshRaycastInfo 
    {
        public bool hit;
        public Vector3 point;
        public float distance;
        public float angle;

        public MeshRaycastInfo(bool bHit, Vector3 vPoint, float fDist, float fAngle)
        {
            hit = bHit;
            point = vPoint;
            distance = fDist;
            angle = fAngle;
        }
    }
    private MeshFilter meshFilter;
    private Mesh mesh;
    private List<Vector3> pointList;

    #endregion

    #region Unity Functions

    void Awake () 
    {
        playerMask = LayerMask.GetMask("Player");
        obstaclesMask = LayerMask.GetMask("Default");

        // Init Mesh
        pointList = new List<Vector3>();
        mesh = new Mesh();
        mesh.name = "Field Mesh";
        meshFilter = GetComponent<MeshFilter>();
        meshFilter.mesh = mesh;

        SetState(StateFindPlayer(0.1f));
    }

    void LateUpdate() 
    {
        DrawFieldOfView();
    }

    // Debug code for designers
    void OnDrawGizmos()
    {
        if(debugMode)
        {
            Gizmos.color = Color.green;
            Gizmos.DrawWireSphere(transform.position, viewRadius);
            Gizmos.color = Color.blue;
            Gizmos.DrawRay(transform.position, DirectionFromAngle(transform.eulerAngles.y));
            Gizmos.color = Color.cyan;
            Gizmos.DrawRay(transform.position, DirectionFromAngle(-viewAngle / 2));
            Gizmos.DrawRay(transform.position, DirectionFromAngle(viewAngle / 2));
        }
    }

    #endregion

    #region Class Methods

    private Vector3 DirectionFromAngle (float angle)
    {
        angle -= transform.eulerAngles.z; 	// Adapts the rotation angle to the z rotation of the game object
        Vector3 direction = new Vector3(Mathf.Sin(angle * Mathf.Deg2Rad), Mathf.Cos(angle * Mathf.Deg2Rad), 0);
        return direction * viewRadius;		// Adds the magnitude of the vector depending on the radius
    }

    private void FindPlayer()
    {
        Collider2D playerCollider = Physics2D.OverlapCircle(transform.position, Mathf.Abs(viewRadius), playerMask);
        // Player is inside the radius
        if(playerCollider != null)
        {
            // Get The player collider (he uses a box collider)
            Vector3 colliderPos = playerCollider.bounds.center;
            Vector3 extents = playerCollider.bounds.extents;
            Vector3[] bounds = {
                new Vector3(colliderPos.x + extents.x, colliderPos.y + extents.y, 0f),
                new Vector3(colliderPos.x + extents.x, colliderPos.y - extents.y, 0f),
                new Vector3(colliderPos.x - extents.x, colliderPos.y + extents.y, 0f),
                new Vector3(colliderPos.x - extents.x, colliderPos.y - extents.y, 0f)
            };

            foreach (Vector3 colPos in bounds)
            {
                Vector3 dirToPlayer = (colPos - transform.position).normalized;
                if(Vector3.Angle(DirectionFromAngle(0), dirToPlayer) < viewAngle / 2)
                {
                    // Checks if there are any obstacles
                    float distanceToPlayer = Vector3.Distance(transform.position, colPos);
                    if(!Physics2D.Raycast(transform.position, dirToPlayer, distanceToPlayer, obstaclesMask) && distanceToPlayer <= viewRadius)
                    {
                        if(debugMode)
                        {
                            Debug.DrawLine(transform.position, colliderPos, Color.magenta);
                        }
                        AudioManager.Instance.Play("PlayerCaught");
                        GameManager.Instance.KillPlayer();
                    }
                }
            }
        }
    }

    private void DrawFieldOfView()
    {
        int rayCount = Mathf.RoundToInt(viewAngle * meshResolution);
        float angleSize = viewAngle / rayCount;

        pointList.Clear();
        for(int i = 0; i < rayCount; i++)
        {
            float angle = transform.eulerAngles.y - viewAngle / 2 + angleSize * i;
            MeshRaycastInfo newMRI = CreateMeshRay(angle);
            pointList.Add(newMRI.point);
            // Debug.DrawLine(transform.position, transform.position + DirectionFromAngle(angle), Color.red);
        }

        CreateFieldOfViewMesh();
    }

    private MeshRaycastInfo CreateMeshRay(float angle)
    {		
        Vector3 direction = DirectionFromAngle(angle);
        RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, viewRadius, obstaclesMask);
        if(hit)
        {
            return new MeshRaycastInfo(true, hit.point, hit.distance, angle);
        }
        else
        {
            return new MeshRaycastInfo(false, transform.position + direction, viewRadius, angle);
        }
    }

    private void CreateFieldOfViewMesh()
    {
        int vertexCount = pointList.Count + 1; // We add the origin vertex
        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(pointList[i]);
            if(i < vertexCount - 2)
            {
                triangles[i * 3] = 0;
                triangles[i * 3 + 1] = i + 1;
                triangles[i * 3 + 2] = i + 2;
            }
        }
        
        mesh.Clear();
        mesh.vertices = vertices;
        mesh.triangles = triangles;
        mesh.RecalculateNormals();
    }

    #endregion

    #region State Machine

    private void SetState(IEnumerator state)
    {
        if(currentState != null)
        {
            StopCoroutine(currentState);
        }
        currentState = state;
        StartCoroutine(currentState);
    }

    IEnumerator StateFindPlayer(float delay) // The delay is for performance 
    {
        while(true)
        {
            yield return new WaitForSeconds(delay);
            FindPlayer();
        }
    }

    #endregion
}
										

/*
* Carlos Adan Cortes De la Fuente
* All rights reserved. Copyright (c)
* Email: dev@carlosadan.com
*/

[RequireComponent(typeof(LineRenderer))]
public class Laser : MonoBehaviour 
{
    #region Properties

    private LineRenderer line;
    private Transform start;
    private Transform end;
    private AudioSource audioSrc;

    private RaycastHit2D hit;

    [Header("Laser Randomnes")]
    [SerializeField]
    private float laserStrength = 0.5f;
    [SerializeField, Range(3, 20)]
    private int laserBreaks = 15;

    [Header("Laser Switch")]
    [SerializeField]
    private bool switchingLaser = true;
    [SerializeField]
    private float timeOn = 3f;
    [SerializeField]
    private float timeOff = 3f;

    private bool isOn = false;

    [Header("Movement")]
    [SerializeField]
    private bool movingLaser = true;
    [Space]
    [SerializeField]
    private float startSpeed = 2f;
    [SerializeField]
    private float startXDis = 5f;
    [SerializeField]
    private float startYDis = 5f;
    
    [Space]
    [SerializeField]
    private float endSpeed = 2f;
    [SerializeField]
    private float endXDis = 5f;
    [SerializeField]
    private float endYDis = 5f;

    private TranslatingObject startTO;
    private TranslatingObject endTO;

    #endregion

    #region Unity Functions

    void Awake () 
    {
        start = transform.Find("Start");
        end = transform.Find("End");
        audioSrc = GetComponent<AudioSource>();

        if(movingLaser)
        {
            startTO = new TranslatingObject(Mathf.Abs(startSpeed), Mathf.Abs(startXDis), Mathf.Abs(startYDis));
            endTO = new TranslatingObject(Mathf.Abs(endSpeed), Mathf.Abs(endXDis), Mathf.Abs(endYDis));
        }

        line = GetComponent<LineRenderer>();
        if(switchingLaser)
        {
            StartCoroutine(SwitchLaser());
        }
    }
    
    void Update () 
    {
        if(movingLaser)
        {
            start.transform.localPosition = startTO.Move(start.transform.localPosition);
            end.transform.localPosition = endTO.Move(end.transform.localPosition);
        }
        DrawLaser();
    }

    #endregion

    #region Class Methods

    private void DrawLaser()
    {	
        if(isOn)
        {
            line.positionCount = laserBreaks;
            line.SetPosition(0, start.localPosition);
            line.SetPosition(line.positionCount - 1, end.localPosition);
            for (int i = 1; i < line.positionCount - 1; ++i)
            {
                Vector3 newPos = ((float)(line.positionCount - 1 - i) / (float)(line.positionCount - 1)) * start.localPosition + ((float)i / (float)(line.positionCount - 1)) * end.localPosition;
                newPos.x += Random.Range(-laserStrength, laserStrength);
                newPos.y += Random.Range(-laserStrength, laserStrength);
                line.SetPosition(i, newPos);
            }
            CheckLaserHit();
        }
        else
        {
            line.positionCount = 2;
            line.SetPosition(0, Vector2.zero);
            line.SetPosition(1, Vector2.zero);
        }
    }

    private void CheckLaserHit() 
    {
        Vector3 ab = -start.position + end.position;
        hit = Physics2D.Raycast(start.position, ab, ab.magnitude, LayerMask.GetMask("Player"));
        if(hit) 
        {
            AudioManager.Instance.Play("PlayerDeath");
            GameManager.Instance.KillPlayer();
        }
    }

    IEnumerator SwitchLaser()
    {
        while(true)
        {
            float time = isOn ? timeOff : timeOn;
            isOn = !isOn;
            if(isOn)
            {
                audioSrc.Play();	
            }
            else
            {
                audioSrc.Stop();
            }
            yield return new WaitForSeconds(time);
        }
    }

    #endregion
}