/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Input { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.UltimateCharacterController.Events; using Opsive.UltimateCharacterController.Utility; using Opsive.UltimateCharacterController.StateSystem; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; /// /// Abstract class to expose a common interface for any input implementation. /// public abstract class PlayerInput : StateBehavior { /// /// Specifies how to set the look vector. /// public enum LookVectorMode { Smoothed, // A smoothing will be applied to the look vector. UnitySmoothed, // The smoothed Unity input values will be used. Raw, // The raw input values will be used. Manual // The look vector is assigned manually. This is useful for VR head movement. } [Tooltip("The name of the horizontal camera input mapping.")] [SerializeField] protected string m_HorizontalLookInputName = "Mouse X"; [Tooltip("The name of the vertical camera input mapping.")] [SerializeField] protected string m_VerticalLookInputName = "Mouse Y"; [Tooltip("Specifies how the look vector is assigned.")] [SerializeField] protected LookVectorMode m_LookVectorMode = LookVectorMode.Smoothed; [Tooltip("If using look smoothing, specifies how sensitive the mouse is. The higher the value the more sensitive.")] [SerializeField] protected Vector2 m_LookSensitivity = new Vector2(2f, 2f); [Tooltip("If using look smoothing, specifies a multiplier to apply to the LookSensitivity value.")] [SerializeField] protected float m_LookSensitivityMultiplier = 1; [Tooltip("If using look smoothing, the amount of history to store of previous look values.")] [SerializeField] protected int m_SmoothLookSteps = 20; [Tooltip("If using look smoothing, specifies how much weight each element should have on the total smoothed value (range 0-1).")] [SerializeField] protected float m_SmoothLookWeight = 0.5f; [Tooltip("If using look smoothing, specifies an exponent to give a smoother feel with smaller inputs.")] [SerializeField] protected float m_SmoothExponent = 1.05f; [Tooltip("If using look smoothing, specifies a maximum acceleration value of the smoothed look value (0 to disable).")] [SerializeField] protected float m_LookAccelerationThreshold = 0.4f; [Tooltip("The rate (in seconds) the component checks to determine if a controller is connected.")] [SerializeField] protected float m_ControllerConnectedCheckRate = 1f; [Tooltip("The state that should be activated when a controller is connected.")] [SerializeField] protected string m_ConnectedControllerState = "ConnectedController"; [Tooltip("Unity event invoked when the gameplay input is enabled or disabled.")] [SerializeField] protected UnityBoolEvent m_EnableGamplayInputEvent; public string HorizontalLookInputName { get { return m_HorizontalLookInputName; } set { m_HorizontalLookInputName = value; } } public string VerticalLookInputName { get { return m_VerticalLookInputName; } set { m_VerticalLookInputName = value; } } public LookVectorMode LookMode { get { return m_LookVectorMode; } set { m_LookVectorMode = value; if (m_LookVectorMode == LookVectorMode.Smoothed && m_SmoothLookBuffer == null) { m_SmoothLookBuffer = new Vector2[m_SmoothLookSteps]; } } } public Vector2 LookSensitivity { get { return m_LookSensitivity; } set { m_LookSensitivity = value; } } public float LookSensitivityMultiplier { get { return m_LookSensitivityMultiplier; } set { m_LookSensitivityMultiplier = value; } } public int SmoothLookSteps { get { return m_SmoothLookSteps; } set { m_SmoothLookSteps = value; } } public float SmoothLookWeight { get { return m_SmoothLookWeight; } set { m_SmoothLookWeight = value; } } public float SmoothExponent { get { return m_SmoothExponent; } set { m_SmoothExponent = value; } } public float LookAccelerationThreshold { get { return m_LookAccelerationThreshold; } set { m_LookAccelerationThreshold = value; } } public float ControllerConnectedCheckRate { get { return m_ControllerConnectedCheckRate; } set { m_ControllerConnectedCheckRate = value; } } public string ConnectedControllerState { get { return m_ConnectedControllerState; } set { m_ConnectedControllerState = value; } } public UnityBoolEvent EnableGameplayInputEvent { get { return m_EnableGamplayInputEvent; } set { m_EnableGamplayInputEvent = value; } } private Vector2[] m_SmoothLookBuffer; private int m_SmoothLookBufferIndex; private int m_SmoothLookBufferCount; protected Vector2 m_RawLookVector; protected Vector2 m_CurrentLookVector; private float m_TimeScale = 1; private bool m_ControllerConnected; private Dictionary m_ButtonDownTime; private Dictionary m_ButtonUpTime; private ScheduledEventBase m_ControllerCheckEvent; private bool m_AllowInput = true; private bool m_Death = false; public Vector2 RawLookVector { set { m_RawLookVector = value; } } public Vector2 CurrentLookVector { set { m_CurrentLookVector = value; } } public bool ControllerConnected { get { return m_ControllerConnected; } } protected virtual bool CanCheckForController { get { return true; } } /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); if (m_LookVectorMode == LookVectorMode.Smoothed) { m_SmoothLookBuffer = new Vector2[m_SmoothLookSteps]; } EventHandler.RegisterEvent(gameObject, "OnCharacterChangeTimeScale", ChangeTimeScale); EventHandler.RegisterEvent(gameObject, "OnEnableGameplayInput", EnableGameplayInput); EventHandler.RegisterEvent(gameObject, "OnDeath", OnDeath); EventHandler.RegisterEvent(gameObject, "OnRespawn", OnRespawn); CheckForController(); } /// /// Returns true if the button is being pressed. /// /// The name of the button. /// True of the button is being pressed. public bool GetButton(string name) { return GetButtonInternal(name); } /// /// Internal method which returns true if the button is being pressed. /// /// The name of the button. /// True of the button is being pressed. protected virtual bool GetButtonInternal(string name) { return false; } /// /// Returns true if the button was pressed this frame. /// /// The name of the button. /// True if the button is pressed this frame. public bool GetButtonDown(string name) { return GetButtonDownInternal(name); } /// /// Internal method which returns true if the button was pressed this frame. /// /// The name of the button. /// True if the button is pressed this frame. protected virtual bool GetButtonDownInternal(string name) { return false; } /// /// Returns true if the button is up. /// /// The name of the button. /// True if the button is up. public bool GetButtonUp(string name) { return GetButtonUpInternal(name); } /// /// Internal method which returns true if the button is up. /// /// The name of the button. /// True if the button is up. protected virtual bool GetButtonUpInternal(string name) { return false; } /// /// Returns true if a double press occurred (double click or double tap). /// /// The button name to check for a double press. /// True if a double press occurred (double click or double tap). public bool GetDoublePress(string name) { if (GetButtonDown(name)) { if (m_ButtonDownTime == null) { m_ButtonDownTime = new Dictionary(); } var time = -1f; if (m_ButtonDownTime.TryGetValue(name, out time)) { if (time != Time.unscaledTime && time + 0.2f > Time.unscaledTime) { return true; } m_ButtonDownTime[name] = Time.unscaledTime; } else { m_ButtonDownTime.Add(name, Time.unscaledTime); } } return false; } /// /// Internal method which returns true if a double press occurred (double click or double tap). /// /// The button name to check for a double press. /// True if a double press occurred (double click or double tap). protected virtual bool GetDoublePressInternal(string name) { return false; } /// /// Returns true if a tap occurred. /// /// The button name to check for a tap. /// True if a tap occurred. public bool GetTap(string name) { var time = -1f; if (GetButton(name)) { if (m_ButtonDownTime == null) { m_ButtonDownTime = new Dictionary(); } if (!m_ButtonDownTime.ContainsKey(name)) { m_ButtonDownTime.Add(name, Time.unscaledTime); } } else if (m_ButtonDownTime != null && m_ButtonDownTime.TryGetValue(name, out time)) { m_ButtonDownTime.Remove(name); if (time != Time.unscaledTime && time + 0.2f > Time.unscaledTime) { return true; } } return false; } /// /// Returns true if a long press occurred. /// /// The button name to check for a long press. /// The duration of a long press. /// Indicates if the long press should occur after the button has been released (true) or after the duration (false). /// True if a long press occurred. public bool GetLongPress(string name, float duration, bool waitForRelease) { // Button down and up times won't be allocated unless double or long press inputs are used. if (m_ButtonDownTime == null) { m_ButtonDownTime = new Dictionary(); m_ButtonUpTime = new Dictionary(); } if (GetButtonInternal(name)) { var downTime = -1f; if (m_ButtonDownTime.TryGetValue(name, out downTime)) { // Only set the down time if the up time is greater than the down time. This will prevent the current time from being set every tick. var upTime = -1f; m_ButtonUpTime.TryGetValue(name, out upTime); if (upTime > downTime) { m_ButtonDownTime[name] = downTime = Time.unscaledTime; } // Return true as soon as the button has been pressed for the duration. if (!waitForRelease) { return downTime + duration <= Time.unscaledTime; } } else { m_ButtonDownTime.Add(name, Time.unscaledTime); } } else { var upTime = -1f; if (m_ButtonUpTime.TryGetValue(name, out upTime)) { // Only set the up time if the down time is greater than the up time. This will prevent the current time from being set every tick. var downTime = -1f; m_ButtonDownTime.TryGetValue(name, out downTime); if (downTime > upTime) { m_ButtonUpTime[name] = upTime = Time.unscaledTime; if (waitForRelease) { return downTime + duration <= Time.unscaledTime; } } } else { m_ButtonUpTime.Add(name, Time.unscaledTime); } } return false; } /// /// Returns the value of the axis with the specified name. /// /// The name of the axis. /// The value of the axis. public float GetAxis(string name) { return GetAxisInternal(name); } /// /// Internal method which returns the value of the axis with the specified name. /// /// The name of the axis. /// The value of the axis. protected virtual float GetAxisInternal(string name) { return 0; } /// /// Returns the value of the raw axis with the specified name. /// /// The name of the axis. /// The value of the raw axis. public float GetAxisRaw(string name) { return GetAxisRawInternal(name); } /// /// Returns the value of the raw axis with the specified name. /// /// The name of the axis. /// The value of the raw axis. protected virtual float GetAxisRawInternal(string name) { return 0; } /// /// Is a controller connected? /// /// True if a controller is connected. public bool IsControllerConnected() { return m_ControllerConnected; } /// /// Is the cursor visible? /// /// True if the cursor is visible. public bool IsCursorVisible() { return Cursor.visible; } /// /// Returns the position of the mouse. /// /// The mouse position. public virtual Vector2 GetMousePosition() { return Input.mousePosition; } /// /// Determines if a controller is connected. /// private void CheckForController() { if (!CanCheckForController) { return; } var controllerConencted = Input.GetJoystickNames().Length > 0; if (m_ControllerConnected != controllerConencted) { m_ControllerConnected = controllerConencted; if (!string.IsNullOrEmpty(m_ConnectedControllerState)) { StateManager.SetState(gameObject, m_ConnectedControllerState, m_ControllerConnected); } EventHandler.ExecuteEvent(gameObject, "OnInputControllerConnected", m_ControllerConnected); } // Schedule the controller check event if the rate is positive. // UnityEngine.Input.GetJoystickNames generates garbage so limit the amount of time the controller is checked. if (m_ControllerConnectedCheckRate > 0 && (m_ControllerCheckEvent == null || !m_ControllerCheckEvent.Active)) { m_ControllerCheckEvent = Scheduler.Schedule(m_ControllerConnectedCheckRate, CheckForController); } } /// /// Updates the look smoothing buffer to the current look vector. /// private void FixedUpdate() { if (!Application.isFocused) { return; } m_RawLookVector.x = GetAxisRaw(m_HorizontalLookInputName); m_RawLookVector.y = GetAxisRaw(m_VerticalLookInputName); if (m_LookVectorMode == LookVectorMode.Smoothed) { // Set the current input to the look buffer. m_SmoothLookBuffer[m_SmoothLookBufferIndex].x = m_RawLookVector.x; m_SmoothLookBuffer[m_SmoothLookBufferIndex].y = m_RawLookVector.y; if (m_SmoothLookBufferCount < m_SmoothLookBufferIndex + 1) { m_SmoothLookBufferCount = m_SmoothLookBufferIndex + 1; } // Calculate the input smoothing value. The more recent the input value occurred the higher the influence it has on the final smoothing value. var weight = 1f; var average = Vector2.zero; var averageTotal = 0f; var deltaTime = m_TimeScale * TimeUtility.FramerateDeltaTime; for (int i = 0; i < m_SmoothLookBufferCount; ++i) { var index = m_SmoothLookBufferIndex - i; if (index < 0) { index = m_SmoothLookBufferCount + m_SmoothLookBufferIndex - i; } average += m_SmoothLookBuffer[index] * weight; averageTotal += weight; // The deltaTime will be 0 if Unity just started to play after stepping through the editor. if (deltaTime > 0) { weight *= (m_SmoothLookWeight / deltaTime); } } m_SmoothLookBufferIndex = (m_SmoothLookBufferIndex + 1) % m_SmoothLookBuffer.Length; // Store the averaged input value. averageTotal = Mathf.Max(1, averageTotal); m_CurrentLookVector = average / averageTotal; // Apply any look acceleration. The delta time will be zero on the very first frame. var lookAcceleration = 0f; if (m_LookAccelerationThreshold > 0 && deltaTime != 0) { var accX = Mathf.Abs(m_CurrentLookVector.x); var accY = Mathf.Abs(m_CurrentLookVector.y); lookAcceleration = Mathf.Sqrt((accX * accX) + (accY * accY)) / deltaTime; if (lookAcceleration > m_LookAccelerationThreshold) { lookAcceleration = m_LookAccelerationThreshold; } } // Determine the final value. m_CurrentLookVector.x *= ((m_LookSensitivity.x * m_LookSensitivityMultiplier) + lookAcceleration) * TimeUtility.FramerateDeltaTime; m_CurrentLookVector.y *= ((m_LookSensitivity.y * m_LookSensitivityMultiplier) + lookAcceleration) * TimeUtility.FramerateDeltaTime; m_CurrentLookVector.x = Mathf.Sign(m_CurrentLookVector.x) * Mathf.Pow(Mathf.Abs(m_CurrentLookVector.x), m_SmoothExponent); m_CurrentLookVector.y = Mathf.Sign(m_CurrentLookVector.y) * Mathf.Pow(Mathf.Abs(m_CurrentLookVector.y), m_SmoothExponent); } else if (m_LookVectorMode == LookVectorMode.UnitySmoothed) { m_CurrentLookVector.x = GetAxis(m_HorizontalLookInputName); m_CurrentLookVector.y = GetAxis(m_VerticalLookInputName); } else if (m_LookVectorMode == LookVectorMode.Raw) { m_CurrentLookVector = m_RawLookVector; } } /// /// Returns the look vector. Will apply smoothing if specified otherwise will return the GetAxis value. /// /// Should the smoothing value be returned? If false the raw look vector will be returned. /// The current look vector. public virtual Vector2 GetLookVector(bool smoothed) { if (smoothed) { return m_CurrentLookVector; } return m_RawLookVector; } /// /// Returns true if the pointer is over a UI element. /// /// True if the pointer is over a UI element. public virtual bool IsPointerOverUI() { if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) { return true; } return false; } /// /// The character's local timescale has changed. /// /// The new timescale. private void ChangeTimeScale(float timeScale) { m_TimeScale = timeScale; } /// /// Enables or disables gameplay input. An example of when it will not be enabled is when there is a fullscreen UI over the main camera. /// /// True if the input is enabled. protected virtual void EnableGameplayInput(bool enable) { m_AllowInput = enable; enabled = m_AllowInput && !m_Death; if (enabled && !Application.isFocused) { OnApplicationFocus(true); } else if (!enabled) { m_RawLookVector = m_CurrentLookVector = Vector3.zero; } if (m_EnableGamplayInputEvent != null) { m_EnableGamplayInputEvent.Invoke(enable); } } /// /// The character has died. Disable the component. /// /// The position of the force. /// The amount of force which killed the character. /// The GameObject that killed the character. private void OnDeath(Vector3 position, Vector3 force, GameObject attacker) { m_Death = true; enabled = m_AllowInput && !m_Death; } /// /// The character has respawned. Enable the component. /// private void OnRespawn() { m_Death = false; enabled = m_AllowInput && !m_Death; } /// /// Does the game have focus? /// /// True if the game has focus. protected virtual void OnApplicationFocus(bool hasFocus) { if (Application.isFocused) { CheckForController(); } else { m_CurrentLookVector = Vector3.zero; } } /// /// The GameObject has been destroyed. /// private void OnDestroy() { EventHandler.UnregisterEvent(gameObject, "OnCharacterChangeTimeScale", ChangeTimeScale); EventHandler.UnregisterEvent(gameObject, "OnEnableGameplayInput", EnableGameplayInput); EventHandler.UnregisterEvent(gameObject, "OnDeath", OnDeath); EventHandler.UnregisterEvent(gameObject, "OnRespawn", OnRespawn); if (m_ControllerCheckEvent != null) { Scheduler.Cancel(m_ControllerCheckEvent); m_ControllerCheckEvent = null; } } } }