Skip to content

Instantly share code, notes, and snippets.

@scryptonite
Last active June 29, 2017 15:22
Show Gist options
  • Save scryptonite/90c63e7ebf02dfa4bf3c19173754b410 to your computer and use it in GitHub Desktop.
Save scryptonite/90c63e7ebf02dfa4bf3c19173754b410 to your computer and use it in GitHub Desktop.
using UnityEngine;
// Requires the MousePointer script if you plan on having the mouse be restored to its former position when released from being locked.
/// <summary>A Camera Controller that allows the user to pan/orbit and zoom around a target with their mouse.</summary>
[DisallowMultipleComponent, AddComponentMenu("Camera Controllers/Better Mouse Orbit")]
public class BetterMouseOrbit : MonoBehaviour {
#region Enumerations
/// <summary>
/// An enumeration of possible MouseButtons combinations for panning.
/// <para>See BetterMouseOribit's panMouseButton property.</para>
/// </summary>
public enum MouseButton {
None,
AnyMouseButton,
LeftMouseButton,
RightMouseButton,
MiddleMouseButton,
LeftOrRightMouseButton,
LeftAndRightMouseButton,
}
#endregion Enumerations
#region Camera Target Settings
/// <summary>The target that the camera is looking at and following.</summary>
[Header("Camera Target Settings"), Tooltip("The target that the camera is looking at and following.")]
public Transform target;
/// <summary>Controls the offset from the target's center.</summary>
[Tooltip("Controls the offset from the target's center.")]
public Vector3 targetOffset = Vector3.up * .5f;
/// <summary>Controls whether or not the camera should inherit the target's rotation.</summary>
[Tooltip("Should the camera be positioned relative to the target's rotation?")]
public bool inheritTargetRotation = false;
#endregion Camera Target Settings
#region Camera Distance & Zoom Settings
/// <summary>
/// Controls the desired distance away from the target.
/// <para>Changes when the user scrolls the mouse's scrollwheel.</para>
/// </summary>
[Header("Camera Distance & Zoom Settings"), Tooltip("The distance the camera is away from the target.")]
public float distance = 8f;
/// <summary>Represents the *true* distance from the target. Always being lerped towards the desired distance if not obstructed by a raycast.</summary>
public float _distance { get; private set; }
/// <summary>The closest the camera may be from the target.</summary>
[Tooltip("The closest the camera may be from the target.")]
public float distanceMin = 0f;
/// <summary>The furthest the camera may be from the target.</summary>
[Tooltip("The furthest the camera may be from the target.")]
public float distanceMax = 15f;
/// <summary>A multiplier that controls how fast the desired distance can change per delta of the mouse scrollwheel.</summary>
[Space, Tooltip("A multiplier that controls how fast the desired distance can change per delta of the mouse scrollwheel.")]
public float zoomSpeed = 8f;
/// <summary>The speed in which the camera's true distance lerps towards its desired distance.</summary>
[Tooltip("The speed in which the camera's true distance lerps towards its desired distance.")]
public float zoomLerpSpeed = 20f;
/// <summary>The speed in which the camera's true distance lerps towards its desired distance when the raycast behind the camera hits a surface.</summary>
[Tooltip("The speed in which the camera's true distance lerps towards its desired distance when the raycast behind the camera hits a surface.")]
public float zoomLerpClipSpeed = 50f;
#endregion Camera Distance & Zoom Settings
#region Camera Orientation & Pan Settings
/// <summary>
/// Controls the desired orientation of the camera relative to the target.
/// <para>Changes when the user pans the camera by activating the panMouseButton and moving their cursor.</para>
/// </summary>
[Header("Camera Orientation & Pan Settings"), Tooltip("The orientation of the camera relative to the target.")]
public Vector2 orientation = Vector2.zero;
/// <summary>Represents the *true* orientation of the camera. Always being lerped towards the desired orientation.</summary>
public Vector2 _orientation { get; private set; }
/// <summary>Restricts both orientations' y- axis from going below this value.</summary>
[Tooltip("Restricts both orientations' y- axis from going below this value.")]
public float orientationYMin = -20f;
/// <summary>Restricts both orientations' y- axis from exceeding this value.</summary>
[Tooltip("Restricts both orientations' y- axis from exceeding this value.")]
public float orientationYMax = 80f;
/// <summary>A multiplier that controls how fast the desired orientation can change per delta of mouse movement.</summary>
[Space, Tooltip("A multiplier that controls how fast the desired orientation can change per delta of mouse movement.")]
public Vector2 panSpeed = Vector2.one * 200f;
/// <summary>The speed in which the camera's true orientation lerps towards the desired orientation.</summary>
[Tooltip("The speed in which the camera's true orientation lerps towards the desired orientation.")]
public float panLerpSpeed = 50f;
/// <summary>The mouse button combination that must be pressed before panning takes place.</summary>
[Tooltip("Controls the mouse button combination that must be pressed before panning takes place.")]
public MouseButton panMouseButton = MouseButton.LeftOrRightMouseButton;
/// <summary>A property that is true while the configured panMouseButton combination is pressed.</summary>
private bool IsMouseButtonHeld {
get {
// switch depending on the configured mouse button:
switch (panMouseButton) {
// always being held if not configured:
case MouseButton.None: return true;
// true if any of the three mouse buttons are held:
case MouseButton.AnyMouseButton:
return Input.GetMouseButton(0)
|| Input.GetMouseButton(1)
|| Input.GetMouseButton(2);
// true if the left mouse button is held:
case MouseButton.LeftMouseButton: return Input.GetMouseButton(0);
// true if the right mouse button is held:
case MouseButton.RightMouseButton: return Input.GetMouseButton(1);
// true if the middle mouse button is held:
case MouseButton.MiddleMouseButton: return Input.GetMouseButton(2);
// true if either the left or right mouse button is held:
case MouseButton.LeftOrRightMouseButton:
return Input.GetMouseButton(0)
|| Input.GetMouseButton(1);
// true if both the left and right mouse button are held:
case MouseButton.LeftAndRightMouseButton:
return Input.GetMouseButton(0)
&& Input.GetMouseButton(1);
// false if unrecognized mouse button configuration:
default: return false;
}
}
}
#endregion Camera Orientation & Pan Settings
#region Mouse Pointer Locking Settings
/// <summary>Controls whether or not the mouse pointer should be locked while panning.</summary>
[Header("Mouse Lock Settings"), Tooltip("Should the mouse pointer be locked while panning?")]
public bool lockWhilePanning = true;
/// <summary>
/// Controls whether or not the mouse pointer should be centered when released after being locked.
/// <para>If false, only restores the mouse to its former position on windows.</para>
/// </summary>
[Tooltip("Should the mouse pointer be placed at the center of the game window when released after being locked?")]
public bool centerMouseAfterLock = false;
#endregion Mouse Pointer Locking Settings
#region Camera Distance Raycast Settings
/// <summary>Controls the distance the camera should be away from the surface of a raycast hit.</summary>
[Header("Camera Distance Raycast Settings"), Tooltip("Controls the distance the camera should be away from the surface of a raycast hit.")]
public float minSurfaceDistance = 0.3f;
/// <summary>Controls which layers are valid for a raycast hit (Default: -1, Everything)</summary>
[Tooltip("Controls which layers are valid for a raycast hit.")]
public LayerMask raycastLayerMask = -1;
/// <summary>If enabled, excludes the target's layer from the raycast at runtime.</summary>
[Tooltip("Should the camera exclude the target's layer from the raycast automatically?")]
public bool excludeTargetLayer = false;
#endregion Camera Distance Raycast Settings
#region Camera Controller Methods
/// <summary>Updates the camera's position values given a delta of time.</summary>
/// <param name="deltaTime">
/// A parameter that represents how much time has passed since the last call.
/// <para>Used to render the position the camera should be at immediately if 1.0f</para>
/// </param>
private void MoveCamera(float deltaTime) {
// store the target's position (or Vector3.zero if no target is configured):
Vector3 targetPosition = target ? target.position : Vector3.zero;
// store the target's layer (or none if no target is configured):
LayerMask targetLayer = target ? 1 << target.gameObject.layer : 0;
// store the target's rotation (or a neutral rotation if no target is configured or if not configured to inherit the target's rotation):
Quaternion targetRotation = inheritTargetRotation && target ? target.rotation : Quaternion.identity;
// update the target offset:
targetPosition += targetRotation * targetOffset;
// normalize and constrain both the desired and true orientation's x and y axis:
orientation = new Vector2(LoopAngle(orientation.x), ClampAngle(orientation.y, orientationYMin, orientationYMax));
_orientation = new Vector2(LoopAngle(_orientation.x), ClampAngle(_orientation.y, orientationYMin, orientationYMax));
// constrain both the desired and true distance:
distance = Mathf.Clamp(distance, distanceMin, distanceMax);
_distance = Mathf.Clamp(_distance, distanceMin, distanceMax);
// store current distance for later during logic handling the movement after a raycast hits something:
float prevDistance = _distance;
// lerp the true orientation towards the desired orientation:
_orientation = new Vector2(
Mathf.LerpAngle(_orientation.x, orientation.x, Mathf.Clamp01(deltaTime * panLerpSpeed)),
Mathf.LerpAngle(_orientation.y, orientation.y, Mathf.Clamp01(deltaTime * panLerpSpeed))
);
// lerp the true distance towards the desired distance:
_distance = Mathf.Lerp(_distance, distance, Mathf.Clamp01(deltaTime * zoomLerpSpeed));
// store the desired orientation as a quaternion:
Quaternion rotation = Quaternion.Euler(_orientation.y, _orientation.x, 0);
// create a ray that is pointing back and away from the target:
Vector3 ray = targetRotation * rotation * Vector3.back;
// prepare to capture a raycast hit:
RaycastHit hit;
// store the layermask of what counts as a valid raycast hit (and exclude the target's layer if exclude target layer is configured):
LayerMask layerMask = raycastLayerMask & (excludeTargetLayer ? ~targetLayer : -1);
// spherecast (which is a raycast but with a minimum surface distance) away from the target along the ray
// to prevent the target from being obstructed and the camera from clipping into a surface:
if (Physics.SphereCast(targetPosition, minSurfaceDistance, ray, out hit, distance, layerMask)) {
// recalculate the lerping of the true distance towards the desired distance, this time accounting for the raycast hit:
_distance = Mathf.Lerp(prevDistance, hit.distance, Mathf.Clamp01(deltaTime * zoomLerpClipSpeed));
}
// update the camera positoin and rotation:
transform.position = targetPosition + (ray * _distance);
transform.rotation = targetRotation * rotation;
//// draw debug rays to show the desired distance (red) and the true distance (white) of the camera in the scene view:
//Debug.DrawRay(targetPosition, ray * distance, Color.red);
//Debug.DrawRay(targetPosition, ray * _distance, Color.white);
}
/// <summary>
/// Called by Unity when the component values in the inspector are changed.
/// <para>Used to reposition the Camera while editing the scene.</para>
/// </summary>
private void OnValidate() {
// force the true orientation and distance to their desired values:
_orientation = orientation;
_distance = distance;
// immediately move the camera to where it should be without lerping:
MoveCamera(1f);
}
/// <summary>Awake is called once when script instance is being loaded. See Unity3D docs.</summary>
private void Awake() {
// setup the true orientation and distance:
_orientation = orientation;
_distance = distance;
}
/// <summary>LateUpdate is called every frame, if the component is enabled. See Unity3D docs.</summary>
private void LateUpdate() {
// store true distance and orientation for delta checking:
float prevDistance = _distance;
Vector2 prevOrientation = _orientation;
// if we should lock the mouse cursor while panning:
if (lockWhilePanning) {
// if the mouse shouldn't be centered after the pan is complete:
if (!centerMouseAfterLock) {
// using the MousePointer class means the mouse position will be restored on release:
MousePointer.lockState = IsMouseButtonHeld ? CursorLockMode.Locked : CursorLockMode.None;
MousePointer.visible = !IsMouseButtonHeld;
} else {
// using the default Unity3D cursor class means the mouse will be centered on release:
Cursor.lockState = IsMouseButtonHeld ? CursorLockMode.Locked : CursorLockMode.None;
Cursor.visible = !IsMouseButtonHeld;
}
}
// if the configured mouse button combination is pressed, then pan the camera's desired orientation by the mouse's movement deltas:
if (IsMouseButtonHeld) {
orientation += new Vector2(
Input.GetAxis("Mouse X") * panSpeed.x,
-Input.GetAxis("Mouse Y") * panSpeed.y) * Time.deltaTime;
}
// change the camera's desired distance by the mouses's scrollwheel delta:
distance -= Input.GetAxis("Mouse ScrollWheel") * zoomSpeed;
// move the camera's real orientation and distance towards their desired values given the delta of time since the last frame:
MoveCamera(Time.deltaTime);
}
/// <summary>A helper function that takes an angle and returns what it would be if it was looped between 0 and 360.</summary>
private static float LoopAngle(float angle) {
return Mathf.Repeat(angle, 360);
}
/// <summary>A helper function that takes an angle and returns it clamped between a minimum and maximum.</summary>
private static float ClampAngle(float angle, float min, float max) {
if (angle < -360F) angle += 360F;
if (angle > 360F) angle -= 360F;
return Mathf.Clamp(angle, min, max);
}
#endregion Camera Controller Methods
}
using System.Runtime.InteropServices;
using UnityEngine;
/// <summary>
/// A Wrapper API for UnityEngine.Cursor.
/// <para>Use this the same as you would the UnityEngine.Cursor API, but enjoy the fact that locking
/// and unlocking the cursor will restore the mouse pointer to the position before it was locked.</para>
/// <para>Also provides a static property for reading and writing the mouse position.</para>
/// <para>See MSDN docs' GetCursorPos and SetCursorPos, as well as Unity3D docs' UnityEngine.Cursor</para>
/// </summary>
internal class MousePointer {
#region User32 Cursor Position Manipulation
/// <summary>
/// (Private) A structure that defines an x- and y- coordinate on the screen.
/// Used exclusively to store a mouse's position coordinates.
/// <para>See MSDN docs' POINT structure</para>
/// </summary>
[StructLayout(LayoutKind.Sequential)]
private struct MousePosition {
public int x;
public int y;
public MousePosition(int x, int y) {
this.x = x;
this.y = y;
}
}
/// <summary>
/// (Private) Moves the cursor to the specified screen coordinates.
/// <para>See MSDN docs' SetCursorPos()</para>
/// </summary>
/// <param name="x">The new x-coordinate of the cursor.</param>
/// <param name="y">The new y-coordinate of the cursor.</param>
/// <returns>Returns true if successful, false otherwise.</returns>
[DllImport("User32.dll")]
private static extern bool SetCursorPos(int x, int y);
/// <summary>
/// (Private) Moves the cursor to the specified screen coordinates.
/// </summary>
/// <param name="position">The new position of the cursor.</param>
/// <returns>Returns true if successful, false otherwise.</returns>
private static bool SetCursorPos(MousePosition position) {
return SetCursorPos(position.x, position.y);
}
/// <summary>
/// Retrieves the position of the mouse cursor, in screen coordinates.
/// <para>See MSDN docs' GetCursorPos()</para>
/// </summary>
/// <param name="position">A pointer to a POINT structure that receives the screen coordinates of the cursor.</param>
/// <returns>Returns true if successful or false otherwise.</returns>
[DllImport("User32.dll")]
private static extern bool GetCursorPos(out MousePosition position);
#endregion User32 Cursor Position Manipulation
#region Wrapper API for UnityEngine.Cursor
/// <summary>
/// (Private) Represents the last position the mouse was at before the cursor was locked.
/// <para>Used for restoring the mouse to its former position when unlocking the cursor.</para>
/// </summary>
private static MousePosition lastMousePosition = new MousePosition { x = 0, y = 0 };
/// <summary>Represents and controls the mouse's position on the screen as a Vector2.</summary>
public static Vector2 position {
get {
MousePosition p;
if (GetCursorPos(out p)) return new Vector2(p.x, p.y);
return Vector2.zero;
}
set {
MousePosition p = new MousePosition((int)value.x, (int)value.y);
SetCursorPos(p);
}
}
/// <summary>
/// Determines whether the hardware pointer is visible or not.
/// <para>See Unity3D docs' UnityEngine.Cursor.visible</para>
/// </summary>
public static bool visible {
get { return Cursor.visible; }
set { Cursor.visible = value; }
}
/// <summary>
/// Determines whether the hardware pointer is locked to the center of the view, constrained to the window, or not constrained at all.
/// <para>Restores the cursor to its position prior to being locked when you set the lockState to CursorLockMode.None.</para>
/// <para>See Unity3D docs' UnityEngine.Cursor.lockState</para>
/// </summary>
public static CursorLockMode lockState {
get { return Cursor.lockState; }
set {
switch (value) {
case CursorLockMode.Locked:
if (Cursor.lockState == CursorLockMode.Locked) return;
Cursor.visible = false;
MousePosition position;
if (GetCursorPos(out position))
lastMousePosition = position;
Cursor.lockState = CursorLockMode.Locked;
break;
default:
if (Cursor.lockState == CursorLockMode.Locked) {
Cursor.lockState = value;
Cursor.visible = false;
//lastMousePosition.x += (int)Input.GetAxisRaw("Mouse X");
//lastMousePosition.y += (int)-Input.GetAxisRaw("Mouse Y");
SetCursorPos(lastMousePosition);
Cursor.visible = true;
} else {
Cursor.lockState = value;
}
break;
}
}
}
/// <summary>
/// Specify a custom cursor that you wish to use as a cursor.
/// <para>See Unity3D docs' UnityEngine.Cursor.SetCursor()</para>
/// </summary>
public static void SetCursor(Texture2D texture, Vector3 hotspot, CursorMode mode) {
Cursor.SetCursor(texture, hotspot, mode);
}
#endregion Wrapper API for UnityEngine.Cursor
}
using UnityEngine;
/// <summary>
/// Bonus Script: Fades MeshRenderers in or out depending on how close the mouse orbit camera is.
/// </summary>
public class PlayerFadeOnOrbitCameraNear : MonoBehaviour {
private void Update() {
float targetAlpha = 1f;
float distance = Camera.main.GetComponent<BetterMouseOrbit>()._distance;
if (distance < 1.3f)
targetAlpha = 0f;
else if (distance < 2)
targetAlpha = .5f;
var renderers = GetComponentsInChildren<MeshRenderer>();
foreach (var renderer in renderers) {
var color = renderer.material.color;
color.a = Mathf.MoveTowards(color.a, targetAlpha, 10f * Time.deltaTime);
renderer.material.color = color;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment