-
Adrian Paschkowski authoredAdrian Paschkowski authored
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);
}
}
}