Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 120 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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)
- [Save data](#save-data)

---
Expand Down Expand Up @@ -52,6 +55,122 @@ Given the player it returns a `Vector2` representing the stick direction.
---
---

## 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
submenuManager.setEnabled(true);

submenuManager.onPressed(up, () => {
selected--;
});

submenuManager.onPressed(down, () => {
selected++;
});
```

---
## Save data
In the Devcade.SaveData namespace, the SaveManager singleton class has two methods used to save or load text data to or from the cloud.

Expand All @@ -65,4 +184,4 @@ Example: `SaveManager.Instance.SaveText("saves/user/save1.txt", "This is save da
### `LoadText(string path)`
Loads data from a given path, returns the loaded data.

Example: `SaveManager.Instance.LoadText("saves/user/save1.txt");`
Example: `SaveManager.Instance.LoadText("saves/user/save1.txt");`
95 changes: 60 additions & 35 deletions devcade-library/Input.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
using Microsoft.Xna.Framework.Input;
using System;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework;

namespace Devcade
{
public static class Input
{
namespace Devcade {
public static class Input {
/// <summary>
/// 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.
/// </summary>
public enum ArcadeButtons
{
public enum ArcadeButtons {
A1=Buttons.X,
A2=Buttons.Y,
A3=Buttons.RightShoulder,
Expand All @@ -35,19 +33,28 @@ public enum ArcadeButtons
public static GamePadState p2State { get; private set; }
public static GamePadState p2LastState { get; private set; }
#endregion

private static bool externalUpdate;
private static bool internalUpdate;

private class UpdateManagerException : ApplicationException {
public UpdateManagerException(string message) : base(message) { }
}

/// <summary>
/// Checks if a button is currently pressed.
/// </summary>
/// <param name="playerNum">The player whose controls should be checked.</param>
/// <param name="button">The button to check.</param>
/// <returns>True when button is pressed, false otherwise.</returns>
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;
}
}

/// <summary>
Expand All @@ -56,12 +63,14 @@ public static bool GetButton(int playerNum, ArcadeButtons button)
/// <param name="playerNum">The player whose controls should be checked.</param>
/// <param name="button">The button to check.</param>
/// <returns>True if the button was pressed last frame, false otherwise.</returns>
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;
private static bool GetLastButton(int playerNum, ArcadeButtons button) {
switch (playerNum) {
case 1 when p1LastState.IsButtonDown((Buttons)button):
case 2 when p2LastState.IsButtonDown((Buttons)button):
return true;
default:
return false;
}
}

/// <summary>
Expand All @@ -70,8 +79,7 @@ private static bool GetLastButton(int playerNum, ArcadeButtons button)
/// <param name="playerNum">The player whose controls should be checked.</param>
/// <param name="button">The button to check.</param>
/// <returns>True if the button transitioned from up to down in the current frame, false otherwise.</returns>
public static bool GetButtonDown(int playerNum, ArcadeButtons button)
{
public static bool GetButtonDown(int playerNum, ArcadeButtons button) {
return (GetButton(playerNum, button) && !GetLastButton(playerNum, button));
}

Expand All @@ -81,8 +89,7 @@ public static bool GetButtonDown(int playerNum, ArcadeButtons button)
/// <param name="playerNum">The player whose controls should be checked.</param>
/// <param name="button">The button to check.</param>
/// <returns>True if the button transitioned from down to up in the current frame, false otherwise.</returns>
public static bool GetButtonUp(int playerNum, ArcadeButtons button)
{
public static bool GetButtonUp(int playerNum, ArcadeButtons button) {
return (!GetButton(playerNum, button) && GetLastButton(playerNum, button));
}

Expand All @@ -92,8 +99,7 @@ public static bool GetButtonUp(int playerNum, ArcadeButtons button)
/// <param name="playerNum">The player whose controls should be checked.</param>
/// <param name="button">The button to check.</param>
/// <returns>True if the button was down last frame and is still down, false otherwise.</returns>
public static bool GetButtonHeld(int playerNum, ArcadeButtons button)
{
public static bool GetButtonHeld(int playerNum, ArcadeButtons button) {
return (GetButton(playerNum, button) && GetLastButton(playerNum, button));
}

Expand All @@ -102,34 +108,53 @@ public static bool GetButtonHeld(int playerNum, ArcadeButtons button)
/// </summary>
/// <param name="playerNum">The player whose controls should be checked.</param>
/// <returns>A Vector2 representing the stick direction.</returns>
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
};
}

/// <summary>
/// Setup initial input states.
/// </summary>
public static void Initialize()
{
public static void Initialize() {
p1State = GamePad.GetState(0);
p2State = GamePad.GetState(1);
p1LastState = GamePad.GetState(0);
p2LastState = GamePad.GetState(1);
}

internal static void UpdateInternal() {
internalUpdate = true;
if (externalUpdate) {
// 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;
p1State = GamePad.GetState(0);
p2State = GamePad.GetState(1);
}

/// <summary>
/// Updates input states.
/// </summary>
public static void Update()
{
public static void Update() {
externalUpdate = true;
if (internalUpdate) {
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 player1, GamePadState player2) GetStates() {
return (player1: p1State, player2: p2State);
}
}
}
Loading