diff --git a/README.md b/README.md index ab576719..ddbce14c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ MonoGame.Extended is designed to build on top of MonoGame, but may work with XNA - **Tile based maps** using [Tiled](http://www.mapeditor.org/) (orthogonal only in v0.3, isometric in master) - **Bitmap fonts** using [BMFont](http://www.angelcode.com/products/bmfont/) - **[Sprites](http://dylanwilson.net/sprites-and-spritebatch-extensions-in-monogame-extended)** (with SpriteBatch extensions!) -- **Input listeners** for event driven input handling (Keyboard, Mouse, Touch) +- **Input listeners** for event driven input handling (Keyboard, Mouse, Touch) (GamePad in master) - **Texture Atlases** using the JSON format in [TexturePacker](https://www.codeandweb.com/texturepacker) - **2D Camera** with pan, zoom, and rotation - **Viewport Adapters** for resolution independent rendering diff --git a/Source/MonoGame.Extended/InputListeners/GamePadEventArgs.cs b/Source/MonoGame.Extended/InputListeners/GamePadEventArgs.cs new file mode 100644 index 00000000..ced422ee --- /dev/null +++ b/Source/MonoGame.Extended/InputListeners/GamePadEventArgs.cs @@ -0,0 +1,62 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.InputListeners +{ + /// + /// This class contains all information resulting from events fired by + /// . + /// + public class GamePadEventArgs : EventArgs + { + internal GamePadEventArgs(GamePadState previousState, GamePadState currentState, + TimeSpan elapsedTime, PlayerIndex playerIndex, Buttons? button = null, + float triggerState = 0, Vector2? thumbStickState = null) + { + PlayerIndex = playerIndex; + PreviousState = previousState; + CurrentState = currentState; + ElapsedTime = elapsedTime; + if (button != null) + Button = button.Value; + TriggerState = triggerState; + ThumbStickState = thumbStickState ?? Vector2.Zero; + } + + /// + /// The index of the controller. + /// + public PlayerIndex PlayerIndex { get; private set; } + + /// + /// The state of the controller in the previous update. + /// + public GamePadState PreviousState { get; private set; } + + /// + /// The state of the controller in this update. + /// + public GamePadState CurrentState { get; private set; } + + /// + /// The button that triggered this event, if appliable. + /// + public Buttons Button { get; private set; } + + /// + /// The time elapsed since last event. + /// + public TimeSpan ElapsedTime { get; private set; } + + /// + /// If a TriggerMoved event, displays the responsible trigger's position. + /// + public float TriggerState { get; private set; } + + /// + /// If a ThumbStickMoved event, displays the responsible stick's position. + /// + public Vector2 ThumbStickState { get; private set; } + } +} diff --git a/Source/MonoGame.Extended/InputListeners/GamePadListener.cs b/Source/MonoGame.Extended/InputListeners/GamePadListener.cs new file mode 100644 index 00000000..a5fc45bc --- /dev/null +++ b/Source/MonoGame.Extended/InputListeners/GamePadListener.cs @@ -0,0 +1,486 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGame.Extended.InputListeners +{ + /// + /// This is a listener that exposes several events for easier handling of gamepads. + /// In order to initialise it, call the method + /// with a object. + /// + public class GamePadListener : InputListener + { + internal GamePadListener(GamePadListenerSettings settings) + { + PlayerIndex = settings.PlayerIndex; + VibrationEnabled = settings.VibrationEnabled; + VibrationStrengthLeft = settings.VibrationStrengthLeft; + VibrationStrengthRight = settings.VibrationStrengthRight; + ThumbStickDeltaTreshold = settings.ThumbStickDeltaTreshold; + ThumbstickDownTreshold = settings.ThumbstickDownTreshold; + TriggerDeltaTreshold = settings.TriggerDeltaTreshold; + TriggerDownTreshold = settings.TriggerDownTreshold; + RepeatInitialDelay = settings.RepeatInitialDelay; + RepeatDelay = settings.RepeatDelay; + + _previousGameTime = new GameTime(); + _previousState = GamePadState.Default; + } + + #region events + + /// + /// This event fires whenever a controller connects or disconnects. In order + /// for it to work, the property must + /// be set to true. + /// + public static event EventHandler ControllerConnectionChanged; + + /// + /// This event fires whenever a button changes from the Up + /// to the Down state. + /// + public event EventHandler ButtonDown; + + /// + /// This event fires whenever a button changes from the Down + /// to the Up state. + /// + public event EventHandler ButtonUp; + + /// + /// This event fires repeatedly whenever a button is held sufficiently + /// long. Use this for things like menu navigation. + /// + public event EventHandler ButtonRepeated; + + /// + /// This event fires whenever a thumbstick changes position. + /// The parameter governing the sensitivity of this functionality + /// is . + /// + public event EventHandler ThumbStickMoved; + + /// + /// This event fires whenever a trigger changes position. + /// The parameter governing the sensitivity of this functionality + /// is . + /// + public event EventHandler TriggerMoved; + + #endregion + + #region public properties + + /// + /// If set to true, the static event + /// will fire when any controller changes in connectivity status. + /// This functionality requires that you have one actively updating + /// . + /// + public static bool CheckControllerConnections { get; set; } + + /// + /// The index of the controller. + /// + public PlayerIndex PlayerIndex { get; private set; } + + /// + /// When a button is held down, the interval in which + /// ButtonRepeated fires. Value in milliseconds. + /// + public int RepeatDelay { get; private set; } + + /// + /// The amount of time a button has to be held down + /// in order to fire ButtonRepeated the first time. + /// Value in milliseconds. + /// + public int RepeatInitialDelay { get; private set; } + + /// + /// Whether vibration is enabled for this controller. + /// + public bool VibrationEnabled { get; set; } + + /// + /// General setting for the strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// + public float VibrationStrengthLeft + { + get { return _vibrationStrengthLeft; } + // Clamp the value, just to be sure. + set { _vibrationStrengthLeft = MathHelper.Clamp(value, 0, 1); } + } + + /// + /// General setting for the strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// + public float VibrationStrengthRight + { + get { return _vibrationStrengthRight; } + // Clamp the value, just to be sure. + set { _vibrationStrengthRight = MathHelper.Clamp(value, 0, 1); } + } + + /// + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the trigger's + /// updated position. + /// In essence this defines the event's + /// resolution. + /// At a value of 0 this will fire every time + /// the trigger's position is not 0f. + /// + public float TriggerDeltaTreshold { get; private set; } + + /// + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the thumbstick's + /// updated position. + /// In essence this defines the event's + /// resolution. + /// At a value of 0 this will fire every time + /// the thumbstick's position is not {x:0, y:0}. + /// + public float ThumbStickDeltaTreshold { get; private set; } + + /// + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// + public float TriggerDownTreshold { get; private set; } + + /// + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// + public float ThumbstickDownTreshold { get; private set; } + + #endregion + + #region private fields + private static readonly bool[] GamePadConnections = new bool[4]; + + private float _vibrationStrengthLeft; + private float _vibrationStrengthRight; + + private GamePadState _currentState; + private GamePadState _previousState; + //private int _lastPacketNumber; + // Implementation doesn't work, see explanation in CheckAllButtons(). + private GameTime _gameTime; + private GameTime _previousGameTime; + private Buttons _lastButton; + private int _repeatedButtonTimer; + + private GamePadState _lastTriggerState; + private GamePadState _lastThumbStickState; + private bool _leftTriggerDown; + private bool _rightTriggerDown; + private bool _leftStickDown; + private bool _rightStickDown; + private Buttons _lastRightStickDirection; + private Buttons _lastLeftStickDirection; + + private float _leftCurVibrationStrength; + private float _rightCurVibrationStrength; + private TimeSpan _vibrationStart; + private TimeSpan _vibrationDurationLeft; + private TimeSpan _vibrationDurationRight; + private bool _leftVibrating; + private bool _rightVibrating; + + // These buttons are not to be evaluated normally, but with the debounce filter + // in their respective methods. + private readonly Buttons[] _excludedButtons = + { + Buttons.LeftTrigger, Buttons.RightTrigger, + Buttons.LeftThumbstickDown, Buttons.LeftThumbstickUp, Buttons.LeftThumbstickRight, Buttons.LeftThumbstickLeft, + Buttons.RightThumbstickLeft, Buttons.RightThumbstickRight, Buttons.RightThumbstickUp, Buttons.RightThumbstickDown + }; + + #endregion + + + /// + /// Send a vibration command to the controller. + /// Returns true if the operation succeeded. + /// Motor values that are unset preserve + /// their current vibration strength and duration. + /// Note: Vibration currently only works on select platforms, + /// like Monogame.Windows. + /// + /// Duration of the vibration in milliseconds. + /// The strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// The strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// Returns true if the operation succeeded. + public bool Vibrate(int durationMs, float leftStrength = float.NegativeInfinity, + float rightStrength = float.NegativeInfinity) + { + if (!VibrationEnabled) + return false; + + var lstrength = MathHelper.Clamp(leftStrength, 0, 1); + var rstrength = MathHelper.Clamp(rightStrength, 0, 1); + + if (float.IsNegativeInfinity(leftStrength)) + lstrength = _leftCurVibrationStrength; + if (float.IsNegativeInfinity(rightStrength)) + rstrength = _rightCurVibrationStrength; + + var success = GamePad.SetVibration(PlayerIndex, lstrength * VibrationStrengthLeft, + rstrength * VibrationStrengthRight); + if (success) + { + _leftVibrating = true; + _rightVibrating = true; + + if (leftStrength > 0) + _vibrationDurationLeft = new TimeSpan(0, 0, 0, 0, durationMs); + else if (lstrength > 0) + _vibrationDurationLeft -= _gameTime.TotalGameTime - _vibrationStart; + else + _leftVibrating = false; + + if (rightStrength > 0) + _vibrationDurationRight = new TimeSpan(0, 0, 0, 0, durationMs); + else if (rstrength > 0) + _vibrationDurationRight -= _gameTime.TotalGameTime - _vibrationStart; + else + _rightVibrating = false; + + _vibrationStart = _gameTime.TotalGameTime; + + _leftCurVibrationStrength = lstrength; + _rightCurVibrationStrength = rstrength; + } + return success; + } + + private void CheckAllButtons() + { + // PacketNumber only and always changes if there is a difference between GamePadStates. + // ...At least, that's the theory. It doesn't seem to be implemented. Disabled for now. + //if (_lastPacketNumber == _currentState.PacketNumber) + // return; + foreach (Buttons button in Enum.GetValues(typeof (Buttons))) + { + if (_excludedButtons.Contains(button)) + break; + if (_currentState.IsButtonDown(button) && _previousState.IsButtonUp(button)) + RaiseButtonDown(button); + if (_currentState.IsButtonUp(button) && _previousState.IsButtonDown(button)) + RaiseButtonUp(button); + } + + // Checks triggers as buttons and floats + CheckTriggers(s => s.Triggers.Left, Buttons.LeftTrigger); + CheckTriggers(s => s.Triggers.Right, Buttons.RightTrigger); + + // Checks thumbsticks as vector2s + CheckThumbSticks(s => s.ThumbSticks.Right, Buttons.RightStick); + CheckThumbSticks(s => s.ThumbSticks.Left, Buttons.LeftStick); + } + + private void CheckTriggers(Func getButtonState, Buttons button) + { + float debounce = 0.05f; // Value used to qualify a trigger as coming Up from a Down state + var curstate = getButtonState(_currentState); + bool curdown = curstate > TriggerDownTreshold; + bool prevdown = button == Buttons.RightTrigger ? _rightTriggerDown : _leftTriggerDown; + + if (!prevdown && curdown) + { + RaiseButtonDown(button); + if (button == Buttons.RightTrigger) + _rightTriggerDown = true; + else + _leftTriggerDown = true; + + } + else if (prevdown && curstate < debounce) + { + RaiseButtonUp(button); + if (button == Buttons.RightTrigger) + _rightTriggerDown = false; + else + _leftTriggerDown = false; + } + + var prevstate = getButtonState(_lastTriggerState); + if (curstate > TriggerDeltaTreshold) + { + if (Math.Abs(prevstate - curstate) >= TriggerDeltaTreshold) + { + TriggerMoved.Raise(this, MakeArgs(button, triggerstate: curstate)); + _lastTriggerState = _currentState; + } + } + else if (prevstate > TriggerDeltaTreshold) + { + TriggerMoved.Raise(this, MakeArgs(button, triggerstate: curstate)); + _lastTriggerState = _currentState; + } + } + + private void CheckThumbSticks(Func getButtonState, Buttons button) + { + const float debounce = 0.15f; + var curVector = getButtonState(_currentState); + bool curdown = curVector.Length() > ThumbstickDownTreshold; + bool right = button == Buttons.RightStick; + bool prevdown = right ? _rightStickDown : _leftStickDown; + + var prevdir = button == Buttons.RightStick ? _lastRightStickDirection : _lastLeftStickDirection; + Buttons curdir; + if (curVector.Y > curVector.X) + { + if (curVector.Y > -curVector.X) + curdir = right ? Buttons.RightThumbstickUp : Buttons.LeftThumbstickUp; + else + curdir = right ? Buttons.RightThumbstickLeft : Buttons.LeftThumbstickLeft; + } + else + { + if (curVector.Y < -curVector.X) + curdir = right ? Buttons.RightThumbstickDown : Buttons.LeftThumbstickDown; + else + curdir = right ? Buttons.RightThumbstickRight : Buttons.LeftThumbstickRight; + } + + if (!prevdown && curdown) + { + if (right) + _lastRightStickDirection = curdir; + else + _lastLeftStickDirection = curdir; + + RaiseButtonDown(curdir); + if (button == Buttons.RightStick) + _rightStickDown = true; + else + _leftStickDown = true; + } + else if (prevdown && curVector.Length() < debounce) + { + RaiseButtonUp(prevdir); + if (button == Buttons.RightStick) + _rightStickDown = false; + else + _leftStickDown = false; + } + else if (prevdown && curdown && curdir != prevdir) + { + RaiseButtonUp(prevdir); + if (right) + _lastRightStickDirection = curdir; + else + _lastLeftStickDirection = curdir; + RaiseButtonDown(curdir); + } + + var prevVector = getButtonState(_lastThumbStickState); + if (curVector.Length() > ThumbStickDeltaTreshold) + { + if (Vector2.Distance(curVector, prevVector) >= ThumbStickDeltaTreshold) + { + ThumbStickMoved.Raise(this, MakeArgs(button, thumbStickState: curVector)); + _lastThumbStickState = _currentState; + } + } else if (prevVector.Length() > ThumbStickDeltaTreshold) + { + ThumbStickMoved.Raise(this, MakeArgs(button, thumbStickState: curVector)); + _lastThumbStickState = _currentState; + } + } + + internal static void CheckConnections() + { + if (!CheckControllerConnections) + return; + + foreach (PlayerIndex index in Enum.GetValues(typeof(PlayerIndex))) + { + if (GamePad.GetState(index).IsConnected ^ GamePadConnections[(int) index]) // We need more XORs in this world + { + GamePadConnections[(int) index] = !GamePadConnections[(int) index]; + ControllerConnectionChanged.Raise(null, + new GamePadEventArgs(GamePadState.Default, GamePad.GetState(index), TimeSpan.Zero, index)); + } + } + } + + private void CheckVibrate() + { + if (_leftVibrating && _vibrationStart + _vibrationDurationLeft < _gameTime.TotalGameTime) + Vibrate(0, leftStrength: 0); + if (_rightVibrating && _vibrationStart + _vibrationDurationRight < _gameTime.TotalGameTime) + Vibrate(0, rightStrength: 0); + } + + internal override void Update(GameTime gameTime) + { + _gameTime = gameTime; + _currentState = GamePad.GetState(PlayerIndex); + CheckVibrate(); + if (!_currentState.IsConnected) + return; + CheckAllButtons(); + CheckRepeatButton(); + //_lastPacketNumber = _currentState.PacketNumber; + _previousGameTime = gameTime; + _previousState = _currentState; + } + private GamePadEventArgs MakeArgs(Buttons? button, + float triggerstate = 0, Vector2? thumbStickState = null) + { + var elapsedTime = _gameTime.TotalGameTime - _previousGameTime.TotalGameTime; + return new GamePadEventArgs(_previousState, _currentState, + elapsedTime, PlayerIndex, button, triggerstate, thumbStickState); + } + + private void RaiseButtonDown(Buttons button) + { + ButtonDown.Raise(this, MakeArgs(button)); + ButtonRepeated.Raise(this, MakeArgs(button)); + _lastButton = button; + _repeatedButtonTimer = 0; + } + + private void RaiseButtonUp(Buttons button) + { + ButtonUp.Raise(this, MakeArgs(button)); + _lastButton = 0; + } + + private void CheckRepeatButton() + { + _repeatedButtonTimer += _gameTime.ElapsedGameTime.Milliseconds; + + if(_repeatedButtonTimer < RepeatInitialDelay || _lastButton == 0) + return; + + if (_repeatedButtonTimer < RepeatInitialDelay + RepeatDelay) + { + ButtonRepeated.Raise(this, MakeArgs(_lastButton)); + _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay; + } + else if (_repeatedButtonTimer > RepeatInitialDelay + RepeatDelay*2) + { + ButtonRepeated.Raise(this, MakeArgs(_lastButton)); + _repeatedButtonTimer = RepeatDelay + RepeatInitialDelay; + } + } + } +} diff --git a/Source/MonoGame.Extended/InputListeners/GamePadListenerSettings.cs b/Source/MonoGame.Extended/InputListeners/GamePadListenerSettings.cs new file mode 100644 index 00000000..33b53892 --- /dev/null +++ b/Source/MonoGame.Extended/InputListeners/GamePadListenerSettings.cs @@ -0,0 +1,121 @@ +using Microsoft.Xna.Framework; + +namespace MonoGame.Extended.InputListeners +{ + /// + /// This is a class that contains settings to be used to initialise a . + /// + /// + public class GamePadListenerSettings : InputListenerSettings + { + /// + /// This is a class that contains settings to be used to initialise a . + /// Note: There are a number of extra settings that are settable properties. + /// + /// The index of the controller the listener will be tied to. + /// Whether vibration is enabled on the controller. + /// General setting for the strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// General setting for the strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// This setting will modify all future vibrations + /// through this listener. + public GamePadListenerSettings(PlayerIndex playerIndex, bool vibrationEnabled = true, + float vibrationStrengthLeft = 1.0f, float vibrationStrengthRight = 1.0f) + { + PlayerIndex = playerIndex; + VibrationEnabled = vibrationEnabled; + VibrationStrengthLeft = vibrationStrengthLeft; + VibrationStrengthRight = vibrationStrengthRight; + TriggerDownTreshold = 0.15f; + ThumbstickDownTreshold = 0.5f; + RepeatInitialDelay = 500; + RepeatDelay = 50; + } + + /// + /// The index of the controller. + /// + public PlayerIndex PlayerIndex { get; set; } + + /// + /// When a button is held down, the interval in which + /// ButtonRepeated fires. Value in milliseconds. + /// + public int RepeatDelay { get; set; } + + /// + /// The amount of time a button has to be held down + /// in order to fire ButtonRepeated the first time. + /// Value in milliseconds. + /// + public int RepeatInitialDelay { get; set; } + + + /// + /// Whether vibration is enabled for this controller. + /// + public bool VibrationEnabled { get; set; } + + /// + /// General setting for the strength of the left motor. + /// This motor has a slow, deep, powerful rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// + public float VibrationStrengthLeft { get; set; } + + /// + /// General setting for the strength of the right motor. + /// This motor has a snappy, quick, high-pitched rumble. + /// This setting will modify all future vibrations + /// through this listener. + /// + public float VibrationStrengthRight { get; set; } + + /// + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the trigger's + /// updated position. + /// In essence this defines the event's + /// resolution. + /// At a value of 0 this will fire every time + /// the trigger's position is not 0f. + /// + public float TriggerDeltaTreshold { get; set; } + + /// + /// The treshold of movement that has to be met in order + /// for the listener to fire an event with the thumbstick's + /// updated position. + /// In essence this defines the event's + /// resolution. + /// At a value of 0 this will fire every time + /// the thumbstick's position is not {x:0, y:0}. + /// + public float ThumbStickDeltaTreshold { get; set; } + + /// + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// + public float TriggerDownTreshold { get; set; } + + /// + /// How deep the triggers have to be depressed in order to + /// register as a ButtonDown event. + /// + public float ThumbstickDownTreshold { get; private set; } + + internal override GamePadListener CreateListener() + { + return new GamePadListener(this); + } + + // For internal use only. This limits the user to only having + // one controller. + internal GamePadListenerSettings() : this(PlayerIndex.One) { } + } +} diff --git a/Source/MonoGame.Extended/InputListeners/InputListenerManager.cs b/Source/MonoGame.Extended/InputListeners/InputListenerManager.cs index 8d01f2ff..f80c46a6 100644 --- a/Source/MonoGame.Extended/InputListeners/InputListenerManager.cs +++ b/Source/MonoGame.Extended/InputListeners/InputListenerManager.cs @@ -62,6 +62,8 @@ namespace MonoGame.Extended.InputListeners { foreach (var listener in _listeners) listener.Update(gameTime); + + GamePadListener.CheckConnections(); } } }