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