A camera script to prevent object occlusion

A camera script to prevent object occlusion

Hey, today I want to share with you a camera script that prevents environment objects from occluding the player.

I’m currently working on a project where the camera follows the player from a fixed distance and angle, independently of the player's direction.

Collision Detection

The camera's initial behavior was fairly easy to implement. Now, the problem was preventing the player from being occluded by obstacles in the environment.

The intended behavior was to position the camera in the collision point with the obstacle and rotate it to keep the player in sight.

To solve it, I’ve added a trigger to the camera to detect any collision. In this way, I was able to detect when the camera was colliding with another object and position it accordingly:

Now, I just had to stop the camera movement and adjust the rotation to keep the player in sight:

To correctly position the camera I’ve used the ClosestPoint method of the Collider class.

 public Vector3 ClosestPoint(Vector3 position);

This method, returns the position of the closest point in the collider surface, to the position given in the argument. In this case, I was looking for the closest point to the player's position and using it to properly position the camera at the intersection point.

To this point, anytime the camera would collide with any other object, it would move to the surface nearest point to the player and adjust its rotation accordingly.

void Update()
{
    // direction vector from the camera to the player position
    Vector3 direction = _player.transform.position - transform.position;

    // create a rotation aligned with the previous direction vector
    _targetRotation = Quaternion.LookRotation(direction);

    // only apply the X axis of the rotation
    // this way even if the camera is falling behind when following the player 
    // it won't rotate sideways
    _targetRotation = Quaternion.Euler(_targetRotation.eulerAngles.x, 0, 0);

    // change the camera rotation in update
    transform.rotation = Quaternion.Slerp(transform.rotation, _targetRotation, Time.deltaTime * _speedRotation);


    // target position
    _desiredPosition = new Vector3(_player.transform.position.x, transform.position.y, _player.transform.position.z - _distance);

    // update the adjusted position
    _adjustedPosition = _desiredPosition;

    // if the camera collided with something
    if (_collider != null)
    {
        // change the camera position to collider closest point to the player
        _adjustedPosition.z = _collider.ClosestPoint(_player.transform.position).z + _wallOffset;

        // if the player is far enough from the camera just follow him
        if (_player.transform.position.z - _adjustedPosition.z > _distance)
        {
            _collider = null;
        }
    }

    // change the camera position
    transform.position = Vector3.Lerp(transform.position, _adjustedPosition, Time.deltaTime * _speedMovement);
}

private void OnTriggerEnter(Collider other)
{
    _inTrigger = true;
    _collider = other;
}

private void OnTriggerExit(Collider other)
{
    _inTrigger = false;
    _collider = null;
}

This was all going well, and I thought my job was nearly done. Always optimistic.

Linecasting

Then the following question hit me:

What if the camera is not colliding with the object that is occluding the player?

At this point, I’ve realized I would need more than just the trigger to solve this issue.

Linecast to the rescue!

public static bool Linecast(Vector3 start, Vector3 end, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

A linecast casts an invisible line between two points returning true if it’s intersected by any collider.

So, in this case, in addition to the trigger, I’ve added a linecast between the camera and the player's position. Every time the cast detects a collider it behaves exactly in the same way as with the trigger.

IEnumerator Start()
{
    if (_player == null)
    {
        Debug.LogError("You need to assign a Player Object!");
    }

    while (true)
    {
        bool occluded = Physics.Linecast(transform.position, _player.transform.position, out _hit);

        if (_collider == null && occluded) // if the camera it's not fixed and the player is occluded
        {
            _collider = _hit.transform.GetComponent<Collider>();
        }
        else if (!occluded && !_inTrigger) // else if not occluded nor in any trigger, release the camera
        {
            if (!Physics.Linecast(_desiredPosition, _player.transform.position, out _hit))
            {
                _collider = null;
            }
        }

        yield return new WaitForSeconds(0.1f);
    }
}

I’m using the linecast inside a coroutine to use it every 0.1 seconds instead of every frame. But it could as easily be implemented in the Update method. Then, after a few tweaks to make it work with both systems, I think my camera script is ready. At least a very first version.

As always, all feedback is more than welcome.

Check the example here.