using System;
using UnityEngine;
using Framework.Input.Profiles;
using Framework.State;
using Framework.Services;
namespace Framework.Actors.Shooter {
/// Lets the player to control a character's head via the mouse
[RequireComponent(typeof(HeadPoser))]
public class MouseLookHeadController : ScriptComponent, IHeadController {
/// Fired to pass on head rotation that was clamped
///
///
/// When the player rotates the character's head beyond its movement range,
/// by default the head simply stops. For first person games, it is more
/// intuitive (and established practice) to rotate the body so the player can
/// use the mouse not only turn the head but also the whole body around.
/// Use this event to get notified of head rotation that was clamped off so
/// you can apply it to the body.
///
///
/// Fancy implementation might even want to watch this event and play
/// a "step in place" animation to turn the body completely
/// into the direction the player is looking at once it reaches the boundaries
/// of what pure head movement allows.
///
///
public event Action ExcessHeadRotation;
/// Input profiles in which the mouse-based head controller is enabled
public InputSources EnabledInputProfiles = InputSources.Controllers;
/// How sensitive the mouse is in degrees per mickey
///
/// There is no standard method by which the actual sensitivity of the mouse
/// hardware can be queried, so this is pretty much a guessing game between
/// user preferences and hardware features.
///
public Vector2 MouseSensitivity = new Vector2(2.0f, 2.0f);
/// Whether the vertical mouse axis should be inverted
///
/// Some players prefer the mouse to work like a flight stick (forward = dive,
/// look down) while others prefer it to
///
public bool InvertVerticalAxis = true;
/// Whether 'pitch' is included in excess rotation reports
///
/// Enabling this will allow the player to rotate the actor by looking up
/// or down beyond the head range. The problem with this is that when
/// the player looks down by 10 degrees near the center of the actor,
/// it will spin 45 degrees or so to face the spot the player was pointing at.
///
[HideInInspector, NonSerialized] // I think this is just confusing at this point
public bool IncludePitchInExcessRotation = false;
/// Camera transform to place where the eyes are
///
/// This is an optional feature: if you have a VR rig where the camera is
/// not parented to the chest/neck/head bones (because player head movements
/// move the camera around), you make the mouse look head controller update
/// the camera position to match the position of the head poser's eyes.
///
public Transform EyeCamera;
/// Injects the instance's dependencies
///
/// Input selector the instance uses to determine the active input source
///
///
/// This method is called automatically by the services framework. Any parameters
/// it requires will be filled out by looking up the respective services and creating
/// them as needed.
///
protected void Inject(IInputSelector inputSelector) {
this.inputSelector = inputSelector;
}
/// Called when the component has been added to a game object
protected override void Awake() {
base.Awake();
// This implementation exclusively uses the head poser to change
// the orientation of the actor's head
this.headPoser = GetComponent();
if (this.headPoser == null) {
Debug.LogError(
"Mouse look head controller assigned to '" + gameObject.name + "' found " +
"no head poser component and will not work."
);
}
// The HeadMovementRange component is shared between all head controllers
// to define the limits within which the actor can move its head
this.headMovementRange = GetComponent();
if(this.headMovementRange == null) {
this.headMovementRange = gameObject.AddComponent();
}
// Hook into the controllables framework if it is being used
this.controllable = GetComponentInParent();
if(this.controllable == null) {
OnControlTaken(0); // Fallback if 'controllables' framework not used
} else {
this.onControlTakenDelegate = new Action(OnControlTaken);
this.onControlReleasedDelegate = new Action(OnControlReleased);
this.controllable.ControlTaken += this.onControlTakenDelegate;
this.controllable.ControlReleased += this.onControlReleasedDelegate;
}
}
/// Called when the component gets destroyed
protected virtual void OnDestroy() {
if(this.controllable == null) {
OnControlReleased(0); // Fallback if 'controllables' framework not used
} else {
this.controllable.ControlReleased -= this.onControlReleasedDelegate;
this.controllable.ControlTaken -= this.onControlReleasedDelegate;
this.controllable = null;
}
}
/// Called once per physics frame
protected virtual void FixedUpdate() {
// If the player doesn't control this actor, don't do head movement.
if(this.controllable != null) {
if(this.controllable.ControllingPlayerIndex == -1) {
return;
}
}
// If the active input profile doesn't use mouse look, don't do anything.
if((this.inputSelector.ActiveSource & this.EnabledInputProfiles) == 0) {
return;
}
// Cursor lock can be lost at any time and Unity seems to put it in
// the hands of the game's code to check and correct for this. Re-checking
// once per frame seems to be okay and if the game is not in the foreground,
// will not cause any issues (tested as of Unity 5.6).
if(this.shouldCursorBeLocked) {
#if !UNITY_EDITOR
if(Cursor.lockState != CursorLockMode.Locked) {
Cursor.lockState = CursorLockMode.Locked;
}
#endif // !UNITY_EDITOR
}
// Update the pitch and yaw according to mouse deltas. Within the editor,
// allow mouse look to be disabled by holding the control key.
#if UNITY_EDITOR
if(!UnityEngine.Input.GetKey(KeyCode.LeftControl)) {
handleMouseInput();
}
#else
handleMouseInput();
#endif
}
/// Called once per physics frame
protected virtual void LateUpdate() {
// If the player doesn't control this actor, don't do head movement.
if(this.controllable != null) {
if(this.controllable.ControllingPlayerIndex == -1) {
return;
}
}
// If the active input profile doesn't use mouse look, don't do anything.
if((this.inputSelector.ActiveSource & this.EnabledInputProfiles) == 0) {
return;
}
// If the head poser component is missing, we can't do anything
// (a warning will have been logged when the component was initialized).
if(this.headPoser == null) {
return;
}
// Calculate the target orientation relative to the transform all this
// is based on (either the neck or the chest). This will ensure the player
// can look around, no matter if the protagonist is standing or lying down.
Quaternion targetOrientation;
{
targetOrientation = this.headPoser.GetBaseOrientation();
targetOrientation *= Quaternion.AngleAxis(this.headYaw, Vector3.up);
targetOrientation *= Quaternion.AngleAxis(this.headPitch, Vector3.right);
}
// Tell the head poser to match the head to our look direction
this.headPoser.Orient(targetOrientation);
// If we need to update a non-parented camera, do so now
if (this.EyeCamera != null) {
copyTransformToEyeCamera();
}
}
/// Fires the event
///
/// Excess rotation that will be reported through the event
///
protected virtual void OnExcessHeadRotation(Quaternion excessRotation) {
if(ExcessHeadRotation != null) {
ExcessHeadRotation(excessRotation);
}
}
/// Called when a player assumes control of the actor
/// Index of the player that has assumed control
protected virtual void OnControlTaken(int playerIndex) {
#if !UNITY_EDITOR
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
#endif
this.shouldCursorBeLocked = true;
}
/// Called when a player releases control of the actor
/// Index of the player that has released control
protected virtual void OnControlReleased(int playerIndex) {
this.shouldCursorBeLocked = false;
#if !UNITY_EDITOR
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
#endif
}
/// Handles mouse control of the head
private void handleMouseInput() {
// Get raw mouse input for a cleaner reading on more sensitive mice.
this.headYaw += UnityEngine.Input.GetAxisRaw("Mouse X") * this.MouseSensitivity.x;
if(this.InvertVerticalAxis) {
this.headPitch += UnityEngine.Input.GetAxisRaw("Mouse Y") * this.MouseSensitivity.y;
} else {
this.headPitch -= UnityEngine.Input.GetAxisRaw("Mouse Y") * this.MouseSensitivity.y;
}
clampAndReportExcessHeadRotation();
}
///
/// Clamps the head rotation to the configured limits and triggers
/// the excess rotation event to report the amount clamped off
///
private void clampAndReportExcessHeadRotation() {
// Perform clamping end remember the amount of rotation we originally wanted to perform
float excessHeadPitch = this.headPitch;
float excessHeadYaw = this.headYaw;
this.headMovementRange.ClampPitchAndYaw(ref this.headPitch, ref this.headYaw);
excessHeadPitch -= this.headPitch;
excessHeadYaw -= this.headYaw;
// Report the excess rotation
bool excessRotationPresent = (
(Math.Abs(excessHeadPitch) >= float.Epsilon) ||
(Math.Abs(excessHeadYaw) >= float.Epsilon)
);
if(excessRotationPresent) {
if(IncludePitchInExcessRotation) {
OnExcessHeadRotation(
Quaternion.AngleAxis(excessHeadYaw, Vector3.up) *
Quaternion.AngleAxis(excessHeadPitch, Vector3.right)
);
} else {
OnExcessHeadRotation(Quaternion.AngleAxis(excessHeadYaw, Vector3.up));
}
}
}
/// Positions a non-parented camera a the eyes of the head rig
private void copyTransformToEyeCamera() {
Transform headTransform = this.headPoser.Head;
Vector3 eyePosition = headTransform.position + (
headTransform.rotation * this.headPoser.EyeOffset
);
Transform eyeCameraTransform = this.EyeCamera.transform;
eyeCameraTransform.position = eyePosition;
eyeCameraTransform.rotation = headTransform.rotation;
}
/// Transform to which the virtual reality camera is parented
Transform IHeadController.VirtualRealityCameraRoot {
get { return null; } // We're not a virtual reality head controller
}
/// Base transform in which the player controls the look direction
Quaternion IHeadController.HeadBaseOrientation {
get {
if(this.headPoser == null) {
return Quaternion.identity;
} else {
return this.headPoser.GetBaseOrientation();
}
}
}
/// Where the player is looking within the head's coordinate space
///
/// Append this rotation to the to
/// get the direction the player is looking at in the global coordinate frame.
///
Quaternion IHeadController.LocalHeadOrientation {
get {
return (
Quaternion.AngleAxis(this.headYaw, Vector3.up) *
Quaternion.AngleAxis(this.headPitch, Vector3.right)
);
}
set {
// Apply the Euler angles (barf!) to the mouse-controlled counters
Vector3 headEulers = value.eulerAngles;
if(headEulers.x >= 180.0f) {
this.headPitch = headEulers.x - 360.0f;
} else {
this.headPitch = headEulers.x;
}
if(headEulers.y >= 180.0f) {
this.headYaw = headEulers.y - 360.0f;
} else {
this.headYaw = headEulers.y;
}
// Now do the clamping and report excess rotation to the body
clampAndReportExcessHeadRotation();
// We do not update the head poser here right away because it will be done in
// LateUpdate() anyway (and this method is most likely to be called during
// FixedUpdate() or Update()).
}
}
/// Whether this look handler is part of the active input profile
///
/// It is not unusual for actors to have multiple head controllers. For example,
/// there might be a mouse look head controller and a virtual reality head controller
/// that are switched between depending on the input profile the game runs with.
/// This property allows the actor controller to figure out which one to talk to.
///
bool IHeadController.IsActive {
get {
if(this.controllable != null) {
if(this.controllable.ControllingPlayerIndex == -1) {
return false;
}
}
return ((this.inputSelector.ActiveSource & this.EnabledInputProfiles) != 0);
}
}
/// Input selector from which the active input source will be queried
private IInputSelector inputSelector;
/// Tracks, if present, whether the player is controlling this actor
private IControllable controllable;
/// Delegate for the OnControlTaken() method
private Action onControlTakenDelegate;
/// Delegate for the OnControlReleased() method
private Action onControlReleasedDelegate;
/// Head poser the mouse look controller uses to move the head
private HeadPoser headPoser;
/// Controls how far the head can be moved on each axis
private HeadMovementRange headMovementRange;
/// Yaw rotation of the head
private float headYaw;
/// Pitch rotation of the head
private float headPitch;
/// Whether the mouse cursor should currently be locked
///
/// Only true when the player is in control of this actor
///
private bool shouldCursorBeLocked;
}
} // namespace Framework.Actors.Shooter