using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; using MLEM.Misc; namespace MLEM.Input { /// /// An input handler is a more advanced wrapper around MonoGame's default input system. /// It includes keyboard, mouse, gamepad and touch states, as well as a new "pressed" state for keys and the ability for keyboard and gamepad repeat events. /// public class InputHandler : GameComponent { /// /// Contains the keyboard state from the last update call /// public KeyboardState LastKeyboardState { get; private set; } /// /// Contains the current keyboard state /// public KeyboardState KeyboardState { get; private set; } /// /// Contains the keyboard keys that are currently being pressed /// public Keys[] PressedKeys { get; private set; } /// /// Set this property to false to disable keyboard handling for this input handler. /// public bool HandleKeyboard; /// /// Contains the mouse state from the last update call /// public MouseState LastMouseState { get; private set; } /// /// Contains the current mouse state /// public MouseState MouseState { get; private set; } /// /// Contains the current position of the mouse, extracted from /// public Point MousePosition => this.MouseState.Position; /// /// Contains the position of the mouse from the last update call, extracted from /// public Point LastMousePosition => this.LastMouseState.Position; /// /// Contains the current scroll wheel value, in increments of 120 /// public int ScrollWheel => this.MouseState.ScrollWheelValue; /// /// Contains the scroll wheel value from the last update call, in increments of 120 /// public int LastScrollWheel => this.LastMouseState.ScrollWheelValue; /// /// Set this property to false to disable mouse handling for this input handler. /// public bool HandleMouse; private readonly GamePadState[] lastGamepads = new GamePadState[GamePad.MaximumGamePadCount]; private readonly GamePadState[] gamepads = new GamePadState[GamePad.MaximumGamePadCount]; /// /// Contains the amount of gamepads that are currently connected. /// This property is automatically updated in /// public int ConnectedGamepads { get; private set; } /// /// Set this property to false to disable keyboard handling for this input handler. /// public bool HandleGamepads; /// /// Contains the touch state from the last update call /// public TouchCollection LastTouchState { get; private set; } /// /// Contains the current touch state /// public TouchCollection TouchState { get; private set; } /// /// Contains all of the gestures that have finished during the last update call. /// To easily query these gestures, use /// public readonly ReadOnlyCollection Gestures; private readonly List gestures = new List(); /// /// Set this property to false to disable touch handling for this input handler. /// public bool HandleTouch; /// /// This is the amount of time that has to pass before the first keyboard repeat event is triggered. /// /// public TimeSpan KeyRepeatDelay = TimeSpan.FromSeconds(0.65); /// /// This is the amount of time that has to pass between keyboard repeat events. /// /// public TimeSpan KeyRepeatRate = TimeSpan.FromSeconds(0.05); /// /// Set this property to false to disable keyboard repeat event handling. /// public bool HandleKeyboardRepeats = true; private DateTime heldKeyStart; private DateTime lastKeyRepeat; private bool triggerKeyRepeat; private Keys heldKey; /// /// Set this property to false to disable gamepad repeat event handling. /// public bool HandleGamepadRepeats = true; private readonly DateTime[] heldGamepadButtonStarts = new DateTime[GamePad.MaximumGamePadCount]; private readonly DateTime[] lastGamepadButtonRepeats = new DateTime[GamePad.MaximumGamePadCount]; private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount]; private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount]; /// /// Creates a new input handler with optional initial values. /// /// The game instance that this input handler belongs to /// If keyboard input should be handled /// If mouse input should be handled /// If gamepad input should be handled /// If touch input should be handled public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true) : base(game) { this.HandleKeyboard = handleKeyboard; this.HandleMouse = handleMouse; this.HandleGamepads = handleGamepads; this.HandleTouch = handleTouch; this.Gestures = this.gestures.AsReadOnly(); } /// /// Updates this input handler, querying pressed and released keys and calculating repeat events. /// Call this in your method. /// public void Update() { var active = this.Game.IsActive; if (this.HandleKeyboard) { this.LastKeyboardState = this.KeyboardState; this.KeyboardState = active ? Keyboard.GetState() : default; this.PressedKeys = this.KeyboardState.GetPressedKeys(); if (this.HandleKeyboardRepeats) { this.triggerKeyRepeat = false; if (this.heldKey == Keys.None) { // if we're not repeating a key, set the first key being held to the repeat key // note that modifier keys don't count as that wouldn't really make sense var key = this.PressedKeys.FirstOrDefault(k => !k.IsModifier()); if (key != Keys.None) { this.heldKey = key; this.heldKeyStart = DateTime.UtcNow; } } else { // if the repeating key isn't being held anymore, reset if (!this.IsKeyDown(this.heldKey)) { this.heldKey = Keys.None; } else { var now = DateTime.UtcNow; var holdTime = now - this.heldKeyStart; // if we've been holding the key longer than the initial delay... if (holdTime >= this.KeyRepeatDelay) { var diff = now - this.lastKeyRepeat; // and we've been holding it for longer than a repeat... if (diff >= this.KeyRepeatRate) { this.lastKeyRepeat = now; // then trigger a repeat, causing IsKeyPressed to be true once this.triggerKeyRepeat = true; } } } } } } if (this.HandleMouse) { this.LastMouseState = this.MouseState; var state = Mouse.GetState(); if (active && this.Game.GraphicsDevice.Viewport.Bounds.Contains(state.Position)) { this.MouseState = state; } else { // mouse position and scroll wheel value should be preserved when the mouse is out of bounds this.MouseState = new MouseState(state.X, state.Y, state.ScrollWheelValue, 0, 0, 0, 0, 0, state.HorizontalScrollWheelValue); } } if (this.HandleGamepads) { this.ConnectedGamepads = GamePad.MaximumGamePadCount; for (var i = 0; i < GamePad.MaximumGamePadCount; i++) { this.lastGamepads[i] = this.gamepads[i]; this.gamepads[i] = active ? GamePad.GetState(i) : default; if (this.ConnectedGamepads > i && !GamePad.GetCapabilities(i).IsConnected) this.ConnectedGamepads = i; } if (this.HandleGamepadRepeats) { for (var i = 0; i < this.ConnectedGamepads; i++) { this.triggerGamepadButtonRepeat[i] = false; if (!this.heldGamepadButtons[i].HasValue) { foreach (var b in EnumHelper.Buttons) { if (this.IsGamepadButtonDown(b, i)) { this.heldGamepadButtons[i] = b; this.heldGamepadButtonStarts[i] = DateTime.UtcNow; break; } } } else { if (!this.IsGamepadButtonDown(this.heldGamepadButtons[i].Value, i)) { this.heldGamepadButtons[i] = null; } else { var now = DateTime.UtcNow; var holdTime = now - this.heldGamepadButtonStarts[i]; if (holdTime >= this.KeyRepeatDelay) { var diff = now - this.lastGamepadButtonRepeats[i]; if (diff >= this.KeyRepeatRate) { this.lastGamepadButtonRepeats[i] = now; this.triggerGamepadButtonRepeat[i] = true; } } } } } } } if (this.HandleTouch) { this.LastTouchState = this.TouchState; this.TouchState = active ? TouchPanel.GetState() : default; this.gestures.Clear(); while (active && TouchPanel.IsGestureAvailable) this.gestures.Add(TouchPanel.ReadGesture()); } } /// public override void Update(GameTime gameTime) { this.Update(); } /// /// Returns the state of the indexth gamepad from the last update call /// /// The zero-based gamepad index /// The state of the gamepad last update public GamePadState GetLastGamepadState(int index) { return this.lastGamepads[index]; } /// /// Returns the current state of the indexth gamepad /// /// The zero-based gamepad index /// The current state of the gamepad public GamePadState GetGamepadState(int index) { return this.gamepads[index]; } /// public bool IsKeyDown(Keys key) { return this.KeyboardState.IsKeyDown(key); } /// public bool IsKeyUp(Keys key) { return this.KeyboardState.IsKeyUp(key); } /// public bool WasKeyDown(Keys key) { return this.LastKeyboardState.IsKeyDown(key); } /// public bool WasKeyUp(Keys key) { return this.LastKeyboardState.IsKeyUp(key); } /// /// Returns whether the given key is considered pressed. /// A key is considered pressed if it was not down the last update call, but is down the current update call. /// If is true, this method will also return true to signify a key repeat. /// /// The key to query /// If the key is pressed public bool IsKeyPressed(Keys key) { // if the queried key is the held key and a repeat should be triggered, return true if (this.HandleKeyboardRepeats && key == this.heldKey && this.triggerKeyRepeat) return true; return this.IsKeyPressedIgnoreRepeats(key); } /// /// Returns whether the given key is considered pressed. /// This has the same behavior as , but ignores keyboard repeat events. /// If is false, this method does the same as . /// /// The key to query /// If the key is pressed public bool IsKeyPressedIgnoreRepeats(Keys key) { return this.WasKeyUp(key) && this.IsKeyDown(key); } /// /// Returns whether the given modifier key is down. /// /// The modifier key /// If the modifier key is down public bool IsModifierKeyDown(ModifierKey modifier) { return modifier.GetKeys().Any(this.IsKeyDown); } /// /// Returns whether the given mouse button is currently down. /// /// The button to query /// Whether or not the queried button is down public bool IsMouseButtonDown(MouseButton button) { return this.MouseState.GetState(button) == ButtonState.Pressed; } /// /// Returns whether the given mouse button is currently up. /// /// The button to query /// Whether or not the queried button is up public bool IsMouseButtonUp(MouseButton button) { return this.MouseState.GetState(button) == ButtonState.Released; } /// /// Returns whether the given mouse button was down the last update call. /// /// The button to query /// Whether or not the queried button was down public bool WasMouseButtonDown(MouseButton button) { return this.LastMouseState.GetState(button) == ButtonState.Pressed; } /// /// Returns whether the given mouse button was up the last update call. /// /// The button to query /// Whether or not the queried button was up public bool WasMouseButtonUp(MouseButton button) { return this.LastMouseState.GetState(button) == ButtonState.Released; } /// /// Returns whether the given mouse button is considered pressed. /// A mouse button is considered pressed if it was up the last update call, and is down the current update call. /// /// The button to query /// Whether the button is pressed public bool IsMouseButtonPressed(MouseButton button) { return this.WasMouseButtonUp(button) && this.IsMouseButtonDown(button); } /// public bool IsGamepadButtonDown(Buttons button, int index = -1) { if (index < 0) { for (var i = 0; i < this.ConnectedGamepads; i++) if (this.GetGamepadState(i).IsButtonDown(button)) return true; return false; } return this.GetGamepadState(index).IsButtonDown(button); } /// public bool IsGamepadButtonUp(Buttons button, int index = -1) { if (index < 0) { for (var i = 0; i < this.ConnectedGamepads; i++) if (this.GetGamepadState(i).IsButtonUp(button)) return true; return false; } return this.GetGamepadState(index).IsButtonUp(button); } /// public bool WasGamepadButtonDown(Buttons button, int index = -1) { if (index < 0) { for (var i = 0; i < this.ConnectedGamepads; i++) if (this.GetLastGamepadState(i).IsButtonDown(button)) return true; return false; } return this.GetLastGamepadState(index).IsButtonDown(button); } /// public bool WasGamepadButtonUp(Buttons button, int index = -1) { if (index < 0) { for (var i = 0; i < this.ConnectedGamepads; i++) if (this.GetLastGamepadState(i).IsButtonUp(button)) return true; return false; } return this.GetLastGamepadState(index).IsButtonUp(button); } /// /// Returns whether the given gamepad button on the given index is considered pressed. /// A gamepad button is considered pressed if it was down the last update call, and is up the current update call. /// If is true, this method will also return true to signify a gamepad button repeat. /// /// The button to query /// The zero-based index of the gamepad, or -1 for any gamepad /// Whether the given button is pressed public bool IsGamepadButtonPressed(Buttons button, int index = -1) { if (this.HandleGamepadRepeats) { if (index < 0) { for (var i = 0; i < this.ConnectedGamepads; i++) if (this.heldGamepadButtons[i] == button && this.triggerGamepadButtonRepeat[i]) return true; } else if (this.heldGamepadButtons[index] == button && this.triggerGamepadButtonRepeat[index]) { return true; } } return this.IsGamepadButtonPressedIgnoreRepeats(button, index); } /// /// Returns whether the given key is considered pressed. /// This has the same behavior as , but ignores gamepad repeat events. /// If is false, this method does the same as . /// /// The button to query /// The zero-based index of the gamepad, or -1 for any gamepad /// Whether the given button is pressed public bool IsGamepadButtonPressedIgnoreRepeats(Buttons button, int index = -1) { return this.WasGamepadButtonUp(button, index) && this.IsGamepadButtonDown(button, index); } /// /// Queries for a gesture of a given type that finished during the current update call. /// /// The type of gesture to query for /// The resulting gesture sample, or default if there isn't one /// True if a gesture of the type was found, otherwise false public bool GetGesture(GestureType type, out GestureSample sample) { foreach (var gesture in this.Gestures) { if (type.HasFlag(gesture.GestureType)) { sample = gesture; return true; } } return false; } /// /// Returns if a given control of any kind is down. /// This is a helper function that can be passed a , or . /// /// The control whose down state to query /// The index of the gamepad to query (if applicable), or -1 for any gamepad /// Whether the given control is down /// If the passed control isn't of a supported type public bool IsDown(GenericInput control, int index = -1) { switch (control.Type) { case GenericInput.InputType.Keyboard: return this.IsKeyDown(control); case GenericInput.InputType.Gamepad: return this.IsGamepadButtonDown(control, index); case GenericInput.InputType.Mouse: return this.IsMouseButtonDown(control); default: throw new ArgumentException(nameof(control)); } } /// /// Returns if a given control of any kind is up. /// This is a helper function that can be passed a , or . /// /// The control whose up state to query /// The index of the gamepad to query (if applicable), or -1 for any gamepad /// Whether the given control is down /// If the passed control isn't of a supported type public bool IsUp(GenericInput control, int index = -1) { switch (control.Type) { case GenericInput.InputType.Keyboard: return this.IsKeyUp(control); case GenericInput.InputType.Gamepad: return this.IsGamepadButtonUp(control, index); case GenericInput.InputType.Mouse: return this.IsMouseButtonUp(control); default: throw new ArgumentException(nameof(control)); } } /// /// Returns if a given control of any kind is pressed. /// This is a helper function that can be passed a , or . /// /// The control whose pressed state to query /// The index of the gamepad to query (if applicable), or -1 for any gamepad /// Whether the given control is down /// If the passed control isn't of a supported type public bool IsPressed(GenericInput control, int index = -1) { switch (control.Type) { case GenericInput.InputType.Keyboard: return this.IsKeyPressed(control); case GenericInput.InputType.Gamepad: return this.IsGamepadButtonPressed(control, index); case GenericInput.InputType.Mouse: return this.IsMouseButtonPressed(control); default: throw new ArgumentException(nameof(control)); } } /// public bool IsAnyDown(params GenericInput[] control) { return control.Any(c => this.IsDown(c)); } /// public bool IsAnyUp(params GenericInput[] control) { return control.Any(c => this.IsUp(c)); } /// public bool IsAnyPressed(params GenericInput[] control) { return control.Any(c => this.IsPressed(c)); } /// /// Helper function to enable gestures for a easily. /// Note that, if other gestures were previously enabled, they will not get overridden. /// /// The gestures to enable public static void EnableGestures(params GestureType[] gestures) { foreach (var gesture in gestures) TouchPanel.EnabledGestures |= gesture; } /// /// Helper function to disable gestures for a easily. /// /// The gestures to disable public static void DisableGestures(params GestureType[] gestures) { foreach (var gesture in gestures) TouchPanel.EnabledGestures &= ~gesture; } /// /// Helper function to enable or disable the given gestures for a easily. /// This method is equivalent to calling if the enabled value is true and calling if it is false. /// Note that, if other gestures were previously enabled, they will not get overridden. /// /// Whether to enable or disable the gestures /// The gestures to enable or disable public static void SetGesturesEnabled(bool enabled, params GestureType[] gestures) { if (enabled) { EnableGestures(gestures); } else { DisableGestures(gestures); } } } }