From 87700cfc1f3f5e4a3d48fed8809c5a8eed6ad9fa Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 30 Nov 2022 16:33:52 -0500 Subject: [PATCH 1/5] Add input callback management --- devcade-library/Input.cs | 28 ++- devcade-library/devcade-library.csproj | 1 + devcade-library/events/CButton.cs | 204 +++++++++++++++++++++ devcade-library/events/DevState.cs | 53 ++++++ devcade-library/events/GButton.cs | 208 +++++++++++++++++++++ devcade-library/events/InputManager.cs | 242 +++++++++++++++++++++++++ 6 files changed, 733 insertions(+), 3 deletions(-) create mode 100644 devcade-library/events/CButton.cs create mode 100644 devcade-library/events/DevState.cs create mode 100644 devcade-library/events/GButton.cs create mode 100644 devcade-library/events/InputManager.cs diff --git a/devcade-library/Input.cs b/devcade-library/Input.cs index 351893c..4e6afb2 100644 --- a/devcade-library/Input.cs +++ b/devcade-library/Input.cs @@ -1,4 +1,5 @@ -using Microsoft.Xna.Framework.Input; +using System; +using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework; namespace Devcade @@ -35,6 +36,9 @@ public enum ArcadeButtons private static GamePadState p2State; private static GamePadState p2LastState; #endregion + + private static bool externalUpdate = false; + private static bool internalUpdate = false; /// /// Checks if a button is currently pressed. @@ -120,16 +124,34 @@ public static void Initialize() p1LastState = GamePad.GetState(0); p2LastState = GamePad.GetState(1); } + internal static void UpdateInternal() { + internalUpdate = true; + if (externalUpdate) { + throw new Exception("Cannot call Input.Update() and InputManager.Update() in the same project"); + } + p1LastState = p1State; + p2LastState = p2State; + p1State = GamePad.GetState(0); + p2State = GamePad.GetState(1); + } /// /// Updates input states. /// - public static void Update() - { + public static void Update() { + externalUpdate = true; + if (internalUpdate) { + throw new Exception("Cannot call Input.Update() and InputManager.Update() in the same project"); + } p1LastState = p1State; p2LastState = p2State; p1State = GamePad.GetState(0); p2State = GamePad.GetState(1); } + + public static (GamePadState, GamePadState) GetStates() + { + return (p1State, p2State); + } } } \ No newline at end of file diff --git a/devcade-library/devcade-library.csproj b/devcade-library/devcade-library.csproj index 02e0dbb..da47944 100644 --- a/devcade-library/devcade-library.csproj +++ b/devcade-library/devcade-library.csproj @@ -10,6 +10,7 @@ LICENSE https://github.com/ComputerScienceHouse/Devcade-library Github + latest diff --git a/devcade-library/events/CButton.cs b/devcade-library/events/CButton.cs new file mode 100644 index 0000000..0fc66b1 --- /dev/null +++ b/devcade-library/events/CButton.cs @@ -0,0 +1,204 @@ +using System; +using System.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace Devcade.events { + public class CButton { + + public static readonly CButton exit = or( + Keys.Escape, + and( + from(Buttons.Start, PlayerIndex.One), + from(Buttons.Start, PlayerIndex.Two) + ) + ); + + private enum Operator { + And, + Or, + Xor, + Not, + None + } + + private readonly Operator op; + private readonly PlayerIndex playerIndex = PlayerIndex.One; + private readonly CButton left; + private readonly CButton right; + private readonly GButton button; + + private CButton(GButton button, PlayerIndex playerIndex) { + this.button = button; + this.op = Operator.None; + this.left = null; + this.right = null; + this.playerIndex = playerIndex; + } + + private CButton(CButton left, CButton right, Operator op) { + this.op = op; + this.left = left; + this.right = right; + } + + /// + /// Creates a new Compound Button from the given Key + /// + /// A Keyboard Key + /// + public static CButton from(Keys key) { + return new CButton((GButton)key, PlayerIndex.Four); // PlayerIndex.Four is a dummy value that will always refer to the keyboard + } + + /// + /// Creates a new Compound Button from the given Button with a default PlayerIndex of One + /// + /// A Gamepad Button or Devcade Button + /// + public static CButton from(Buttons btn) { + return new CButton((GButton)btn, PlayerIndex.One); + } + + /// + /// Creates a new Compound Button from the given Button with the given PlayerIndex + /// + /// A Gamepad Button or Devcade Button + /// PlayerIndex of the button + /// + public static CButton from(Buttons btn, PlayerIndex playerIndex) { + return new CButton((GButton)btn, playerIndex); + } + + /// + /// Creates a new Compound Button that is active when both of the buttons are active + /// + /// + /// + /// + public static CButton operator &(CButton left, CButton right) { + return new CButton(left, right, Operator.And); + } + + /// + /// Creates a new Compound Button that is active when either of the buttons are active + /// + /// + /// + /// + public static CButton operator |(CButton left, CButton right) { + return new CButton(left, right, Operator.Or); + } + + /// + /// Creates a new Compound Button that is active when exactly one of the buttons are active + /// + /// + /// + /// + public static CButton operator ^(CButton left, CButton right) { + return new CButton(left, right, Operator.Xor); + } + + /// + /// Creates a new Compound Button that is active when the given button is not active + /// + /// + /// + public static CButton operator !(CButton button) { + return new CButton(button, null, Operator.Not); + } + + /// + /// Creates a Compound Button which is active when any of the buttons in the array are active + /// + /// + /// + public static CButton or(params CButton[] btns) { + switch (btns.Length) { + case 2: + return btns[0] | btns[1]; + case 1: + return btns[0]; + default: { + int mid = btns.Length / 2; + return or(btns.Take(mid).ToArray()) | or(btns.Skip(mid).ToArray()); + } + } + } + + /// + /// Creates a Compound Button which is active when any of the buttons in the array are active. + /// + /// An array of Compound Buttons, Buttons, or Keys + /// + /// Thrown if one of the input objects is not a valid button type + public static CButton or(params object[] btns) { + var cbtns = new CButton[btns.Length]; + for (int i = 0; i < btns.Length; i++) { + switch (btns[i]) { + case CButton _: + cbtns[i] = (CButton)btns[i]; + break; + case Buttons _: + cbtns[i] = from((Buttons)btns[i]); + break; + case Keys _: + cbtns[i] = from((Keys)btns[i]); + break; + default: + throw new ArgumentException("buttons must only contain CompoundButtons, Buttons, or Keys"); + } + } + return or(cbtns); + } + + /// + /// Creates a Compound Button which is active when all of the buttons in the array are active. + /// + /// An array of Compound Buttons + /// + public static CButton and(params CButton[] btns) { + switch (btns.Length) { + case 2: + return btns[0] & btns[1]; + case 1: + return btns[0]; + default: { + int mid = btns.Length / 2; + return and(btns.Take(mid).ToArray()) & and(btns.Skip(mid).ToArray()); + } + } + } + + /// + /// Creates a Compound Button which is active when all of the buttons in the array are active. + /// + /// An array of Compound Buttons, Buttons, or Keys + /// + /// Thrown if one of the input objects is not a valid button type + public static CButton and(params object[] btns) { + var cbtns = new CButton[btns.Length]; + for (int i = 0; i < btns.Length; i++) { + cbtns[i] = btns[i] switch { + CButton c => c, + Buttons b => from(b), + Keys k => from(k), + _ => throw new ArgumentException("buttons must only contain CompoundButtons, Buttons, or Keys") + }; + } + return and(cbtns); + } + + internal bool isDown(DevState state) { + return op switch { + Operator.And => left.isDown(state) && right.isDown(state), + Operator.Or => left.isDown(state) || right.isDown(state), + Operator.Xor => left.isDown(state) ^ right.isDown(state), + Operator.Not => !left.isDown(state), + Operator.None => state.isDown(button, playerIndex), + _ => throw new ArgumentOutOfRangeException() + }; + } + } +} \ No newline at end of file diff --git a/devcade-library/events/DevState.cs b/devcade-library/events/DevState.cs new file mode 100644 index 0000000..cbd03dc --- /dev/null +++ b/devcade-library/events/DevState.cs @@ -0,0 +1,53 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace Devcade.events { + internal class DevState { + private KeyboardState kbState; + private GamePadState gp1; + private GamePadState gp2; + + public DevState() { + this.kbState = Keyboard.GetState(); + this.gp1 = GamePad.GetState(PlayerIndex.One); + this.gp2 = GamePad.GetState(PlayerIndex.Two); + } + + public DevState(KeyboardState kbState, GamePadState gp1, GamePadState gp2) { + this.kbState = kbState; + this.gp1 = gp1; + this.gp2 = gp2; + } + + private bool isKeyDown(Keys key) { + return kbState.IsKeyDown(key); + } + + private bool isButtonDown(Buttons button, PlayerIndex player) { + switch (player) { + case PlayerIndex.One: + return gp1.IsButtonDown(button); + case PlayerIndex.Two: + return gp2.IsButtonDown(button); + case PlayerIndex.Three: + case PlayerIndex.Four: + default: + return false; + } + } + + public bool isDown(GButton button, PlayerIndex playerIndex) { + switch (playerIndex) { + case PlayerIndex.One: + case PlayerIndex.Two: + return isButtonDown((Buttons)button, playerIndex); + case PlayerIndex.Four: + return isKeyDown((Keys)button); + case PlayerIndex.Three: + default: + throw new ArgumentOutOfRangeException(nameof(playerIndex), playerIndex, null); + } + } + } +} \ No newline at end of file diff --git a/devcade-library/events/GButton.cs b/devcade-library/events/GButton.cs new file mode 100644 index 0000000..7e98ac0 --- /dev/null +++ b/devcade-library/events/GButton.cs @@ -0,0 +1,208 @@ +using Microsoft.Xna.Framework.Input; + +namespace Devcade.events { + internal enum GButton { + #region Keyboard + kNone = Keys.None, + kBack = Keys.Back, + kTab = Keys.Tab, + kEnter = Keys.Enter, + kPause = Keys.Pause, + kCapsLock = Keys.CapsLock, + kEscape = Keys.Escape, + kSpace = Keys.Space, + kPageUp = Keys.PageUp, + kPageDown = Keys.PageDown, + kEnd = Keys.End, + kHome = Keys.Home, + kLeft = Keys.Left, + kUp = Keys.Up, + kRight = Keys.Right, + kDown = Keys.Down, + kSelect = Keys.Select, + kPrint = Keys.Print, + kExecute = Keys.Execute, + kPrintScreen = Keys.PrintScreen, + kInsert = Keys.Insert, + kDelete = Keys.Delete, + kHelp = Keys.Help, + kD0 = Keys.D0, + kD1 = Keys.D1, + kD2 = Keys.D2, + kD3 = Keys.D3, + kD4 = Keys.D4, + kD5 = Keys.D5, + kD6 = Keys.D6, + kD7 = Keys.D7, + kD8 = Keys.D8, + kD9 = Keys.D9, + kA = Keys.A, + kB = Keys.B, + kC = Keys.C, + kD = Keys.D, + kE = Keys.E, + kF = Keys.F, + kG = Keys.G, + kH = Keys.H, + kI = Keys.I, + kJ = Keys.J, + kK = Keys.K, + kL = Keys.L, + kM = Keys.M, + kN = Keys.N, + kO = Keys.O, + kP = Keys.P, + kQ = Keys.Q, + kR = Keys.R, + kS = Keys.S, + kT = Keys.T, + kU = Keys.U, + kV = Keys.V, + kW = Keys.W, + kX = Keys.X, + kY = Keys.Y, + kZ = Keys.Z, + kLeftWindows = Keys.LeftWindows, + kRightWindows = Keys.RightWindows, + kApps = Keys.Apps, + kSleep = Keys.Sleep, + kNumPad0 = Keys.NumPad0, + kNumPad1 = Keys.NumPad1, + kNumPad2 = Keys.NumPad2, + kNumPad3 = Keys.NumPad3, + kNumPad4 = Keys.NumPad4, + kNumPad5 = Keys.NumPad5, + kNumPad6 = Keys.NumPad6, + kNumPad7 = Keys.NumPad7, + kNumPad8 = Keys.NumPad8, + kNumPad9 = Keys.NumPad9, + kMultiply = Keys.Multiply, + kAdd = Keys.Add, + kSeparator = Keys.Separator, + kSubtract = Keys.Subtract, + kDecimal = Keys.Decimal, + kDivide = Keys.Divide, + kF1 = Keys.F1, + kF2 = Keys.F2, + kF3 = Keys.F3, + kF4 = Keys.F4, + kF5 = Keys.F5, + kF6 = Keys.F6, + kF7 = Keys.F7, + kF8 = Keys.F8, + kF9 = Keys.F9, + kF10 = Keys.F10, + kF11 = Keys.F11, + kF12 = Keys.F12, + kF13 = Keys.F13, + kF14 = Keys.F14, + kF15 = Keys.F15, + kF16 = Keys.F16, + kF17 = Keys.F17, + kF18 = Keys.F18, + kF19 = Keys.F19, + kF20 = Keys.F20, + kF21 = Keys.F21, + kF22 = Keys.F22, + kF23 = Keys.F23, + kF24 = Keys.F24, + kNumLock = Keys.NumLock, + kScroll = Keys.Scroll, + kLeftShift = Keys.LeftShift, + kRightShift = Keys.RightShift, + kLeftControl = Keys.LeftControl, + kRightControl = Keys.RightControl, + kLeftAlt = Keys.LeftAlt, + kRightAlt = Keys.RightAlt, + kBrowserBack = Keys.BrowserBack, + kBrowserForward = Keys.BrowserForward, + kBrowserRefresh = Keys.BrowserRefresh, + kBrowserStop = Keys.BrowserStop, + kBrowserSearch = Keys.BrowserSearch, + kBrowserFavorites = Keys.BrowserFavorites, + kBrowserHome = Keys.BrowserHome, + kVolumeMute = Keys.VolumeMute, + kVolumeDown = Keys.VolumeDown, + kVolumeUp = Keys.VolumeUp, + kMediaNextTrack = Keys.MediaNextTrack, + kMediaPreviousTrack = Keys.MediaPreviousTrack, + kMediaStop = Keys.MediaStop, + kMediaPlayPause = Keys.MediaPlayPause, + kLaunchMail = Keys.LaunchMail, + kSelectMedia = Keys.SelectMedia, + kLaunchApplication1 = Keys.LaunchApplication1, + kLaunchApplication2 = Keys.LaunchApplication2, + kOemSemicolon = Keys.OemSemicolon, + kOemPlus = Keys.OemPlus, + kOemComma = Keys.OemComma, + kOemMinus = Keys.OemMinus, + kOemPeriod = Keys.OemPeriod, + kOemQuestion = Keys.OemQuestion, + kOemTilde = Keys.OemTilde, + kChatPadGreen = Keys.ChatPadGreen, + kChatPadOrange = Keys.ChatPadOrange, + kOemOpenBrackets = Keys.OemOpenBrackets, + kOemPipe = Keys.OemPipe, + kOemCloseBrackets = Keys.OemCloseBrackets, + kOemQuotes = Keys.OemQuotes, + kOem8 = Keys.Oem8, + kOemBackslash = Keys.OemBackslash, + kProcessKey = Keys.ProcessKey, + kOemCopy = Keys.OemCopy, + kOemAuto = Keys.OemAuto, + kOemEnlW = Keys.OemEnlW, + kAttn = Keys.Attn, + kCrSel = Keys.Crsel, + kExSel = Keys.Exsel, + kEraseEof = Keys.EraseEof, + kPlay = Keys.Play, + kZoom = Keys.Zoom, + kPa1 = Keys.Pa1, + kOemClear = Keys.OemClear, + #endregion + + #region Gamepad + gDPadUp = Buttons.DPadUp, + gDPadDown = Buttons.DPadDown, + gDPadLeft = Buttons.DPadLeft, + gDPadRight = Buttons.DPadRight, + gStart = Buttons.Start, + gBack = Buttons.Back, + gLeftStick = Buttons.LeftStick, + gRightStick = Buttons.RightStick, + gLeftShoulder = Buttons.LeftShoulder, + gRightShoulder = Buttons.RightShoulder, + gBigButton = Buttons.BigButton, + gA = Buttons.A, + gB = Buttons.B, + gX = Buttons.X, + gY = Buttons.Y, + gLeftThumstickLeft = Buttons.LeftThumbstickLeft, + gLeftTrigger = Buttons.LeftTrigger, + gRightTrigger = Buttons.RightTrigger, + gLeftThumbstickUp = Buttons.LeftThumbstickUp, + gLeftThumbstickDown = Buttons.LeftThumbstickDown, + gLeftThumbstickRight = Buttons.LeftThumbstickRight, + gRightThumbstickLeft = Buttons.RightThumbstickLeft, + gRightThumbstickUp = Buttons.RightThumbstickUp, + gRightThumbstickDown = Buttons.RightThumbstickDown, + gRightThumbstickRight = Buttons.RightThumbstickRight, + #endregion + + #region Devcade + dA1 = Input.ArcadeButtons.A1, + dA2 = Input.ArcadeButtons.A2, + dA3 = Input.ArcadeButtons.A3, + dA4 = Input.ArcadeButtons.A4, + dB1 = Input.ArcadeButtons.B1, + dB2 = Input.ArcadeButtons.B2, + dB3 = Input.ArcadeButtons.B3, + dB4 = Input.ArcadeButtons.B4, + dMenu = Input.ArcadeButtons.Menu, + dStickUp = Input.ArcadeButtons.StickUp, + dStickDown = Input.ArcadeButtons.StickDown, + dStickLeft = Input.ArcadeButtons.StickLeft, + dStickRight = Input.ArcadeButtons.StickRight, + #endregion + } +} \ No newline at end of file diff --git a/devcade-library/events/InputManager.cs b/devcade-library/events/InputManager.cs new file mode 100644 index 0000000..0079836 --- /dev/null +++ b/devcade-library/events/InputManager.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Xna.Framework.Input; + +namespace Devcade.events { + public class InputManager { + private static readonly Dictionary inputManagers = new(); + private static Dictionary enabled = new(); + private static readonly InputManager globalInputManager = new(); + + private const int debounce = 6; + private readonly DevState[] _states = new DevState[debounce]; + private int ptr; + private readonly List events = new(); + public string name { get; } + + private enum State { + Held, + Released, + Pressed, + } + private struct Event { + public CButton button { get; set; } + public State state { get; set; } + public Action action { get; set; } + public bool async { get; set; } + + public bool Invoke() { + if (async) { + Task.Run(action); + } + else { + action(); + } + return true; + } + + public bool Matches(InputManager inputManager) { + if (!enabled[inputManager.name]) return false; + switch (state) { + case State.Held: + if (inputManager.IsHeld(button)) return true; + break; + case State.Pressed: + if (inputManager.IsPressed(button)) return true; + break; + case State.Released: + if (inputManager.IsReleased(button)) return true; + break; + default: + throw new ArgumentOutOfRangeException(); + } + return false; + } + } + + private InputManager(string name) { + this.name = name; + } + + private InputManager() { + this.name = null; + } + + /// + /// Update Input and invoke events + /// + public static void Update() { + Input.UpdateInternal(); + (GamePadState, GamePadState) gamePad = Input.GetStates(); + KeyboardState kbState = Keyboard.GetState(); + DevState state = new(kbState, gamePad.Item1, gamePad.Item2); + globalInputManager.Update(state); + foreach (InputManager inputManager in inputManagers.Values) { + inputManager.Update(state); + } + } + + private void Update(DevState state) { + _states[ptr++] = state; + if (ptr > debounce) { + ptr = 0; + } + events.ForEach(e => { + bool _ = e.Matches(this) && e.Invoke(); + }); + } + + private DevState GetState(int offset) { + int index = ptr - offset; + if (index < 0) { + index += debounce; + } + + return _states[index]; + } + + private bool IsPressed(CButton btn) { + return btn.isDown(GetState(0)) && !btn.isDown(GetState(1)); + } + + private bool IsReleased(CButton btn) { + return !btn.isDown(GetState(0)) && btn.isDown(GetState(1)); + } + + private bool IsHeld(CButton btn) { + int acc = 0; + for (int i = 0; i < debounce; i++) { + if (btn.isDown(GetState(i))) { + acc++; + } + else { + return false; // if any of the previous frames were not held, then it is not held + } + + if (acc > 1) return true; + } + + return false; + } + + public void OnPress(CButton btn, Action action, bool async = false) { + events.Add(new Event { + button = btn, + state = State.Pressed, + action = action, + async = async + }); + } + + public void OnRelease(CButton btn, Action action, bool async = false) { + events.Add(new Event { + button = btn, + state = State.Released, + action = action, + async = async + }); + } + + public void OnHeld(CButton btn, Action action, bool async = false) { + events.Add(new Event { + button = btn, + state = State.Held, + action = action, + async = async + }); + } + + public static void OnPressGlobal(CButton btn, Action action, bool async = false) { + globalInputManager.OnPress(btn, action, async); + } + + public static void OnReleaseGlobal(CButton btn, Action action, bool async = false) { + globalInputManager.OnRelease(btn, action, async); + } + + public static void OnHeldGlobal(CButton btn, Action action, bool async = false) { + globalInputManager.OnHeld(btn, action, async); + } + + public static void OnHeld(CButton btn, Action action, string name, bool async = false) { + if (!inputManagers.ContainsKey(name)) { + inputManagers[name] = new InputManager(); + } + inputManagers[name].OnHeld(btn, action, async); + } + + public static void OnPress(CButton btn, Action action, string name, bool async = false) { + if (!inputManagers.ContainsKey(name)) { + inputManagers[name] = new InputManager(); + } + inputManagers[name].OnPress(btn, action, async); + } + + public static void OnRelease(CButton btn, Action action, string name, bool async = false) { + if (!inputManagers.ContainsKey(name)) { + inputManagers[name] = new InputManager(); + } + inputManagers[name].OnRelease(btn, action, async); + } + + public static InputManager getInputManager(string name) { + if (inputManagers.ContainsKey(name)) { + return inputManagers[name]; + } + InputManager inputManager = new(name); + inputManagers[name] = inputManager; + enabled[name] = true; + return inputManager; + } + + public void setEnabled(bool enabled) { + InputManager.enabled[name] = enabled; + } + + public static void setEnabled(string name, bool enabled) { + if (!inputManagers.ContainsKey(name)) return; + InputManager.enabled[name] = enabled; + } + + public static void setSoleEnabled(string name) { + foreach (string key in inputManagers.Keys) { + enabled[key] = key == name; + } + } + + public static void setAllEnabled(bool enabled) { + foreach (string key in inputManagers.Keys) { + InputManager.enabled[key] = enabled; + } + } + + public static List getEnabled() { + return inputManagers.Keys.Where(key => enabled[key]).ToList(); + } + + public static List getDisabled() { + return inputManagers.Keys.Where(key => !enabled[key]).ToList(); + } + + public static void setEnabled(IEnumerable names) { + // IEnumerable can be lazy so we need to force it to evaluate + // otherwise every time we check name.Contains(key) it will evaluate the whole thing again + names = names.ToList(); + foreach (string key in inputManagers.Keys) { + enabled[key] = names.Contains(key); + } + } + + public static void setDisabled(IEnumerable names) { + // IEnumerable can be lazy so we need to force it to evaluate + // otherwise every time we check name.Contains(key) it will evaluate the whole thing again + names = names.ToList(); + foreach (string key in inputManagers.Keys) { + enabled[key] = !names.Contains(key); + } + } + } +} \ No newline at end of file From 2278241c4c62abef1fc7d6d79a1cee62d938500f Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 30 Nov 2022 18:59:06 -0500 Subject: [PATCH 2/5] Fixed thing --- devcade-library/Input.cs | 16 ++++++----- devcade-library/events/InputManager.cs | 37 +++++++++++++++----------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/devcade-library/Input.cs b/devcade-library/Input.cs index 4e6afb2..c5e4e86 100644 --- a/devcade-library/Input.cs +++ b/devcade-library/Input.cs @@ -62,10 +62,13 @@ public static bool GetButton(int playerNum, ArcadeButtons button) /// True if the button was pressed last frame, false otherwise. private static bool GetLastButton(int playerNum, ArcadeButtons button) { - if (playerNum == 1 && p1LastState.IsButtonDown((Buttons)button)) { return true; } - if (playerNum == 2 && p2LastState.IsButtonDown((Buttons)button)) { return true; } - - return false; + switch (playerNum) { + case 1 when p1LastState.IsButtonDown((Buttons)button): + case 2 when p2LastState.IsButtonDown((Buttons)button): + return true; + default: + return false; + } } /// @@ -124,10 +127,11 @@ public static void Initialize() p1LastState = GamePad.GetState(0); p2LastState = GamePad.GetState(1); } + internal static void UpdateInternal() { internalUpdate = true; if (externalUpdate) { - throw new Exception("Cannot call Input.Update() and InputManager.Update() in the same project"); + throw new Exception("Cannot use Input.Update() and InputManager.Update() in the same project"); } p1LastState = p1State; p2LastState = p2State; @@ -141,7 +145,7 @@ internal static void UpdateInternal() { public static void Update() { externalUpdate = true; if (internalUpdate) { - throw new Exception("Cannot call Input.Update() and InputManager.Update() in the same project"); + throw new Exception("Cannot use Input.Update() and InputManager.Update() in the same project"); } p1LastState = p1State; p2LastState = p2State; diff --git a/devcade-library/events/InputManager.cs b/devcade-library/events/InputManager.cs index 0079836..5fc1212 100644 --- a/devcade-library/events/InputManager.cs +++ b/devcade-library/events/InputManager.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; namespace Devcade.events { public class InputManager { private static readonly Dictionary inputManagers = new(); - private static Dictionary enabled = new(); + private static readonly Dictionary enabled = new(); private static readonly InputManager globalInputManager = new(); private const int debounce = 6; @@ -39,7 +39,7 @@ public bool Invoke() { } public bool Matches(InputManager inputManager) { - if (!enabled[inputManager.name]) return false; + if (inputManager.name != null /* null is global manager */ && !enabled[inputManager.name]) return false; switch (state) { case State.Held: if (inputManager.IsHeld(button)) return true; @@ -59,10 +59,16 @@ public bool Matches(InputManager inputManager) { private InputManager(string name) { this.name = name; + for(int i = 0; i < debounce; i++) { + _states[i] = new DevState(); + } } private InputManager() { this.name = null; + for (int i = 0; i < debounce; i++) { + _states[i] = new DevState(); + } } /// @@ -80,10 +86,11 @@ public static void Update() { } private void Update(DevState state) { - _states[ptr++] = state; - if (ptr > debounce) { + ptr++; + if (ptr >= debounce) { ptr = 0; } + _states[ptr] = state; events.ForEach(e => { bool _ = e.Matches(this) && e.Invoke(); }); @@ -122,7 +129,7 @@ private bool IsHeld(CButton btn) { return false; } - public void OnPress(CButton btn, Action action, bool async = false) { + public void OnPressed(CButton btn, Action action, bool async = false) { events.Add(new Event { button = btn, state = State.Pressed, @@ -131,7 +138,7 @@ public void OnPress(CButton btn, Action action, bool async = false) { }); } - public void OnRelease(CButton btn, Action action, bool async = false) { + public void OnReleased(CButton btn, Action action, bool async = false) { events.Add(new Event { button = btn, state = State.Released, @@ -149,12 +156,12 @@ public void OnHeld(CButton btn, Action action, bool async = false) { }); } - public static void OnPressGlobal(CButton btn, Action action, bool async = false) { - globalInputManager.OnPress(btn, action, async); + public static void OnPressedGlobal(CButton btn, Action action, bool async = false) { + globalInputManager.OnPressed(btn, action, async); } - public static void OnReleaseGlobal(CButton btn, Action action, bool async = false) { - globalInputManager.OnRelease(btn, action, async); + public static void OnReleasedGlobal(CButton btn, Action action, bool async = false) { + globalInputManager.OnReleased(btn, action, async); } public static void OnHeldGlobal(CButton btn, Action action, bool async = false) { @@ -168,18 +175,18 @@ public static void OnHeld(CButton btn, Action action, string name, bool async = inputManagers[name].OnHeld(btn, action, async); } - public static void OnPress(CButton btn, Action action, string name, bool async = false) { + public static void OnPressed(CButton btn, Action action, string name, bool async = false) { if (!inputManagers.ContainsKey(name)) { inputManagers[name] = new InputManager(); } - inputManagers[name].OnPress(btn, action, async); + inputManagers[name].OnPressed(btn, action, async); } - public static void OnRelease(CButton btn, Action action, string name, bool async = false) { + public static void OnReleased(CButton btn, Action action, string name, bool async = false) { if (!inputManagers.ContainsKey(name)) { inputManagers[name] = new InputManager(); } - inputManagers[name].OnRelease(btn, action, async); + inputManagers[name].OnReleased(btn, action, async); } public static InputManager getInputManager(string name) { From 1da3782cfc8726500f5a1571b9da5fbdb96965b4 Mon Sep 17 00:00:00 2001 From: joe Date: Wed, 30 Nov 2022 21:09:44 -0500 Subject: [PATCH 3/5] Update README --- README.md | 124 ++++++++++++++++++++++++- devcade-library/events/InputManager.cs | 6 +- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8660102..42096de 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ A monogame library for allowing games to interact with cabinet functions. - [Input](#input-wrapping) - [ArcadeButtons enum](#arcadebuttons-enum) - [Get methods](#get-methods) - +- [Event-Based Input](#input-events) + - [CButton](#Combined-button) + - [Callbacks](#Callbacks) + --- --- @@ -49,4 +52,121 @@ Given the player and button to check, it will return true if the button is being Given the player it returns a `Vector2` representing the stick direction. --- ---- \ No newline at end of file +--- + +## Input Events + +The primary difference for event-based input is that instead of calling `Input.Update()` every frame, you should call `InputManager.Update()` + +### Combined Button + +Combined buttons are generally better for your health than a thousand `||`s and `&&`s between everything [Citation needed] + +`CButton` is a class that represents a combined button, which will be 'pressed' when its conditions are met + +A CButton can be created from a single button using `CButton.from` + +```csharp +CButton keyA = CButton.from(Keys.A); +CButton gpX = CButton.from(Buttons.X); +CButton devB1 = CButton.from(Input.ArcadeButtons.B1); +``` + +CButtons can also be combined arbitrarily or inverted + +```csharp +CButton AorX = keyA | gpX; +CButton AandB1 = keyA & devB1; +CButton notX = !gpX; +``` + +A CButton can be created from an arbitrary number of inputs + +```csharp +CButton anyDirection = CButton.or(Keys.Up, Keys.Down, Keys.Left, Keys.Right); +CButton sprint = CButton.and(Keys.Shift, Keys.W); +``` + +The inputs to `CButton.or()` and `CButton.and()` can be other CButtons, Keys, or Buttons (GamePad or Devcade). + +--- +--- + +### Callbacks + +The InputManager uses C#'s `Action`s, which are functions that have no inputs and return `void` + +All Actions should be bound in the `Initialize()` method of your game + +You have three options for binding an `Action` to a Key: + +#### Global Input Manager + +The Global input manager represents a group of keybinds that cannot be turned off in any circumstances. For simple games, everything can be done in the global manager, for more control over which keys are bound to what and when, use multiple instances of the Input Manager. + +```csharp +// This is the 'exit' button, which is either the Esc key or both Menu buttons. +// This is acutally defined in CButton as CButton.exit, so you won't need to +// make one yourself +CButton exit = CButton.or( + CButton.from(Keys.Escape), + CButton.and( + CButton.from(Input.ArcadeButtons.Menu, PlayerIndex.One), + CButton.from(Input.ArcadeButtons.Menu, PlayerIndex.Two), + ), +); + +InputManager.onPressedGlobal(exit, () => { Exit(); }); + +// You can also use a method group here, which is just a shorthand for the above +// InputManager.onPressedGlobal(exit, Exit); +``` + +#### Static groups + +Static management interacts with the InputManager class statically to define groups of keybindings. These groups can be individually enabled and disabled to allow for finer control over which keybinds are active when. + +```csharp +// Class managing input in a submenu + +CButton up = CButton.or(Keys.Up, Keys.W, Input.ArcadeButtons.StickUp); +CButton down = CButton.or(Keys.Down, Keys.S, Input.ArcadeButtons.StickUp); + +// This is not necessary, as binding groups are enabled by default. +// This is only to demonstrate how to enable and disable groups +InputManager.setEnabled("submenu", true); + +InputManager.onPressed(up, "submenu", () => { + selected--; +}) +InputManager.onPressed(down, "submenu", () => { + selected++; +}) +``` + +#### Instance groups + +This uses instances of the InputManager class to define groups of keybindings. This is, under the hood, entirely identical to static management. The only difference is in code style. Both static and non-static management can be used in the same game, and can in fact both refer to the same binding group interchangeably. + +```csharp +// Class managing input in a submenu + +CButton up = CButton.or(Keys.Up, Keys.W, Input.ArcadeButtons.StickUp); +CButton down = CButton.or(Keys.Down, Keys.S, Input.ArcadeButtons.StickUp); + +InputManager submenuManager = InputManager.getInputManager("submenu"); + +// This is not necessary, as binding groups are enabled by default. +// This is only to demonstrate how to enable and disable groups +submentManager.setEnabled(true); + +submenuManager.onPressed(up, () => { + selected--; +}); + +submenuManager.onPressed(down, () => { + selected++; +}); +``` + +--- diff --git a/devcade-library/events/InputManager.cs b/devcade-library/events/InputManager.cs index 5fc1212..3d22652 100644 --- a/devcade-library/events/InputManager.cs +++ b/devcade-library/events/InputManager.cs @@ -168,21 +168,21 @@ public static void OnHeldGlobal(CButton btn, Action action, bool async = false) globalInputManager.OnHeld(btn, action, async); } - public static void OnHeld(CButton btn, Action action, string name, bool async = false) { + public static void OnHeld(CButton btn, string name, Action action, bool async = false) { if (!inputManagers.ContainsKey(name)) { inputManagers[name] = new InputManager(); } inputManagers[name].OnHeld(btn, action, async); } - public static void OnPressed(CButton btn, Action action, string name, bool async = false) { + public static void OnPressed(CButton btn, string name, Action action, bool async = false) { if (!inputManagers.ContainsKey(name)) { inputManagers[name] = new InputManager(); } inputManagers[name].OnPressed(btn, action, async); } - public static void OnReleased(CButton btn, Action action, string name, bool async = false) { + public static void OnReleased(CButton btn, string name, Action action, bool async = false) { if (!inputManagers.ContainsKey(name)) { inputManagers[name] = new InputManager(); } From b790cec46369d4e74e843599786380f0900887a8 Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 11 Feb 2023 20:58:23 -0500 Subject: [PATCH 4/5] Fix requested changes --- README.md | 2 +- devcade-library/Input.cs | 69 +++++++++++++------------- devcade-library/events/CButton.cs | 19 +++---- devcade-library/events/InputManager.cs | 29 +++++++---- 4 files changed, 61 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 42096de..bd85d32 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ InputManager submenuManager = InputManager.getInputManager("submenu"); // This is not necessary, as binding groups are enabled by default. // This is only to demonstrate how to enable and disable groups -submentManager.setEnabled(true); +submenuManager.setEnabled(true); submenuManager.onPressed(up, () => { selected--; diff --git a/devcade-library/Input.cs b/devcade-library/Input.cs index c5e4e86..44cd62f 100644 --- a/devcade-library/Input.cs +++ b/devcade-library/Input.cs @@ -2,18 +2,15 @@ using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework; -namespace Devcade -{ - public static class Input - { +namespace Devcade { + public static class Input { /// /// Enum of button names available on the cabinet. Maps directly to the /// equivalent in the Buttons enum. Allows you to use existing controller /// logic and essentially just rename the buttons but you must explicitly /// cast it to a Button each time. /// - public enum ArcadeButtons - { + public enum ArcadeButtons { A1=Buttons.X, A2=Buttons.Y, A3=Buttons.RightShoulder, @@ -37,8 +34,12 @@ public enum ArcadeButtons private static GamePadState p2LastState; #endregion - private static bool externalUpdate = false; - private static bool internalUpdate = false; + private static bool externalUpdate; + private static bool internalUpdate; + + private class UpdateManagerException : ApplicationException { + public UpdateManagerException(string message) : base(message) { } + } /// /// Checks if a button is currently pressed. @@ -46,12 +47,14 @@ public enum ArcadeButtons /// The player whose controls should be checked. /// The button to check. /// True when button is pressed, false otherwise. - public static bool GetButton(int playerNum, ArcadeButtons button) - { - if (playerNum == 1 && p1State.IsButtonDown((Buttons)button)) { return true; } - if (playerNum == 2 && p2State.IsButtonDown((Buttons)button)) { return true; } - - return false; + public static bool GetButton(int playerNum, ArcadeButtons button) { + switch (playerNum) { + case 1 when p1State.IsButtonDown((Buttons)button): + case 2 when p2State.IsButtonDown((Buttons)button): + return true; + default: + return false; + } } /// @@ -60,8 +63,7 @@ public static bool GetButton(int playerNum, ArcadeButtons button) /// The player whose controls should be checked. /// The button to check. /// True if the button was pressed last frame, false otherwise. - private static bool GetLastButton(int playerNum, ArcadeButtons button) - { + private static bool GetLastButton(int playerNum, ArcadeButtons button) { switch (playerNum) { case 1 when p1LastState.IsButtonDown((Buttons)button): case 2 when p2LastState.IsButtonDown((Buttons)button): @@ -77,8 +79,7 @@ private static bool GetLastButton(int playerNum, ArcadeButtons button) /// The player whose controls should be checked. /// The button to check. /// True if the button transitioned from up to down in the current frame, false otherwise. - public static bool GetButtonDown(int playerNum, ArcadeButtons button) - { + public static bool GetButtonDown(int playerNum, ArcadeButtons button) { return (GetButton(playerNum, button) && !GetLastButton(playerNum, button)); } @@ -88,8 +89,7 @@ public static bool GetButtonDown(int playerNum, ArcadeButtons button) /// The player whose controls should be checked. /// The button to check. /// True if the button transitioned from down to up in the current frame, false otherwise. - public static bool GetButtonUp(int playerNum, ArcadeButtons button) - { + public static bool GetButtonUp(int playerNum, ArcadeButtons button) { return (!GetButton(playerNum, button) && GetLastButton(playerNum, button)); } @@ -99,8 +99,7 @@ public static bool GetButtonUp(int playerNum, ArcadeButtons button) /// The player whose controls should be checked. /// The button to check. /// True if the button was down last frame and is still down, false otherwise. - public static bool GetButtonHeld(int playerNum, ArcadeButtons button) - { + public static bool GetButtonHeld(int playerNum, ArcadeButtons button) { return (GetButton(playerNum, button) && GetLastButton(playerNum, button)); } @@ -109,19 +108,18 @@ public static bool GetButtonHeld(int playerNum, ArcadeButtons button) /// /// The player whose controls should be checked. /// A Vector2 representing the stick direction. - public static Vector2 GetStick(int playerNum) - { - if (playerNum == 1) { return p1State.ThumbSticks.Left; } - if (playerNum == 2) { return p2State.ThumbSticks.Left; } - - return Vector2.Zero; + public static Vector2 GetStick(int playerNum) { + return playerNum switch { + 1 => p1State.ThumbSticks.Left, + 2 => p2State.ThumbSticks.Left, + _ => Vector2.Zero + }; } /// /// Setup initial input states. /// - public static void Initialize() - { + public static void Initialize() { p1State = GamePad.GetState(0); p2State = GamePad.GetState(1); p1LastState = GamePad.GetState(0); @@ -131,7 +129,9 @@ public static void Initialize() internal static void UpdateInternal() { internalUpdate = true; if (externalUpdate) { - throw new Exception("Cannot use Input.Update() and InputManager.Update() in the same project"); + // This exception does not necessarily need to be thrown, it could just ignore the calls, but + // calling both Update() methods will probably lead to behavior that is not intended. + throw new UpdateManagerException("Cannot use Input.Update() and InputManager.Update() in the same project"); } p1LastState = p1State; p2LastState = p2State; @@ -145,16 +145,15 @@ internal static void UpdateInternal() { public static void Update() { externalUpdate = true; if (internalUpdate) { - throw new Exception("Cannot use Input.Update() and InputManager.Update() in the same project"); + throw new UpdateManagerException("Cannot use Input.Update() and InputManager.Update() in the same project"); } p1LastState = p1State; p2LastState = p2State; p1State = GamePad.GetState(0); p2State = GamePad.GetState(1); } - - public static (GamePadState, GamePadState) GetStates() - { + + public static (GamePadState, GamePadState) GetStates() { return (p1State, p2State); } } diff --git a/devcade-library/events/CButton.cs b/devcade-library/events/CButton.cs index 0fc66b1..e6217a8 100644 --- a/devcade-library/events/CButton.cs +++ b/devcade-library/events/CButton.cs @@ -136,19 +136,12 @@ public static CButton or(params CButton[] btns) { public static CButton or(params object[] btns) { var cbtns = new CButton[btns.Length]; for (int i = 0; i < btns.Length; i++) { - switch (btns[i]) { - case CButton _: - cbtns[i] = (CButton)btns[i]; - break; - case Buttons _: - cbtns[i] = from((Buttons)btns[i]); - break; - case Keys _: - cbtns[i] = from((Keys)btns[i]); - break; - default: - throw new ArgumentException("buttons must only contain CompoundButtons, Buttons, or Keys"); - } + cbtns[i] = btns[i] switch { + CButton _ => (CButton)btns[i], + Buttons _ => from((Buttons)btns[i]), + Keys _ => from((Keys)btns[i]), + _ => throw new ArgumentException("buttons must only contain CompoundButtons, Buttons, or Keys") + }; } return or(cbtns); } diff --git a/devcade-library/events/InputManager.cs b/devcade-library/events/InputManager.cs index 3d22652..dca1f07 100644 --- a/devcade-library/events/InputManager.cs +++ b/devcade-library/events/InputManager.cs @@ -5,8 +5,10 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; -namespace Devcade.events { - public class InputManager { +namespace Devcade.events +{ + public class InputManager + { private static readonly Dictionary inputManagers = new(); private static readonly Dictionary enabled = new(); private static readonly InputManager globalInputManager = new(); @@ -17,12 +19,14 @@ public class InputManager { private readonly List events = new(); public string name { get; } - private enum State { + private enum State + { Held, Released, Pressed, } - private struct Event { + private struct Event + { public CButton button { get; set; } public State state { get; set; } public Action action { get; set; } @@ -39,16 +43,24 @@ public bool Invoke() { } public bool Matches(InputManager inputManager) { - if (inputManager.name != null /* null is global manager */ && !enabled[inputManager.name]) return false; + if (inputManager.name != null /* null is global manager */ && !enabled[inputManager.name]) { + return false; + } switch (state) { case State.Held: - if (inputManager.IsHeld(button)) return true; + if (inputManager.IsHeld(button)) { + return true; + } break; case State.Pressed: - if (inputManager.IsPressed(button)) return true; + if (inputManager.IsPressed(button)) { + return true; + } break; case State.Released: - if (inputManager.IsReleased(button)) return true; + if (inputManager.IsReleased(button)) { + return true; + } break; default: throw new ArgumentOutOfRangeException(); @@ -122,7 +134,6 @@ private bool IsHeld(CButton btn) { else { return false; // if any of the previous frames were not held, then it is not held } - if (acc > 1) return true; } From e46c88785756f6c0613ffd8de0be619877a7c21d Mon Sep 17 00:00:00 2001 From: joe Date: Thu, 23 Mar 2023 19:21:02 -0400 Subject: [PATCH 5/5] Add field names for GamepadState tuple --- devcade-library/Input.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/devcade-library/Input.cs b/devcade-library/Input.cs index 8f83f40..8f4e5dd 100644 --- a/devcade-library/Input.cs +++ b/devcade-library/Input.cs @@ -152,9 +152,9 @@ public static void Update() { p1State = GamePad.GetState(0); p2State = GamePad.GetState(1); } - - public static (GamePadState, GamePadState) GetStates() { - return (p1State, p2State); + + public static (GamePadState player1, GamePadState player2) GetStates() { + return (player1: p1State, player2: p2State); } } }