Skip to content
Snippets Groups Projects
BaseCameraController.cs 10.13 KiB
using System;
using UnityEngine;

public class BaseCameraController : MonoBehaviour
{

    [Header("General")]
    [SerializeField] bool allowUserControl = true;

    [Header("Movement")]
    [SerializeField] float inputVelocity = 2f;
    [SerializeField] float inputDampening = .1f;
    [SerializeField] Vector2 edgeDetectionBoxInsets = new Vector2(0.1f, 0.1f);

    [Header("Zoom")]
    [SerializeField] float maxZoom = 5f;
    [SerializeField] float minZoom = 1.5f;
    [SerializeField] float zoomStepSize = 1f;
    [SerializeField] float zoomSpeed = 10f;

    new Camera camera;
    new AudioSource audio;
    new BoxCollider2D collider;
    new Rigidbody2D rigidbody;

    // Move cam once mouse is near the screen edges
    InsetCameraEdges mouseMovementEdges;
    bool waitForMouseOutsideEdges = false;
    Vector2 inputVector;
    InterpolatorQueue zoomInterpolator;
    Interpolator xInputInterpolator;
    Interpolator yInputInterpolator;
    InterpolatorQueue xPositionInterpolator;
    InterpolatorQueue yPositionInterpolator;
    float zoomRatio // Move slower when zoomed in
    {
        get => camera.orthographicSize / maxZoom;
    }

    public void PlayGlobalAudioClip(AudioClip clip, float volume = 1f)
    {
        audio.PlayOneShot(clip, volume);
    }

    public void SetUserControlEnabled(bool allowUserControl)
    {
        this.allowUserControl = allowUserControl;
    }

    public void AnimateToPosition(Vector3 worldPos, float zoom, float duration)
    {
        zoomInterpolator.PushInterpolator(new Interpolator(duration, camera.orthographicSize, zoom));
        xPositionInterpolator.PushInterpolator(new Interpolator(duration, transform.position.x, worldPos.x));
        yPositionInterpolator.PushInterpolator(new Interpolator(duration, transform.position.y, worldPos.y));
    }

    void Awake()
    {
        camera = GetComponent<Camera>();
        audio = GetComponent<AudioSource>();
        collider = GetComponent<BoxCollider2D>();
        rigidbody = GetComponent<Rigidbody2D>();

        mouseMovementEdges = new InsetCameraEdges(camera, edgeDetectionBoxInsets);
        zoomInterpolator = new InterpolatorQueue(new Interpolator(1 / zoomSpeed, camera.orthographicSize, camera.orthographicSize, minZoom, maxZoom));
        xInputInterpolator = new Interpolator(inputDampening);
        yInputInterpolator = new Interpolator(inputDampening);
        xPositionInterpolator = new InterpolatorQueue(new Interpolator(inputDampening, transform.position.x));
        yPositionInterpolator = new InterpolatorQueue(new Interpolator(inputDampening, transform.position.y));
        UpdateColliderSize();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Z)) // TODO Remove evntually
        {
            var player = GameObject.FindGameObjectWithTag("Player");
            AnimateToPosition(player.transform.position + new Vector3(0f, 0.75f), 1.5f, 0.1f);
        }

        // Disable collider while there is an ongoing animation
        collider.enabled = !HasOverriddenOnterpolators();

        mouseMovementEdges.DrawDebug(!IsUserControlEnabled() ? Color.yellow : waitForMouseOutsideEdges ? Color.red : Color.green);
        if (IsUserControlEnabled())
        {
            // Keyboard input
            inputVector = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
            if (inputVector.magnitude != 0)
            {
                // If the mouse is in the edge zone and the user uses keyboard navigation, that keybard
                // nav will override the mouse input. Once the user stops keyboard nav, mouse input takes
                // over again, which might continue moving the camera in the same or even move it
                // in the opposite direction.
                // To avoid such confusing behavior, disable mouse input if the mouse is in the edge zone
                // and the user uses keyboard nav. Re-enable it once the user moves the mouse out of the
                // edge zone.
                waitForMouseOutsideEdges = true;
            }

            // Mouse input
            var mouseViewportPos = camera.ScreenToViewportPoint(Input.mousePosition);
            var mouseInputVector = mouseMovementEdges.GetOutOfBoundsDirection(mouseViewportPos);
            if (waitForMouseOutsideEdges && mouseInputVector.magnitude == 0)
            {
                // If mouse input was disabled and mouse left edge zone, re-enable mouse input
                waitForMouseOutsideEdges = false;
            }
            if (!waitForMouseOutsideEdges && inputVector.magnitude == 0)
            {
                // Use mouse input only if enabled and if there is no keyboard input
                inputVector = mouseInputVector;
            }
            xInputInterpolator.targetValue = inputVector.x * zoomRatio * inputVelocity;
            yInputInterpolator.targetValue = inputVector.y * zoomRatio * inputVelocity;

            var inputZoomDelta = Input.mouseScrollDelta.y * -zoomStepSize;
            if (inputZoomDelta != 0)
            {
                zoomInterpolator.targetValue += inputZoomDelta;
            }
        }

        // Always interpolate zoom as that might be animted without user input enabled
        if (zoomInterpolator.running)
        {
            camera.orthographicSize = zoomInterpolator.Tick();
            UpdateColliderSize();
            // TODO move towards mouse location
        }
    }

    void FixedUpdate()
    {
        xPositionInterpolator.currentValue = rigidbody.position.x;
        yPositionInterpolator.currentValue = rigidbody.position.y;
        // If there are interpolators overriding user input, use them instead. Else, use user input.
        var overrideInput = xPositionInterpolator.hasInterpolatorsQueued || yPositionInterpolator.hasInterpolatorsQueued;
        var targetPos = overrideInput ?
            new Vector3(xPositionInterpolator.Tick(), yPositionInterpolator.Tick()) :
            transform.position + new Vector3(xInputInterpolator.Tick(), yInputInterpolator.Tick());
        rigidbody.MovePosition(targetPos);
        if (overrideInput)
        {
            // Reset values and remaining velocity of input interpolators to prevent them from
            // causing a slight movement jump once they are re-enabled
            xInputInterpolator.Reset();
            yInputInterpolator.Reset();
        }
    }

    bool IsUserControlEnabled()
    {
        return allowUserControl && !HasOverriddenOnterpolators();
    }

    bool HasOverriddenOnterpolators()
    {
        return zoomInterpolator.hasInterpolatorsQueued || xPositionInterpolator.hasInterpolatorsQueued || yPositionInterpolator.hasInterpolatorsQueued;
    }

    // Resize the attached collider to fit the visible camera bounds
    void UpdateColliderSize()
    {
        var orthographicWidth = camera.orthographicSize * camera.aspect;
        var colliderSize = collider.size;
        colliderSize.x = orthographicWidth * 2;
        colliderSize.y = camera.orthographicSize * 2;
        collider.size = colliderSize;
    }

    // Draw the edge zone in the editor preview
    void OnDrawGizmosSelected()
    {
        if (camera == null || mouseMovementEdges == null)
        {
            camera = GetComponent<Camera>();
            mouseMovementEdges = new InsetCameraEdges(camera);
        }
        mouseMovementEdges.RecalculateBounds(edgeDetectionBoxInsets);
        mouseMovementEdges.DrawGizmos();
    }

    // Represents the edge zone, used for mouse-based movement. Operates in screen space.
    [Serializable]
    private class InsetCameraEdges
    {
        private Camera camera;
        private Vector3 insetVector;

        private Box edges;

        public InsetCameraEdges(Camera camera) : this(camera, 0) { }

        public InsetCameraEdges(Camera camera, float inset) : this(camera, new Vector2(inset, inset)) { }

        public InsetCameraEdges(Camera camera, Vector2 insets)
        {
            this.camera = camera;
            RecalculateBounds(insets);
        }

        // Returns whether the given point is left (-1, 0), right (1, 0), above (0, 1)
        // or below (0, -1) the current bounds, or a mix of those.
        public Vector2 GetOutOfBoundsDirection(Vector2 point)
        {
            return new Vector2(
                point.x < edges.bottomLeft.x ? -1 : point.x > edges.topRight.x ? 1 : 0,
                point.y < edges.bottomLeft.y ? -1 : point.y > edges.topRight.y ? 1 : 0
            );
        }

        public void DrawGizmos()
        {
            var worldEdges = GetWorldEdges(0);
            Gizmos.color = Color.red;
            Gizmos.DrawLine(worldEdges.bottomLeft, worldEdges.bottomRight);
            Gizmos.DrawLine(worldEdges.bottomRight, worldEdges.topRight);
            Gizmos.DrawLine(worldEdges.topRight, worldEdges.topLeft);
            Gizmos.DrawLine(worldEdges.topLeft, worldEdges.bottomLeft);
        }

        public void DrawDebug(Color color)
        {
            var worldEdges = GetWorldEdges(0);
            Debug.DrawLine(worldEdges.bottomLeft, worldEdges.bottomRight, color);
            Debug.DrawLine(worldEdges.bottomRight, worldEdges.topRight, color);
            Debug.DrawLine(worldEdges.topRight, worldEdges.topLeft, color);
            Debug.DrawLine(worldEdges.topLeft, worldEdges.bottomLeft, color);
        }

        // Update the size of the bounds
        public void RecalculateBounds(float newInset)
        {
            RecalculateBounds(new Vector2(newInset, newInset));
        }

        public void RecalculateBounds(Vector2 newInsets)
        {
            insetVector = newInsets;
            RecalculateBounds();
        }

        public void RecalculateBounds()
        {
            edges = new Box(insetVector, Vector3.one - insetVector);
        }

        // Convert the current bounding box to world coordinates
        private Box GetWorldEdges(float? zOverride = null)
        {
            var worldBottomLeft = camera.ViewportToWorldPoint(edges.bottomLeft);
            var worldTopRight = camera.ViewportToWorldPoint(edges.topRight);
            if (zOverride != null)
            {
                worldBottomLeft.z = (float)zOverride;
                worldTopRight.z = (float)zOverride;
            }
            return new Box(worldBottomLeft, worldTopRight);
        }

    }
}