From c6fe72bdc9a439d1f20d95acd6c3623d021e61c6 Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Fri, 25 Mar 2022 14:19:03 +0100 Subject: [PATCH] Multiple improvements to InputHandler key/button repeats: - Trigger InputHandler key and gamepad repeats for the most recently pressed input - Added InputHandler.TryGetDownTime and store the down times of inputs - Removed InputHandler.StoreAllActiveInputs and always store all active inputs --- CHANGELOG.md | 3 + MLEM.Ui/UiControls.cs | 1 - MLEM/Input/GamepadExtensions.cs | 1 - MLEM/Input/InputHandler.cs | 161 ++++++++++++++++---------------- Sandbox/GameImpl.cs | 4 +- 5 files changed, 83 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3289c87..976b78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Additions - Added TextureRegion.OffsetCopy - Added RectangleF.DistanceSquared and RectangleF.Distance - Added GamepadExtensions.GetAnalogValue to get the analog value of any gamepad button +- Added InputHandler.TryGetDownTime Improvements - Generify GenericFont's string drawing @@ -24,6 +25,7 @@ Improvements - Allow LinkCode to specify a color to draw with - Allow better control over the order and layout of a Keybind's combinations - Allow setting a gamepad button deadzone in InputHandler +- Trigger InputHandler key and gamepad repeats for the most recently pressed input Fixes - **Fixed a formatting Code only knowing about the last Token that it is applied in** @@ -32,6 +34,7 @@ Fixes - Fixed InputHandler.InputsPressed ignoring repeat events for keyboards and gamepads Removals +- **Removed InputHandler.StoreAllActiveInputs and always store all active inputs** - Renamed GenericFont.OneEmSpace to Emsp (and marked OneEmSpace as obsolete) ### MLEM.Ui diff --git a/MLEM.Ui/UiControls.cs b/MLEM.Ui/UiControls.cs index 69cd112..5f5818d 100644 --- a/MLEM.Ui/UiControls.cs +++ b/MLEM.Ui/UiControls.cs @@ -4,7 +4,6 @@ using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input.Touch; -using MLEM.Extensions; using MLEM.Input; using MLEM.Misc; using MLEM.Ui.Elements; diff --git a/MLEM/Input/GamepadExtensions.cs b/MLEM/Input/GamepadExtensions.cs index 981458e..c746d12 100644 --- a/MLEM/Input/GamepadExtensions.cs +++ b/MLEM/Input/GamepadExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; diff --git a/MLEM/Input/InputHandler.cs b/MLEM/Input/InputHandler.cs index 8d0bf7b..d5e4491 100644 --- a/MLEM/Input/InputHandler.cs +++ b/MLEM/Input/InputHandler.cs @@ -56,10 +56,6 @@ namespace MLEM.Input { /// public bool HandleGamepadRepeats = true; /// - /// Set this field to false to enable and being calculated. - /// - public bool StoreAllActiveInputs; - /// /// This field represents the deadzone that gamepad have when input is queried for them using this input handler. /// A deadzone is the percentage (between 0 and 1) that an analog value has to exceed for it to be considered down () or pressed (). /// Querying of analog values is done using . @@ -68,13 +64,12 @@ namespace MLEM.Input { /// /// An array of all , and values that are currently down. - /// Note that this value only gets set if is true. + /// Additionally, or can be used to determine the amount of time that a given input has been down for. /// public GenericInput[] InputsDown { get; private set; } = Array.Empty(); /// /// An array of all , and that are currently considered pressed. /// An input is considered pressed if it was up in the last update, and is up in the current one. - /// Note that this value only gets set if is true. /// public GenericInput[] InputsPressed { get; private set; } = Array.Empty(); /// @@ -141,15 +136,14 @@ namespace MLEM.Input { private readonly GamePadState[] lastGamepads = new GamePadState[GamePad.MaximumGamePadCount]; private readonly GamePadState[] gamepads = new GamePadState[GamePad.MaximumGamePadCount]; - 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]; - private readonly List inputsDownAccum = new List(); private readonly List gestures = new List(); private Point ViewportOffset => new Point(-this.Game.GraphicsDevice.Viewport.X, -this.Game.GraphicsDevice.Viewport.Y); - private DateTime heldKeyStart; + private Dictionary<(GenericInput, int), DateTime> inputsDownAccum = new Dictionary<(GenericInput, int), DateTime>(); + private Dictionary<(GenericInput, int), DateTime> inputsDown = new Dictionary<(GenericInput, int), DateTime>(); private DateTime lastKeyRepeat; private bool triggerKeyRepeat; private Keys heldKey; @@ -162,13 +156,11 @@ namespace MLEM.Input { /// If mouse input should be handled /// If gamepad input should be handled /// If touch input should be handled - /// Whether all inputs that are currently down and pressed should be calculated each update - public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true, bool storeAllActiveInputs = true) : base(game) { + 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.StoreAllActiveInputs = storeAllActiveInputs; this.Gestures = this.gestures.AsReadOnly(); } @@ -177,42 +169,28 @@ namespace MLEM.Input { /// Call this in your method. /// public void Update() { + var now = DateTime.UtcNow; var active = this.Game.IsActive; if (this.HandleKeyboard) { this.LastKeyboardState = this.KeyboardState; this.KeyboardState = active ? Keyboard.GetState() : default; var pressedKeys = this.KeyboardState.GetPressedKeys(); - if (this.StoreAllActiveInputs) { - foreach (var pressed in pressedKeys) - this.inputsDownAccum.Add(pressed); - } + foreach (var pressed in pressedKeys) + this.AccumulateDown(pressed, -1); 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 = 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; - } + // the key that started being held most recently should be the one being repeated + this.heldKey = pressedKeys.OrderBy(k => this.GetDownTime(k)).FirstOrDefault(); + if (this.TryGetDownTime(this.heldKey, out var heldTime)) { + // if we've been holding the key longer than the initial delay... + if (heldTime >= 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; } } } @@ -224,11 +202,9 @@ namespace MLEM.Input { var state = Mouse.GetState(); if (active && this.Game.GraphicsDevice.Viewport.Bounds.Contains(state.Position)) { this.MouseState = state; - if (this.StoreAllActiveInputs) { - foreach (var button in MouseExtensions.MouseButtons) { - if (state.GetState(button) == ButtonState.Pressed) - this.inputsDownAccum.Add(button); - } + foreach (var button in MouseExtensions.MouseButtons) { + if (state.GetState(button) == ButtonState.Pressed) + this.AccumulateDown(button, -1); } } else { // mouse position and scroll wheel value should be preserved when the mouse is out of bounds @@ -244,43 +220,29 @@ namespace MLEM.Input { if (GamePad.GetCapabilities(i).IsConnected) { if (active) { this.gamepads[i] = GamePad.GetState(i); - if (this.StoreAllActiveInputs) { - foreach (var button in EnumHelper.Buttons) { - if (this.IsGamepadButtonDown(button, i)) - this.inputsDownAccum.Add(button); - } + foreach (var button in EnumHelper.Buttons) { + if (this.IsGamepadButtonDown(button, i)) + this.AccumulateDown(button, i); } } - } else { - if (this.ConnectedGamepads > i) - this.ConnectedGamepads = i; + } else if (this.ConnectedGamepads > i) { + 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; - } + this.heldGamepadButtons[i] = EnumHelper.Buttons + .Where(b => this.IsGamepadButtonDown(b, i)) + .OrderBy(b => this.GetDownTime(b, i)) + .Cast().FirstOrDefault(); + if (this.heldGamepadButtons[i].HasValue && this.TryGetDownTime(this.heldGamepadButtons[i].Value, out var heldTime, i)) { + if (heldTime >= this.KeyRepeatDelay) { + var diff = now - this.lastGamepadButtonRepeats[i]; + if (diff >= this.KeyRepeatRate) { + this.lastGamepadButtonRepeats[i] = now; + this.triggerGamepadButtonRepeat[i] = true; } } } @@ -308,15 +270,16 @@ namespace MLEM.Input { this.gestures.Add(TouchPanel.ReadGesture()); } - if (this.StoreAllActiveInputs) { - if (this.inputsDownAccum.Count <= 0) { - this.InputsPressed = Array.Empty(); - this.InputsDown = Array.Empty(); - } else { - this.InputsPressed = this.inputsDownAccum.Where(this.IsPressed).ToArray(); - this.InputsDown = this.inputsDownAccum.ToArray(); - this.inputsDownAccum.Clear(); - } + if (this.inputsDownAccum.Count <= 0) { + this.InputsPressed = Array.Empty(); + this.InputsDown = Array.Empty(); + this.inputsDown.Clear(); + } else { + this.InputsPressed = this.inputsDownAccum.Keys.Where(kv => this.IsPressed(kv.Item1, kv.Item2)).Select(kv => kv.Item1).ToArray(); + this.InputsDown = this.inputsDownAccum.Keys.Select(kv => kv.Item1).ToArray(); + // swapping these collections means that we don't have to keep moving entries between them + (this.inputsDown, this.inputsDownAccum) = (this.inputsDownAccum, this.inputsDown); + this.inputsDownAccum.Clear(); } } @@ -652,6 +615,38 @@ namespace MLEM.Input { return false; } + /// + /// Tries to retrieve the amount of time that a given has been held down for. + /// If the input is currently down, this method returns true and the amount of time that it has been down for is stored in . + /// + /// The input whose down time to query. + /// The resulting down time, or if the input is not being held. + /// The index of the gamepad to query (if applicable), or -1 for any gamepad. + /// Whether the input is currently being held. + public bool TryGetDownTime(GenericInput input, out TimeSpan downTime, int index = -1) { + if (this.inputsDown.TryGetValue((input, index), out var start)) { + downTime = DateTime.UtcNow - start; + return true; + } + return false; + } + + /// + /// Returns the amount of time that a given has been held down for. + /// If this input isn't currently own, this method returns . + /// + /// The input whose down time to query. + /// The index of the gamepad to query (if applicable), or -1 for any gamepad. + /// The resulting down time, or if the input is not being held. + public TimeSpan GetDownTime(GenericInput input, int index = -1) { + this.TryGetDownTime(input, out var time, index); + return time; + } + + private void AccumulateDown(GenericInput input, int index) { + this.inputsDownAccum.Add((input, index), this.inputsDown.TryGetValue((input, index), out var start) ? start : DateTime.UtcNow); + } + /// /// Helper function to enable gestures for a easily. /// Note that, if other gestures were previously enabled, they will not get overridden. diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index 81f0113..201e9e1 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -327,9 +327,9 @@ namespace Sandbox { } /*if (Input.InputsDown.Length > 0) - Console.WriteLine("Down: " + string.Join(", ", Input.InputsDown)); + Console.WriteLine("Down: " + string.Join(", ", Input.InputsDown));*/ if (Input.InputsPressed.Length > 0) - Console.WriteLine("Pressed: " + string.Join(", ", Input.InputsPressed));*/ + Console.WriteLine("Pressed: " + string.Join(", ", Input.InputsPressed)); } protected override void DoDraw(GameTime gameTime) {