diff --git a/articles/toc.yml b/articles/toc.yml index 753da5dd..8ff716cd 100644 --- a/articles/toc.yml +++ b/articles/toc.yml @@ -154,6 +154,10 @@ href: tutorials/building_2d_games/20_implementing_ui_with_gum/ - name: "21: Customizing Gum UI" href: tutorials/building_2d_games/21_customizing_gum_ui/ + - name: "22: Snake Game Mechanics" + href: tutorials/building_2d_games/22_snake_game_mechanics/ + - name: "23: Completing the Game" + href: tutorials/building_2d_games/23_completing_the_game/ - name: Console Access href: console_access.md - name: Help and Support diff --git a/articles/tutorials/building_2d_games/10_handling_input/10_handling_input.md b/articles/tutorials/building_2d_games/10_handling_input/10_handling_input.md new file mode 100644 index 00000000..4782a026 --- /dev/null +++ b/articles/tutorials/building_2d_games/10_handling_input/10_handling_input.md @@ -0,0 +1,465 @@ +--- +title: "Chapter 10: Handling Input" +description: "Learn how to handle keyboard, mouse, and gamepad input in MonoGame." +--- + +When you play a game, you need ways to control what's happening; using a keyboard or gamepad to control a character or clicking the mouse to navigate a menu, MonoGame helps us handle all these different types of controls through dedicated input classes: + +- [**Keyboard**](xref:Microsoft.Xna.Framework.Input.Keyboard): Detects which keys are being pressed. +- [**Mouse**](xref:Microsoft.Xna.Framework.Input.Mouse): Tracks mouse movement, button clicks, and scroll wheel use. +- [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad): Manages controller input like button presses and thumbstick movement. +- [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel): Manages touch input on devices with a touch panel such as mobile phones and tablets. + +Each of these input types has a `GetState` method that, when called, checks what is happening with that device at that moment. Think of it like taking a snapshot; when you call `GetState`, MonoGame looks at that exact moment to see which buttons are pressed, where the mouse is, or how the controller is being used. + +In this chapter you will, we will learn how to use each of these dedicated input classes to handle player input. + +## Keyboard Input + +The keyboard is often the primary input device for PC games, used for everything from character movement to menu navigation. MonoGame provides the [**Keyboard**](xref:Microsoft.Xna.Framework.Input.Keyboard) class to handle keyboard input, making it easy to detect which keys are being pressed at any time. Calling [**Keyboard.GetState**](xref:Microsoft.Xna.Framework.Input.Keyboard.GetState) will retrieve the current state of the keyboard as a [**KeyboardState**](xref:Microsoft.Xna.Framework.Input.KeyboardState) struct. + +### KeyboardState Struct + +The [**KeyboardState**](xref:Microsoft.Xna.Framework.Input.KeyboardState) struct contains methods that can be used to determine if a keyboard key is currently down or up: + +| Method | Description | +|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| [**IsKeyDown(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)) | Returns `true` if the specified key is down; otherwise, returns `false`. | +| [**IsKeyUp(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys)) | Returns `true` if the specified key is up; otherwise, returns `false`. | + +For example, if we wanted to see if the Space key is down, you could use the following: + +[!code-csharp[](./snippets/keyboardstate.cs)] + +> [!TIP] +> Notice we store the keyboard state in a variable instead of calling [**Keyboard.GetState**](xref:Microsoft.Xna.Framework.Input.Keyboard.GetState) multiple times. This is more efficient and ensures consistent input checking within a single frame. + +## Mouse Input + +The mouse is often the secondary input device for PC games, used for various actions from camera movement to interacting with menus and objects. MonoGame provides the [**Mouse**](xref:Microsoft.Xna.Framework.Input.Mouse) class to handle mouse input, making it easy to detect which buttons are pressed, the position of the mouse cursor, and the value of the scroll wheel. Calling [**Mouse.GetState**](xref:Microsoft.Xna.Framework.Input.Mouse.GetState) will retrieve the current state of the mouse as a [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct. + +### MouseState Struct + +The [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct contains properties that can be used to determine the state of the mouse buttons, the mouse position, and the scroll wheel value: + +| Property | Type | Description | +|----------------------------------------------------------------------------------------|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| [**LeftButton**](xref:Microsoft.Xna.Framework.Input.MouseState.LeftButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the left mouse button. | +| [**MiddleButton**](xref:Microsoft.Xna.Framework.Input.MouseState.MiddleButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the middle mouse button. This is often the button when pressing the scroll wheel down as a button | +| [**Position**](xref:Microsoft.Xna.Framework.Input.MouseState.Position) | [**Point**](xref:Microsoft.Xna.Framework.Point) | Returns the position of the mouse cursor relative to the bounds of the game window. | +| [**RightButton**](xref:Microsoft.Xna.Framework.Input.MouseState.RightButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the right mouse button. | +| [**ScrollWheelValue**](xref:Microsoft.Xna.Framework.Input.MouseState.ScrollWheelValue) | `int` | Returns the **cumulative** scroll wheel value since the start of the game | +| [**XButton1**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton1) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the first extended button on the mouse. | +| [**XButton2**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton2) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the second extended button on the mouse. | + +> [!NOTE] +> [**ScrollWheelValue**](xref:Microsoft.Xna.Framework.Input.MouseState.ScrollWheelValue) returns the cumulative value of the scroll wheel since the start of the game, not how much it moved since the last update. To determine how much it moved between one update and the next, you would need to compare it with the previous frame's value. We'll discuss comparing previous and current frame values for inputs in the next chapter. + +Unlike keyboard input which uses [**IsKeyDown(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys))/[**IsKeyUp(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys)) methods mouse buttons return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState): + +- [**ButtonState.Pressed**](xref:Microsoft.Xna.Framework.Input.ButtonState): The button is being held down. +- [**ButtonState.Released**](xref:Microsoft.Xna.Framework.Input.ButtonState): The button is not being pressed. + +For example, if we wanted to see if the left mouse button is down, you could use the following + +[!code-csharp[](./snippets/mousestate.cs)] + +## Gamepad Input + +Gamepads are often used as a primary input for a game or an alternative for keyboard and mouse controls. MonoGame provides the [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad) class to handle gamepad input, making it easy to detect which buttons are pressed and the value of the thumbsticks. Calling [**GamePad.GetState**](xref:Microsoft.Xna.Framework.Input.GamePad.GetState(Microsoft.Xna.Framework.PlayerIndex)) will retrieve the state of the gamepad as a [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct. Since multiple gamepads can be connected, you will need to supply a [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) value to specify which gamepad state to retrieve. + +### GamePadState Struct + +The [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct and properties that can be used to get the state of the buttons, dpad, triggers, and thumbsticks: + +| Property | Type | Description | +|--------------------------------------------------------------------------------|---------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**Buttons**](xref:Microsoft.Xna.Framework.Input.GamePadState.Buttons) | [**GamePadButtons**](xref:Microsoft.Xna.Framework.Input.GamePadButtons) | Returns a struct that identifies which buttons on the controller are pressed. | +| [**DPad**](xref:Microsoft.Xna.Framework.Input.GamePadState.DPad) | [**GamePadDPad**](xref:Microsoft.Xna.Framework.Input.GamePadDPad) | Returns a struct that identifies which directions on the DPad are pressed. | +| [**IsConnected**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsConnected) | `bool` | Returns a value that indicates whether the controller is connected. | +| [**ThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadState.ThumbSticks) | [**GamePadThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks) | Returns a struct that contains the direction of each thumbstick. Each thumbstick (left and right) are represented as a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value between `-1.0f` and `1.0` for the x- and y-axes. | +| [**Triggers**](xref:Microsoft.Xna.Framework.Input.GamePadState.Triggers) | [**GamePadTriggers**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers) | Returns a struct that contains the value of each trigger. Each trigger (left and right) are represented as a `float` value between `0.0f`, meaning not pressed, and `1.0f`, meaning fully pressed. | + +#### Buttons + +The [**GamePadState.Buttons**](xref:Microsoft.Xna.Framework.Input.GamePadState.Buttons) property returns a [**GamePadButtons**](xref:Microsoft.Xna.Framework.Input.GamePadButtons) struct that can be used to identify which buttons on the controller are pressed. This struct contains the following properties: + +| Property | Type | Description | +|--------------------------------------------------------------------------------------|-------------------------------------------------------------------|-----------------------------------------------| +| [**A**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.A) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the A button | +| [**B**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.B) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the B button | +| [**Back**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Back) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Back button | +| [**BigButton**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.BigButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the BigButton button | +| [**LeftShoulder**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.LeftShoulder) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the LeftShoulder button | +| [**LeftStick**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.LeftStick) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the LeftStick button | +| [**RightShoulder**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.RightShoulder) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the RightShoulder button | +| [**RightStick**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.RightStick) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the RightStick button | +| [**Start**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Start) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Start button | +| [**X**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.X) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the X button | +| [**Y**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Y) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Y button | + +> [!NOTE] +> Recall from [Chapter 01](../01_what_is_monogame/index.md) that MonoGame is a implementation the XNA API. Since XNA was originally created for making games on Windows PC and Xbox 360, the names of the gamepad buttons match those of an Xbox 360 controller. +> +> | Front | Back | +> | :--------------------------------------------------------- | :------------------------------------------------------- | +> | Xbox | | +> | ![Front Of Controller](./images/xbox-controller-front.svg) | ![Back Of Controller](./images/xbox-controller-back.svg) | +> | Playstation | | +> | ![Front Of Controller](./images/ps-controller-front.svg) | ![Back Of Controller](./images/ps-controller-back.svg) | + +Like with the [mouse input](#mousestate-struct), each of these buttons are represented by a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value. For instance, if you wanted to check if the A button is being pressed you could do the following: + +[!code-csharp[](./snippets/gamepadstate.cs)] + +#### DPad + +The [**DPad**](xref:Microsoft.Xna.Framework.Input.GamePadState.DPad) property returns a [**GamePadDPad**](xref:Microsoft.Xna.Framework.Input.GamePadDPad) struct that can be used to identify which DPad buttons on the controller are pressed. This struct contains the following properties: + +| Property | Type | Description | +|------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------| +| [**Down**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Down button. | +| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Left button. | +| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Right button. | +| [**Up**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Up Button. | + +Like with the [Buttons](#buttons), these also return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value to represent the state of the DPad button. For instance, if you wanted to check if the DPad up button is being pressed, you could do the following: + +[!code-csharp[](./snippets/buttonstate.cs)] + +#### Thumbsticks + +The [**ThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadState.ThumbSticks) property returns a [**GamePadThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks) struct that can be used to retrieve the values of the left and right thumbsticks. This struct contains the following properties: + +| Property | Type | Description | +|--------------------------------------------------------------------------|-----------------------------------------------------|------------------------------------------------| +| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks.Left) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The direction the left thumbstick is pressed. | +| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks.Right) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The direction the right thumbstick is pressed. | + +The thumbstick values are represented as a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value: + +- X-axis: A value between `-1.0f` (pushed fully to the left) and `1.0f` (pushed fully to the right). +- Y-axis: A value between `-1.0f` (pushed fully downward) and `1.0f` (pushed fully upward). + +For example, if you wanted to move a sprite using the left thumbstick, you could do the following + +[!code-csharp[](./snippets/thumbstick.cs)] + +> [!IMPORTANT] +> Notice that we inverted the y-axis value of the thumbstick by multiplying it by `-1.0f`. This is necessary because the thumbstick y-axis values range from `-1.0f` (down) to `1.0f` (up). The y-axis of the screen coordinates in MonoGame **increases** downward, as we saw in [Chapter 06](../06_working_with_textures/index.md#drawing-a-texture). +> +> This inversion aligns the thumbstick's y-axis value with the screen movement. + +#### Triggers + +The [**Triggers**](xref:Microsoft.Xna.Framework.Input.GamePadState.Triggers) property returns a [**GamePadTriggers**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers) struct that can be used to retrieve the values of the left and right triggers. This struct contains the following properties: + +| Property | Type | Description | +|-----------------------------------------------------------------------|---------|--------------------------------| +| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers.Left) | `float` | The value of the left trigger. | +| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers.Right) | `float` | The value of the left trigger. | + +The trigger values are represented as a float value between `0.0f` (not pressed) to `1.0f` (fully pressed). The triggers on a gamepad, however, can be either *analog* or *digital* depending the gamepad manufacturer. For gamepads with *digital* triggers, the value will always be either `0.0f` or `1.0f`, as a digital trigger does not register values in between based on the amount of pressure applied to the trigger. + +For example, if we were creating a racing game, the right trigger could be used for acceleration like the following: + +[!code-csharp[](./snippets/triggers.cs)] + +### GamePadState Methods + +The [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct also contains two methods that can be used to get information about the device's inputs as either being up or down: + +| Method | Description | +|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**IsButtonDown(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonDown(Microsoft.Xna.Framework.Input.Buttons)) | Returns a value that indicates whether the specified button is down. Multiple [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) values can be given using the bitwise OR `|` operator. When multiple buttons are given, the return value indicates if all buttons specified are down, not just one of them. | +| [**IsButtonUp(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonUp(Microsoft.Xna.Framework.Input.Buttons)) | Returns a value that indicates whether the specified button is up. Multiple [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) values can be given using the bitwise OR `|` operator. When multiple buttons are given, the return value indicates if all buttons specified are up, not just one of them. | + +You can use the [**IsButtonDown(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonDown(Microsoft.Xna.Framework.Input.Buttons)) and [**IsButtonUp(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonUp(Microsoft.Xna.Framework.Input.Buttons)) methods to get the state of all buttons, including the DPad. The following is a complete list of all of the [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) enum values: + +- [**Buttons.A**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.B**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.Back**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.BigButton**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadDown**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadRight**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadUp**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftShoulder**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftStick**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickDown**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickRight**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickUp**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftTrigger**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.None**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightShoulder**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStick**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickDown**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickRight**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickUp**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightTrigger**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.Start**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.X**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.Y**](xref:Microsoft.Xna.Framework.Input.Buttons) + +> [!CAUTION] +> While you can use these methods to get the state of any of these button inputs, the state will only tell you if it is being pressed or released. For the actual thumbstick values and trigger values, you would need to use the properties instead. + +For example, if we wanted to check if the A button on the the first gamepad is pressed, you could use the following: + +[!code-csharp[](./snippets/isbuttondown.cs)] + +### GamePad Vibration + +Another capability of gamepads is haptic feedback through vibration motors. MonoGame allows you to control this feature using the [**GamePad.SetVibration**](xref:Microsoft.Xna.Framework.Input.GamePad.SetVibration(Microsoft.Xna.Framework.PlayerIndex,System.Single,System.Single)) method. This method takes three parameters: + +1. The [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) of the gamepad to vibrate. +2. The intensity of the left motor (from `0.0f` for no vibration to `1.0f` for maximum vibration). +3. The intensity of the right motor (using the same scale). + +Most modern gamepads have two vibration motors, a larger one (usually the left motor) for low-frequency rumble and a smaller one (usually the right motor) for high-frequency feedback. By controlling these independently, you can create various haptic effects: + +[!code-csharp[](./snippets/vibration.cs)] + +## TouchPanel Input + +For mobile devices such as Android/iOS phones and tablets, the primary input device is the touch panel screen. Touching a location on the screen is similar to clicking a location on your computer with a mouse. MonoGame provides the [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) class to handle touch input. + +The [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) class offers two ways of retrieving information about touch input: + +- [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) retrieves a [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) struct that contains [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) values for each point of touch on the touch panel. +- [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) retrieves a [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct that contains information about recent gestures that have been performed like a vertical or horizontal drag across the screen. + +### TouchCollection + +When calling [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) a [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) struct is returned. This collection contains a [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) value for each point of touch. + +#### TouchLocation + +Each [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) value in a touch collection contains the following properties: + +| Property | Type | Description | +|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [**Id**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Id) | `int` | The id of the touch location. | +| [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Position) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The position of the touch location. | +| [**Pressure**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Pressure) | `float` | The amount of pressure applied at the touch location. **(Only available for Android devices.)** | +| [**State**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | [**TouchLocationState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState) | The current state of the touch location. | + +The important properties of the location are the [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Position) and the [**State**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) The position property will tell us the location of the touch event, and the state can be one of the following values: + +| State | Description | +|------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location position is invalid. | +| [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location position was updated or pressed at the same position. | +| [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location was pressed. | +| [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location was released. | + +When the state is moved or pressed, then we know that location on the touch panel is being touched. So we can capture it and use it like the following: + +[!code-csharp[](./snippets/touchstate.cs)] + +> [!NOTE] +> Unlike mouse input which only tracks a single point, [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) supports multiple simultaneous touch points. The [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) contains all active touch points, which is why we loop through them in the sample above. + +The state of a touch location progresses through the states typically in order of: + +- [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State): Initial contact with the screen. +- [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) : Touch point moved while maintaining contact. +- [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State): Contact with screen ended. +- [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) : Touch data is invalid (using when tracking data is lost). + +### GestureSample + +When calling [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) a [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct containing the information about recent gestures that have been performed is returned. The [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct contains the following properties: + +| Property | Type | Description | +|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| [**Delta**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Delta) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the delta information about the first touch-point in the gesture sample. | +| [**Delta2**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Delta2) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the delta information about the second touch-point in the gesture sample. | +| [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.GestureType) | [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | Gets the type of the gesture. | +| [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Position) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the position of the first touch-point in the gesture sample. | +| [**Position2**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Position2) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the position of the second touch-point in the gesture sample. | + +> [!NOTE] +> Gestures have two delta properties and two position properties. This is because some gestures require multiple touch inputs to perform, such as performing a pinch to zoom in or out. You would need the location of both touch points to determine the correct zoom to apply during the gesture. + +To determine what type of gesture is performed, we can get that from the [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.GestureType) property which will be one of the following values: + +| Gesture Type | Description | +|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [**DoubleTap**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user double tapped the device twice which is always preceded by a Tap gesture. | +| [**DragComplete**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | States completion of a drag gesture (VerticalDrag, HorizontalDrag, or FreeDrag). | +| [**Flick**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | States that a touch was combined with a quick swipe. | +| [**FreeDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a point and the performed a free-form drag. | +| [**Hold**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a single point for approximately one second. | +| [**HorizontalDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched the screen and performed either a left-to-right or right-to-left drag gesture. | +| [**None**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | No gesture. | +| [**Pinch**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user converged or diverged two touch-points on the screen which is like a two-finger drag. | +| [**PinchComplete**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | An in-progress pinch gesture was completed. | +| [**Tap**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a single point. | +| [**VerticalDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched the screen and performed either a top-to-bottom or bottom-to-top drag gesture. | + +> [!IMPORTANT] +> Before gestures can be detected, they have to be enabled using [**TouchPanel.EnabledGestures**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.EnabledGestures). This can be done in [**Game.Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) like the following: +> +> [!code-csharp[](./snippets/enablegestures.cs)] + +The following is an example of using a gesture to detect horizontal and vertical drags: + +[!code-csharp[](./snippets/gestures.cs)] + +> [!IMPORTANT] +> Notice above that we use a `while` loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) as the condition for the loop. The reason we do this is because when a user performs a gesture, such as a horizontal drag across the screen, very quickly, what can often occurs is a series of multiple small drag gestures are registered and queued. +> +> Each time [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) is called, it will dequeue the next gesture. So to ensure that we handle the complete gesture, we loop the gesture queue until there are none left. + +## Implementing Input in Our Game + +For our game, we're going to implement keyboard and gamepad controls based on the following criteria: + +| Keyboard Input | Gamepad Input | Description | +|---------------------------|---------------------------------------------|--------------------------------------| +| [Keys.W] and [Keys.Up] | [Thumbstick.Left.Y] and [Buttons.DPadUp] | Moves the slime up the screen. | +| [Keys.S] and [Keys.Down] | [Thumbstick.Left.Y] and [Buttons.DPadDown] | Moves the slime down the screen | +| [Keys.A] and [Keys.Left] | [Thumbstick.Left.X] and [Buttons.DPadLeft] | Moves the slime left on the screen. | +| [Keys.D] and [Keys.Right] | [Thumbstick.Left.X] and [Buttons.DPadRight] | Moves the slime right on the screen. | +| [Keys.Space] | [Buttons.A] | Increased the speed of the slime. | + +Open *Game1.cs* and update it with the following: + +[!code-csharp[](./snippets/game1.cs?highlight=17-21,62-66,71-159,170)] + +The key changes made here are: + +1. The `_slimePosition` field was added to track the position of the slime as it moves. +2. The `MOVEMENT_SPEED` constant was added to use as the base multiplier for the movement speed. +3. The `CheckKeyboardInput` method was added which checks for input from the keyboard based on the input table above and moves the slime based on the keyboard input detected. +4. The `CheckGamePadInput` method was added which checks for input from the gamepad based on the input table above and moves the slime based the gamepad input detected. + + > [!NOTE] + > The gamepad implementation includes a priority system for directional input. The code prioritizes the analog thumbstick values over the digital DPad buttons. This design choice provides players with more nuanced control, as analog inputs allow for a variable movements speed based on how far the thumbstick is pushed, while DPad buttons only provide on/off input states. The code first checks if either thumbstick axis has a non-zero value, and only falls back to DPad input when the thumbstick is centered. + > + > To enhance player experience, the gamepad implementation also includes gamepad vibration when the speed boost is activated. Haptic feedback like this creates a more immersive experience by engaging additional senses for the player beyond just visual and auditory feedback. + +5. In [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) `CheckKeyboardInput` and `CheckGamePadInput` methods are called. +6. In [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), the slime is now drawn using `_slimePosition` as the position. + +Running the game now, you can move the slime around using the keyboard with the arrow keys or WASD keys. If you have a gamepad plugged in you can also use the DPad and left thumbstick. + +| ![Figure 10-1: The slime moving around based on device input](./videos/input-moving-slime.webm) | +|:-----------------------------------------------------------------------------------------------:| +| **Figure 10-1: The slime moving around based on device input** | + +## Input Buffering + +While checking for input every frame works well for continuous actions like movement, many games benefit from more sophisticated input handling techniques. One such technique is **input buffering**, which can significantly improve how responsive controls feel to players. + +### Understanding Input Buffering + +Input buffering is a technique where the game temporarily stores player inputs that cannot be immediately processed. Instead of discarding these inputs, they are placed in a queue and processed in order when the game is ready to handle them. + +Input buffering is particularly valuable in games where: + +- Actions occur at fixed intervals rather than continuously (like turn-based games or grid movement). +- Precise timing is required for complex input sequences (like fighting games). +- Multiple rapid inputs need to be remembered in order (like quick directional changes). + +Without input buffering, players must time their inputs perfectly to align with the game's update cycle. With buffering, the game becomes more forgiving and responsive by: + +1. Storing inputs that arrive between action updates. +2. Preserving the order of inputs for more predictable behavior. +3. Creating a sense that the game is actually listening to the player. + +### Implementing a Simple Input Buffer + +A basic input buffer can be implemented using a queue data structure, which follows a First-In-First-Out (FIFO) pattern: + +[!code-csharp[](./snippets/inputbuffer.cs)] + +> [!NOTE] +> The [`Queue`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1?view=net-9.0>) is a First In, First Out (FIFO) collection in C#. When you add items with `Enqueue()`, they join the end of the line, and when you retrieve items with `Dequeue()`, you always get the oldest item (the one at the front of the line). Think of it like people waiting in line - the first person to arrive is the first one served. +> +> This contrasts with a [`Stack`](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1?view=net-9.0>), which follows Last In, First Out (LIFO) behavior, where the most recently added item is the first one retrieved. + +The size of an input buffer is an important design decision. If it's too small, players might still feel the game isn't responsive enough. If it's too large, the game might feel like it's playing itself by working through a backlog of commands. + +### When to Use Input Buffering + +Consider implementing input buffering in your game when: + +- Players complain about the game feeling "unresponsive". +- Your game uses fixed-interval updates for certain mechanics. +- Actions require precise timing that is difficult for players to hit consistently. +- You want to allow players to "queue up" their next few moves. + +We'll see a practical implementation of input buffering in [Chapter 23](../23_completing_the_game/index.md) when we finalize our snake-like game mechanics, where timing and direction changes are critical to gameplay. + +## Conclusion + +In this chapter, you learned how to: + +- Handle keyboard input to detect key presses. +- Handle mouse input including button clicks and cursor position. +- Work with gamepad controls including buttons, thumbsticks, and vibration. +- Understand touch input for mobile devices including touch points and gestures. +- Implement movement controls using different input methods. +- Consider controller-specific details like coordinate systems and analog vs digital input. + +In the next chapter, we'll learn how to track previous input states to handle single-press events and implement an input management system to simplify some of the complexity of handling input. + +## Test Your Knowledge + +1. Why do we store the result of `GetState` in a variable instead of calling it multiple times? + + :::question-answer + Storing the state in a variable is more efficient and ensures consistent input checking within a frame. Each `GetState` call polls the device, which can impact performance if called repeatedly. + ::: + +2. What's the main difference between how keyboard and mouse/gamepad button states are checked? + + :::question-answer + Keyboard input uses [**IsKeyUp**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys))/[**IsKeyDown**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)) methods, while mouse and gamepad buttons return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value (Pressed or Released). + ::: + +3. When using thumbstick values for movement, why do we multiply the Y value by -1? + + :::question-answer + The thumbstick Y-axis values (-1.0f down to 1.0f up) are inverted compared to MonoGame's screen coordinate system (Y increases downward). Multiplying by -1 aligns the thumbstick direction with screen movement. + ::: + +4. What's the difference between analog and digital trigger input on a gamepad? + + :::question-answer + Analog triggers provide values between 0.0f and 1.0f based on how far they're pressed, while digital triggers only report 0.0f (not pressed) or 1.0f (pressed). This affects how you handle trigger input in your game. + ::: + +5. What's the key difference between [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) and [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture)? + + :::question-answer + [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) returns information about current touch points on the screen, while [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) provides information about specific gesture patterns like taps, drags, and pinches that have been performed. + ::: + +6. Why do we use a while loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) when reading gestures? + + :::question-answer + Quick gestures can generate multiple gesture events that are queued. Using a while loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) ensures we process all queued gestures, as [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) only returns one gesture at a time. + ::: + +7. How does touch input differ from mouse input in terms of handling multiple input points? + + :::question-answer + Touch input can handle multiple simultaneous touch points through the [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection), while mouse input only tracks a single cursor position. This allows touch input to support features like multi-touch gestures that aren't possible with a mouse. + ::: + +8. What are the different states a [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) can have and what do they indicate? + + :::question-answer + A [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) can have four states: + + - [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Initial contact with the screen + - [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Touch point moved while maintaining contact + - [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Contact with the screen ended + - [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Touch data is not valid or tracking was lost + + ::: diff --git a/articles/tutorials/building_2d_games/10_handling_input/index.md b/articles/tutorials/building_2d_games/10_handling_input/index.md index 50c6d200..26ca0809 100644 --- a/articles/tutorials/building_2d_games/10_handling_input/index.md +++ b/articles/tutorials/building_2d_games/10_handling_input/index.md @@ -1,423 +1,423 @@ ---- -title: "Chapter 10: Handling Input" -description: "Learn how to handle keyboard, mouse, and gamepad input in MonoGame." ---- - -When you play a game, you need ways to control what is happening; using a keyboard or gamepad to control a character or clicking the mouse to navigate a menu, MonoGame helps us handle all these different types of controls through dedicated input classes: - -- [**Keyboard**](xref:Microsoft.Xna.Framework.Input.Keyboard): Detects which keys are being pressed. -- [**Mouse**](xref:Microsoft.Xna.Framework.Input.Mouse): Tracks mouse movement, button clicks, and scroll wheel use. -- [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad): Manages controller input like button presses and thumbstick movement. -- [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel): Manages touch input on devices with a touch panel such as mobile phones and tablets. - -Each of these input types has a `GetState` method that, when called, checks what is happening with that device at that moment. Think of it like taking a snapshot; when you call `GetState`, MonoGame looks at that exact moment to see which buttons are pressed, where the mouse is, or how the controller is being used. - -In this chapter you will, we will learn how to use each of these dedicated input classes to handle player input. - -## Keyboard Input - -The keyboard is often the primary input device for PC games, used for everything from character movement to menu navigation. MonoGame provides the [**Keyboard**](xref:Microsoft.Xna.Framework.Input.Keyboard) class to handle keyboard input, making it easy to detect which keys are being pressed at any time. Calling [**Keyboard.GetState**](xref:Microsoft.Xna.Framework.Input.Keyboard.GetState) will retrieve the current state of the keyboard as a [**KeyboardState**](xref:Microsoft.Xna.Framework.Input.KeyboardState) struct. - -### KeyboardState Struct - -The [**KeyboardState**](xref:Microsoft.Xna.Framework.Input.KeyboardState) struct contains methods that can be used to determine if a keyboard key is currently down or up: - -| Method | Description | -|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| -| [**IsKeyDown(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)) | Returns `true` if the specified key is down; otherwise, returns `false`. | -| [**IsKeyUp(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys)) | Returns `true` if the specified key is up; otherwise, returns `false`. | - -For example, if we wanted to see if the Space key is down, you could use the following: - -[!code-csharp[](./snippets/keyboardstate.cs)] - -> [!TIP] -> Notice we store the keyboard state in a variable instead of calling [**Keyboard.GetState**](xref:Microsoft.Xna.Framework.Input.Keyboard.GetState) multiple times. This is more efficient and ensures consistent input checking within a single frame. - -## Mouse Input - -The mouse is often the secondary input device for PC games, used for various actions from camera movement to interacting with menus and objects. MonoGame provides the [**Mouse**](xref:Microsoft.Xna.Framework.Input.Mouse) class to handle mouse input, making it easy to detect which buttons are pressed, the position of the mouse cursor, and the value of the scroll wheel. Calling [**Mouse.GetState**](xref:Microsoft.Xna.Framework.Input.Mouse.GetState) will retrieve the current state of the mouse as a [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct. - -### MouseState Struct - -The [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct contains properties that can be used to determine the state of the mouse buttons, the mouse position, and the scroll wheel value: - -| Property | Type | Description | -|----------------------------------------------------------------------------------------|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| -| [**LeftButton**](xref:Microsoft.Xna.Framework.Input.MouseState.LeftButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the left mouse button. | -| [**MiddleButton**](xref:Microsoft.Xna.Framework.Input.MouseState.MiddleButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the middle mouse button. This is often the button when pressing the scroll wheel down as a button | -| [**Position**](xref:Microsoft.Xna.Framework.Input.MouseState.Position) | [**Point**](xref:Microsoft.Xna.Framework.Point) | Returns the position of the mouse cursor relative to the bounds of the game window. | -| [**RightButton**](xref:Microsoft.Xna.Framework.Input.MouseState.RightButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the right mouse button. | -| [**ScrollWheelValue**](xref:Microsoft.Xna.Framework.Input.MouseState.ScrollWheelValue) | `int` | Returns the **cumulative** scroll wheel value since the start of the game | -| [**XButton1**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton1) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the first extended button on the mouse. | -| [**XButton2**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton2) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the second extended button on the mouse. | - -> [!NOTE] -> [**ScrollWheelValue**](xref:Microsoft.Xna.Framework.Input.MouseState.ScrollWheelValue) returns the cumulative value of the scroll wheel since the start of the game, not how much it moved since the last update. To determine how much it moved between one update and the next, you would need to compare it with the previous frame's value. We will discuss comparing previous and current frame values for inputs in the next chapter. - -Unlike keyboard input which uses [**IsKeyDown(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys))/[**IsKeyUp(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys)) methods mouse buttons return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState): - -- [**ButtonState.Pressed**](xref:Microsoft.Xna.Framework.Input.ButtonState): The button is being held down. -- [**ButtonState.Released**](xref:Microsoft.Xna.Framework.Input.ButtonState): The button is not being pressed. - -For example, if we wanted to see if the left mouse button is down, you could use the following - -[!code-csharp[](./snippets/mousestate.cs)] - -## Gamepad Input - -Gamepads are often used as a primary input for a game or an alternative for keyboard and mouse controls. MonoGame provides the [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad) class to handle gamepad input, making it easy to detect which buttons are pressed and the value of the thumbsticks. Calling [**GamePad.GetState**](xref:Microsoft.Xna.Framework.Input.GamePad.GetState(Microsoft.Xna.Framework.PlayerIndex)) will retrieve the state of the gamepad as a [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct. Since multiple gamepads can be connected, you will need to supply a [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) value to specify which gamepad state to retrieve. - -### GamePadState Struct - -The [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct and properties that can be used to get the state of the buttons, dpad, triggers, and thumbsticks: - -| Property | Type | Description | -|--------------------------------------------------------------------------------|---------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [**Buttons**](xref:Microsoft.Xna.Framework.Input.GamePadState.Buttons) | [**GamePadButtons**](xref:Microsoft.Xna.Framework.Input.GamePadButtons) | Returns a struct that identifies which buttons on the controller are pressed. | -| [**DPad**](xref:Microsoft.Xna.Framework.Input.GamePadState.DPad) | [**GamePadDPad**](xref:Microsoft.Xna.Framework.Input.GamePadDPad) | Returns a struct that identifies which directions on the DPad are pressed. | -| [**IsConnected**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsConnected) | `bool` | Returns a value that indicates whether the controller is connected. | -| [**ThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadState.ThumbSticks) | [**GamePadThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks) | Returns a struct that contains the direction of each thumbstick. Each thumbstick (left and right) are represented as a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value between `-1.0f` and `1.0` for the x- and y-axes. | -| [**Triggers**](xref:Microsoft.Xna.Framework.Input.GamePadState.Triggers) | [**GamePadTriggers**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers) | Returns a struct that contains the value of each trigger. Each trigger (left and right) are represented as a `float` value between `0.0f`, meaning not pressed, and `1.0f`, meaning fully pressed. | - -#### Buttons - -The [**GamePadState.Buttons**](xref:Microsoft.Xna.Framework.Input.GamePadState.Buttons) property returns a [**GamePadButtons**](xref:Microsoft.Xna.Framework.Input.GamePadButtons) struct that can be used to identify which buttons on the controller are pressed. This struct contains the following properties: - -| Property | Type | Description | -|--------------------------------------------------------------------------------------|-------------------------------------------------------------------|-----------------------------------------------| -| [**A**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.A) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the A button | -| [**B**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.B) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the B button | -| [**Back**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Back) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Back button | -| [**BigButton**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.BigButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the BigButton button | -| [**LeftShoulder**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.LeftShoulder) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the LeftShoulder button | -| [**LeftStick**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.LeftStick) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the LeftStick button | -| [**RightShoulder**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.RightShoulder) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the RightShoulder button | -| [**RightStick**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.RightStick) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the RightStick button | -| [**Start**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Start) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Start button | -| [**X**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.X) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the X button | -| [**Y**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Y) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Y button | - -> [!NOTE] -> Recall from [Chapter 01](../01_what_is_monogame/index.md) that MonoGame is a implementation the XNA API. Since XNA was originally created for making games on Windows PC and Xbox 360, the names of the gamepad buttons match those of an Xbox 360 controller. -> -> The [**BigButton**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.BigButton) refers to the large, centrally located button on special Xbox 360 controllers created for games like "Scene It?" - this button is not present on standard controllers and is not mapped to any button on modern controllers. It remains in the API for backward compatibility with XNA. -> -> | Front | Back | -> | :--------------------------------------------------------- | :------------------------------------------------------- | -> | Xbox | | -> | ![Front Of Controller](./images/xbox-controller-front.svg) | ![Back Of Controller](./images/xbox-controller-back.svg) | -> | Playstation | | -> | ![Front Of Controller](./images/ps-controller-front.svg) | ![Back Of Controller](./images/ps-controller-back.svg) | - -Like with the [mouse input](#mousestate-struct), each of these buttons are represented by a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value. For instance, if you wanted to check if the A button is being pressed you could do the following: - -[!code-csharp[](./snippets/gamepadstate.cs)] - -#### DPad - -The [**DPad**](xref:Microsoft.Xna.Framework.Input.GamePadState.DPad) property returns a [**GamePadDPad**](xref:Microsoft.Xna.Framework.Input.GamePadDPad) struct that can be used to identify which DPad buttons on the controller are pressed. This struct contains the following properties: - -| Property | Type | Description | -|------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------| -| [**Down**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Down button. | -| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Left button. | -| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Right button. | -| [**Up**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Up Button. | - -Like with the [Buttons](#buttons), these also return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value to represent the state of the DPad button. For instance, if you wanted to check if the DPad up button is being pressed, you could do the following: - -[!code-csharp[](./snippets/buttonstate.cs)] - -#### Thumbsticks - -The [**ThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadState.ThumbSticks) property returns a [**GamePadThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks) struct that can be used to retrieve the values of the left and right thumbsticks. This struct contains the following properties: - -| Property | Type | Description | -|--------------------------------------------------------------------------|-----------------------------------------------------|------------------------------------------------| -| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks.Left) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The direction the left thumbstick is pressed. | -| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks.Right) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The direction the right thumbstick is pressed. | - -The thumbstick values are represented as a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value: - -- X-axis: A value between `-1.0f` (pushed fully to the left) and `1.0f` (pushed fully to the right). -- Y-axis: A value between `-1.0f` (pushed fully downward) and `1.0f` (pushed fully upward). - -For example, if you wanted to move a sprite using the left thumbstick, you could do the following - -[!code-csharp[](./snippets/thumbstick.cs)] - -> [!IMPORTANT] -> Notice that we inverted the y-axis value of the thumbstick by multiplying it by `-1.0f`. This is necessary because the thumbstick y-axis values range from `-1.0f` (down) to `1.0f` (up). The y-axis of the screen coordinates in MonoGame **increases** downward, as we saw in [Chapter 06](../06_working_with_textures/index.md#drawing-a-texture). -> -> This inversion aligns the thumbstick's y-axis value with the screen movement. - -#### Triggers - -The [**Triggers**](xref:Microsoft.Xna.Framework.Input.GamePadState.Triggers) property returns a [**GamePadTriggers**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers) struct that can be used to retrieve the values of the left and right triggers. This struct contains the following properties: - -| Property | Type | Description | -|-----------------------------------------------------------------------|---------|--------------------------------| -| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers.Left) | `float` | The value of the left trigger. | -| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers.Right) | `float` | The value of the left trigger. | - -The trigger values are represented as a float value between `0.0f` (not pressed) to `1.0f` (fully pressed). The triggers on a gamepad, however, can be either *analog* or *digital* depending the gamepad manufacturer. For gamepads with *digital* triggers, the value will always be either `0.0f` or `1.0f`, as a digital trigger does not register values in between based on the amount of pressure applied to the trigger. - -For example, if we were creating a racing game, the right trigger could be used for acceleration like the following: - -[!code-csharp[](./snippets/triggers.cs)] - -### GamePadState Methods - -The [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct also contains two methods that can be used to get information about the device's inputs as either being up or down: - -| Method | Description | -|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [**IsButtonDown(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonDown(Microsoft.Xna.Framework.Input.Buttons)) | Returns a value that indicates whether the specified button is down. Multiple [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) values can be given using the bitwise OR `|` operator. When multiple buttons are given, the return value indicates if all buttons specified are down, not just one of them. | -| [**IsButtonUp(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonUp(Microsoft.Xna.Framework.Input.Buttons)) | Returns a value that indicates whether the specified button is up. Multiple [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) values can be given using the bitwise OR `|` operator. When multiple buttons are given, the return value indicates if all buttons specified are up, not just one of them. | - -You can use the [**IsButtonDown(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonDown(Microsoft.Xna.Framework.Input.Buttons)) and [**IsButtonUp(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonUp(Microsoft.Xna.Framework.Input.Buttons)) methods to get the state of all buttons, including the DPad. The following is a complete list of all of the [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) enum values: - -- [**Buttons.A**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.B**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.Back**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.BigButton**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.DPadDown**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.DPadLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.DPadRight**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.DPadUp**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftShoulder**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftStick**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftThumbstickDown**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftThumbstickLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftThumbstickRight**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftThumbstickUp**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.LeftTrigger**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.None**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightShoulder**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightStick**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightStickDown**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightStickLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightStickRight**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightStickUp**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.RightTrigger**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.Start**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.X**](xref:Microsoft.Xna.Framework.Input.Buttons) -- [**Buttons.Y**](xref:Microsoft.Xna.Framework.Input.Buttons) - -> [!CAUTION] -> While you can use these methods to get the state of any of these button inputs, the state will only tell you if it is being pressed or released. For the actual thumbstick values and trigger values, you would need to use the properties instead. - -For example, if we wanted to check if the A button on the the first gamepad is pressed, you could use the following: - -[!code-csharp[](./snippets/isbuttondown.cs)] - -### GamePad Vibration - -Another capability of gamepads is haptic feedback through vibration motors. MonoGame allows you to control this feature using the [**GamePad.SetVibration**](xref:Microsoft.Xna.Framework.Input.GamePad.SetVibration(Microsoft.Xna.Framework.PlayerIndex,System.Single,System.Single)) method. This method takes three parameters: - -1. The [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) of the gamepad to vibrate. -2. The intensity of the left motor (from `0.0f` for no vibration to `1.0f` for maximum vibration). -3. The intensity of the right motor (using the same scale). - -Most modern gamepads have two vibration motors, a larger one (usually the left motor) for low-frequency rumble and a smaller one (usually the right motor) for high-frequency feedback. By controlling these independently, you can create various haptic effects: - -[!code-csharp[](./snippets/vibration.cs)] - -## TouchPanel Input - -For mobile devices such as Android/iOS phones and tablets, the primary input device is the touch panel screen. Touching a location on the screen is similar to clicking a location on your computer with a mouse. MonoGame provides the [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) class to handle touch input. - -The [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) class offers two ways of retrieving information about touch input: - -- [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) retrieves a [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) struct that contains [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) values for each point of touch on the touch panel. -- [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) retrieves a [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct that contains information about recent gestures that have been performed like a vertical or horizontal drag across the screen. - -### TouchCollection - -When calling [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) a [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) struct is returned. This collection contains a [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) value for each point of touch. - -#### TouchLocation - -Each [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) value in a touch collection contains the following properties: - -| Property | Type | Description | -|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| -| [**Id**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Id) | `int` | The id of the touch location. | -| [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Position) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The position of the touch location. | -| [**Pressure**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Pressure) | `float` | The amount of pressure applied at the touch location. **(Only available for Android devices.)** | -| [**State**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | [**TouchLocationState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState) | The current state of the touch location. | - -The important properties of the location are the [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Position) and the [**State**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) The position property will tell us the location of the touch event, and the state can be one of the following values: - -| State | Description | -|------------------------------------------------------------------------------|---------------------------------------------------------------------------| -| [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location position is invalid. | -| [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location position was updated or pressed at the same position. | -| [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location was pressed. | -| [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location was released. | - -When the state is moved or pressed, then we know that location on the touch panel is being touched. So we can capture it and use it like the following: - -[!code-csharp[](./snippets/touchstate.cs)] - -> [!NOTE] -> Unlike mouse input which only tracks a single point, [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) supports multiple simultaneous touch points. The [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) contains all active touch points, which is why we loop through them in the sample above. - -The state of a touch location progresses through the states typically in order of: - -- [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State): Initial contact with the screen. -- [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) : Touch point moved while maintaining contact. -- [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State): Contact with screen ended. -- [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) : Touch data is invalid (using when tracking data is lost). - -### GestureSample - -When calling [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) a [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct containing the information about recent gestures that have been performed is returned. The [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct contains the following properties: - -| Property | Type | Description | -|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------------------| -| [**Delta**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Delta) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the delta information about the first touch-point in the gesture sample. | -| [**Delta2**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Delta2) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the delta information about the second touch-point in the gesture sample. | -| [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.GestureType) | [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | Gets the type of the gesture. | -| [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Position) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the position of the first touch-point in the gesture sample. | -| [**Position2**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Position2) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the position of the second touch-point in the gesture sample. | - -> [!NOTE] -> Gestures have two delta properties and two position properties. This is because some gestures require multiple touch inputs to perform, such as performing a pinch to zoom in or out. You would need the location of both touch points to determine the correct zoom to apply during the gesture. - -To determine what type of gesture is performed, we can get that from the [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.GestureType) property which will be one of the following values: - -| Gesture Type | Description | -|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| -| [**DoubleTap**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user double tapped the device twice which is always preceded by a Tap gesture. | -| [**DragComplete**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | States completion of a drag gesture (VerticalDrag, HorizontalDrag, or FreeDrag). | -| [**Flick**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | States that a touch was combined with a quick swipe. | -| [**FreeDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a point and the performed a free-form drag. | -| [**Hold**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a single point for approximately one second. | -| [**HorizontalDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched the screen and performed either a left-to-right or right-to-left drag gesture. | -| [**None**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | No gesture. | -| [**Pinch**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user converged or diverged two touch-points on the screen which is like a two-finger drag. | -| [**PinchComplete**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | An in-progress pinch gesture was completed. | -| [**Tap**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a single point. | -| [**VerticalDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched the screen and performed either a top-to-bottom or bottom-to-top drag gesture. | - -> [!IMPORTANT] -> Before gestures can be detected, they have to be enabled using [**TouchPanel.EnabledGestures**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.EnabledGestures). This can be done in [**Game.Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) like the following: -> -> [!code-csharp[](./snippets/enablegestures.cs)] - -The following is an example of using a gesture to detect horizontal and vertical drags: - -[!code-csharp[](./snippets/gestures.cs)] - -> [!IMPORTANT] -> Notice above that we use a `while` loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) as the condition for the loop. The reason we do this is because when a user performs a gesture, such as a horizontal drag across the screen, very quickly, what can often occurs is a series of multiple small drag gestures are registered and queued. -> -> Each time [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) is called, it will dequeue the next gesture. So to ensure that we handle the complete gesture, we loop the gesture queue until there are none left. - -## Implementing Input in Our Game - -For our game, we are going to implement keyboard and gamepad controls based on the following criteria: - -| Keyboard Input | Gamepad Input | Description | -|---------------------------|---------------------------------------------|--------------------------------------| -| [Keys.W] and [Keys.Up] | [Thumbstick.Left.Y] and [Buttons.DPadUp] | Moves the slime up the screen. | -| [Keys.S] and [Keys.Down] | [Thumbstick.Left.Y] and [Buttons.DPadDown] | Moves the slime down the screen | -| [Keys.A] and [Keys.Left] | [Thumbstick.Left.X] and [Buttons.DPadLeft] | Moves the slime left on the screen. | -| [Keys.D] and [Keys.Right] | [Thumbstick.Left.X] and [Buttons.DPadRight] | Moves the slime right on the screen. | -| [Keys.Space] | [Buttons.A] | Increased the speed of the slime. | - -Open *Game1.cs* and update it with the following: - -[!code-csharp[](./snippets/game1.cs?highlight=17-21,62-66,71-159,170)] - -The key changes made here are: - -1. The `_slimePosition` field was added to track the position of the slime as it moves. -2. The `MOVEMENT_SPEED` constant was added to use as the base multiplier for the movement speed. -3. The `CheckKeyboardInput` method was added which checks for input from the keyboard based on the input table above and moves the slime based on the keyboard input detected. -4. The `CheckGamePadInput` method was added which checks for input from the gamepad based on the input table above and moves the slime based the gamepad input detected. - - > [!NOTE] - > The gamepad implementation includes a priority system for directional input. The code prioritizes the analog thumbstick values over the digital DPad buttons. This design choice provides players with more nuanced control, as analog inputs allow for a variable movements speed based on how far the thumbstick is pushed, while DPad buttons only provide on/off input states. The code first checks if either thumbstick axis has a non-zero value, and only falls back to DPad input when the thumbstick is centered. - > - > To enhance player experience, the gamepad implementation also includes gamepad vibration when the speed boost is activated. Haptic feedback like this creates a more immersive experience by engaging additional senses for the player beyond just visual and auditory feedback. - -5. In [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) `CheckKeyboardInput` and `CheckGamePadInput` methods are called. -6. In [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), the slime is now drawn using `_slimePosition` as the position. - -Running the game now, you can move the slime around using the keyboard with the arrow keys or WASD keys. If you have a gamepad plugged in you can also use the DPad and left thumbstick. - -| ![Figure 10-1: The slime moving around based on device input](./videos/input-moving-slime.webm) | -|:-----------------------------------------------------------------------------------------------:| -| **Figure 10-1: The slime moving around based on device input** | - -## Conclusion - -In this chapter, you accomplished the following: - -- Handle keyboard input to detect key presses. -- Handle mouse input including button clicks and cursor position. -- Work with gamepad controls including buttons, thumbsticks, and vibration. -- Understand touch input for mobile devices including touch points and gestures. -- Implement movement controls using different input methods. -- Consider controller-specific details like coordinate systems and analog vs digital input. - -In the next chapter, we will learn how to track previous input states to handle single-press events and implement an input management system to simplify some of the complexity of handling input. - -## Test Your Knowledge - -1. Why do we store the result of `GetState` in a variable instead of calling it multiple times? - - :::question-answer - Storing the state in a variable is more efficient and ensures consistent input checking within a frame. Each `GetState` call polls the device, which can impact performance if called repeatedly. - ::: - -2. What is the main difference between how keyboard and mouse/gamepad button states are checked? - - :::question-answer - Keyboard input uses [**IsKeyUp**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys))/[**IsKeyDown**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)) methods, while mouse and gamepad buttons return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value (Pressed or Released). - ::: - -3. When using thumbstick values for movement, why do we multiply the Y value by -1? - - :::question-answer - The thumbstick Y-axis values (-1.0f down to 1.0f up) are inverted compared to MonoGame's screen coordinate system (Y increases downward). Multiplying by -1 aligns the thumbstick direction with screen movement. - ::: - -4. What is the difference between analog and digital trigger input on a gamepad? - - :::question-answer - Analog triggers provide values between 0.0f and 1.0f based on how far they are pressed, while digital triggers only report 0.0f (not pressed) or 1.0f (pressed). This affects how you handle trigger input in your game. - ::: - -5. What is the key difference between [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) and [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture)? - - :::question-answer - [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) returns information about current touch points on the screen, while [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) provides information about specific gesture patterns like taps, drags, and pinches that have been performed. - ::: - -6. Why do we use a while loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) when reading gestures? - - :::question-answer - Quick gestures can generate multiple gesture events that are queued. Using a while loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) ensures we process all queued gestures, as [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) only returns one gesture at a time. - ::: - -7. How does touch input differ from mouse input in terms of handling multiple input points? - - :::question-answer - Touch input can handle multiple simultaneous touch points through the [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection), while mouse input only tracks a single cursor position. This allows touch input to support features like multi-touch gestures that are not possible with a mouse. - ::: - -8. What are the different states a [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) can have and what do they indicate? - - :::question-answer - A [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) can have four states: - - - [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Initial contact with the screen - - [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Touch point moved while maintaining contact - - [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Contact with the screen ended - - [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Touch data is not valid or tracking was lost - - ::: +--- +title: "Chapter 10: Handling Input" +description: "Learn how to handle keyboard, mouse, and gamepad input in MonoGame." +--- + +When you play a game, you need ways to control what is happening; using a keyboard or gamepad to control a character or clicking the mouse to navigate a menu, MonoGame helps us handle all these different types of controls through dedicated input classes: + +- [**Keyboard**](xref:Microsoft.Xna.Framework.Input.Keyboard): Detects which keys are being pressed. +- [**Mouse**](xref:Microsoft.Xna.Framework.Input.Mouse): Tracks mouse movement, button clicks, and scroll wheel use. +- [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad): Manages controller input like button presses and thumbstick movement. +- [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel): Manages touch input on devices with a touch panel such as mobile phones and tablets. + +Each of these input types has a `GetState` method that, when called, checks what is happening with that device at that moment. Think of it like taking a snapshot; when you call `GetState`, MonoGame looks at that exact moment to see which buttons are pressed, where the mouse is, or how the controller is being used. + +In this chapter you will, we will learn how to use each of these dedicated input classes to handle player input. + +## Keyboard Input + +The keyboard is often the primary input device for PC games, used for everything from character movement to menu navigation. MonoGame provides the [**Keyboard**](xref:Microsoft.Xna.Framework.Input.Keyboard) class to handle keyboard input, making it easy to detect which keys are being pressed at any time. Calling [**Keyboard.GetState**](xref:Microsoft.Xna.Framework.Input.Keyboard.GetState) will retrieve the current state of the keyboard as a [**KeyboardState**](xref:Microsoft.Xna.Framework.Input.KeyboardState) struct. + +### KeyboardState Struct + +The [**KeyboardState**](xref:Microsoft.Xna.Framework.Input.KeyboardState) struct contains methods that can be used to determine if a keyboard key is currently down or up: + +| Method | Description | +|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| +| [**IsKeyDown(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)) | Returns `true` if the specified key is down; otherwise, returns `false`. | +| [**IsKeyUp(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys)) | Returns `true` if the specified key is up; otherwise, returns `false`. | + +For example, if we wanted to see if the Space key is down, you could use the following: + +[!code-csharp[](./snippets/keyboardstate.cs)] + +> [!TIP] +> Notice we store the keyboard state in a variable instead of calling [**Keyboard.GetState**](xref:Microsoft.Xna.Framework.Input.Keyboard.GetState) multiple times. This is more efficient and ensures consistent input checking within a single frame. + +## Mouse Input + +The mouse is often the secondary input device for PC games, used for various actions from camera movement to interacting with menus and objects. MonoGame provides the [**Mouse**](xref:Microsoft.Xna.Framework.Input.Mouse) class to handle mouse input, making it easy to detect which buttons are pressed, the position of the mouse cursor, and the value of the scroll wheel. Calling [**Mouse.GetState**](xref:Microsoft.Xna.Framework.Input.Mouse.GetState) will retrieve the current state of the mouse as a [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct. + +### MouseState Struct + +The [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct contains properties that can be used to determine the state of the mouse buttons, the mouse position, and the scroll wheel value: + +| Property | Type | Description | +|----------------------------------------------------------------------------------------|-------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| [**LeftButton**](xref:Microsoft.Xna.Framework.Input.MouseState.LeftButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the left mouse button. | +| [**MiddleButton**](xref:Microsoft.Xna.Framework.Input.MouseState.MiddleButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the middle mouse button. This is often the button when pressing the scroll wheel down as a button | +| [**Position**](xref:Microsoft.Xna.Framework.Input.MouseState.Position) | [**Point**](xref:Microsoft.Xna.Framework.Point) | Returns the position of the mouse cursor relative to the bounds of the game window. | +| [**RightButton**](xref:Microsoft.Xna.Framework.Input.MouseState.RightButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the right mouse button. | +| [**ScrollWheelValue**](xref:Microsoft.Xna.Framework.Input.MouseState.ScrollWheelValue) | `int` | Returns the **cumulative** scroll wheel value since the start of the game | +| [**XButton1**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton1) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the first extended button on the mouse. | +| [**XButton2**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton2) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the second extended button on the mouse. | + +> [!NOTE] +> [**ScrollWheelValue**](xref:Microsoft.Xna.Framework.Input.MouseState.ScrollWheelValue) returns the cumulative value of the scroll wheel since the start of the game, not how much it moved since the last update. To determine how much it moved between one update and the next, you would need to compare it with the previous frame's value. We will discuss comparing previous and current frame values for inputs in the next chapter. + +Unlike keyboard input which uses [**IsKeyDown(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys))/[**IsKeyUp(Keys)**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys)) methods mouse buttons return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState): + +- [**ButtonState.Pressed**](xref:Microsoft.Xna.Framework.Input.ButtonState): The button is being held down. +- [**ButtonState.Released**](xref:Microsoft.Xna.Framework.Input.ButtonState): The button is not being pressed. + +For example, if we wanted to see if the left mouse button is down, you could use the following + +[!code-csharp[](./snippets/mousestate.cs)] + +## Gamepad Input + +Gamepads are often used as a primary input for a game or an alternative for keyboard and mouse controls. MonoGame provides the [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad) class to handle gamepad input, making it easy to detect which buttons are pressed and the value of the thumbsticks. Calling [**GamePad.GetState**](xref:Microsoft.Xna.Framework.Input.GamePad.GetState(Microsoft.Xna.Framework.PlayerIndex)) will retrieve the state of the gamepad as a [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct. Since multiple gamepads can be connected, you will need to supply a [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) value to specify which gamepad state to retrieve. + +### GamePadState Struct + +The [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct and properties that can be used to get the state of the buttons, dpad, triggers, and thumbsticks: + +| Property | Type | Description | +|--------------------------------------------------------------------------------|---------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**Buttons**](xref:Microsoft.Xna.Framework.Input.GamePadState.Buttons) | [**GamePadButtons**](xref:Microsoft.Xna.Framework.Input.GamePadButtons) | Returns a struct that identifies which buttons on the controller are pressed. | +| [**DPad**](xref:Microsoft.Xna.Framework.Input.GamePadState.DPad) | [**GamePadDPad**](xref:Microsoft.Xna.Framework.Input.GamePadDPad) | Returns a struct that identifies which directions on the DPad are pressed. | +| [**IsConnected**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsConnected) | `bool` | Returns a value that indicates whether the controller is connected. | +| [**ThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadState.ThumbSticks) | [**GamePadThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks) | Returns a struct that contains the direction of each thumbstick. Each thumbstick (left and right) are represented as a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value between `-1.0f` and `1.0` for the x- and y-axes. | +| [**Triggers**](xref:Microsoft.Xna.Framework.Input.GamePadState.Triggers) | [**GamePadTriggers**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers) | Returns a struct that contains the value of each trigger. Each trigger (left and right) are represented as a `float` value between `0.0f`, meaning not pressed, and `1.0f`, meaning fully pressed. | + +#### Buttons + +The [**GamePadState.Buttons**](xref:Microsoft.Xna.Framework.Input.GamePadState.Buttons) property returns a [**GamePadButtons**](xref:Microsoft.Xna.Framework.Input.GamePadButtons) struct that can be used to identify which buttons on the controller are pressed. This struct contains the following properties: + +| Property | Type | Description | +|--------------------------------------------------------------------------------------|-------------------------------------------------------------------|-----------------------------------------------| +| [**A**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.A) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the A button | +| [**B**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.B) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the B button | +| [**Back**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Back) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Back button | +| [**BigButton**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.BigButton) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the BigButton button | +| [**LeftShoulder**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.LeftShoulder) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the LeftShoulder button | +| [**LeftStick**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.LeftStick) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the LeftStick button | +| [**RightShoulder**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.RightShoulder) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the RightShoulder button | +| [**RightStick**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.RightStick) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the RightStick button | +| [**Start**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Start) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Start button | +| [**X**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.X) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the X button | +| [**Y**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.Y) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the Y button | + +> [!NOTE] +> Recall from [Chapter 01](../01_what_is_monogame/index.md) that MonoGame is a implementation the XNA API. Since XNA was originally created for making games on Windows PC and Xbox 360, the names of the gamepad buttons match those of an Xbox 360 controller. +> +> The [**BigButton**](xref:Microsoft.Xna.Framework.Input.GamePadButtons.BigButton) refers to the large, centrally located button on special Xbox 360 controllers created for games like "Scene It?" - this button is not present on standard controllers and is not mapped to any button on modern controllers. It remains in the API for backward compatibility with XNA. +> +> | Front | Back | +> | :--------------------------------------------------------- | :------------------------------------------------------- | +> | Xbox | | +> | ![Front Of Controller](./images/xbox-controller-front.svg) | ![Back Of Controller](./images/xbox-controller-back.svg) | +> | Playstation | | +> | ![Front Of Controller](./images/ps-controller-front.svg) | ![Back Of Controller](./images/ps-controller-back.svg) | + +Like with the [mouse input](#mousestate-struct), each of these buttons are represented by a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value. For instance, if you wanted to check if the A button is being pressed you could do the following: + +[!code-csharp[](./snippets/gamepadstate.cs)] + +#### DPad + +The [**DPad**](xref:Microsoft.Xna.Framework.Input.GamePadState.DPad) property returns a [**GamePadDPad**](xref:Microsoft.Xna.Framework.Input.GamePadDPad) struct that can be used to identify which DPad buttons on the controller are pressed. This struct contains the following properties: + +| Property | Type | Description | +|------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------| +| [**Down**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Down button. | +| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Left button. | +| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Right button. | +| [**Up**](xref:Microsoft.Xna.Framework.Input.GamePadDPad.Down) | [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) | Returns the state of the DPad Up Button. | + +Like with the [Buttons](#buttons), these also return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value to represent the state of the DPad button. For instance, if you wanted to check if the DPad up button is being pressed, you could do the following: + +[!code-csharp[](./snippets/buttonstate.cs)] + +#### Thumbsticks + +The [**ThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadState.ThumbSticks) property returns a [**GamePadThumbSticks**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks) struct that can be used to retrieve the values of the left and right thumbsticks. This struct contains the following properties: + +| Property | Type | Description | +|--------------------------------------------------------------------------|-----------------------------------------------------|------------------------------------------------| +| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks.Left) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The direction the left thumbstick is pressed. | +| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadThumbSticks.Right) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The direction the right thumbstick is pressed. | + +The thumbstick values are represented as a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value: + +- X-axis: A value between `-1.0f` (pushed fully to the left) and `1.0f` (pushed fully to the right). +- Y-axis: A value between `-1.0f` (pushed fully downward) and `1.0f` (pushed fully upward). + +For example, if you wanted to move a sprite using the left thumbstick, you could do the following + +[!code-csharp[](./snippets/thumbstick.cs)] + +> [!IMPORTANT] +> Notice that we inverted the y-axis value of the thumbstick by multiplying it by `-1.0f`. This is necessary because the thumbstick y-axis values range from `-1.0f` (down) to `1.0f` (up). The y-axis of the screen coordinates in MonoGame **increases** downward, as we saw in [Chapter 06](../06_working_with_textures/index.md#drawing-a-texture). +> +> This inversion aligns the thumbstick's y-axis value with the screen movement. + +#### Triggers + +The [**Triggers**](xref:Microsoft.Xna.Framework.Input.GamePadState.Triggers) property returns a [**GamePadTriggers**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers) struct that can be used to retrieve the values of the left and right triggers. This struct contains the following properties: + +| Property | Type | Description | +|-----------------------------------------------------------------------|---------|--------------------------------| +| [**Left**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers.Left) | `float` | The value of the left trigger. | +| [**Right**](xref:Microsoft.Xna.Framework.Input.GamePadTriggers.Right) | `float` | The value of the left trigger. | + +The trigger values are represented as a float value between `0.0f` (not pressed) to `1.0f` (fully pressed). The triggers on a gamepad, however, can be either *analog* or *digital* depending the gamepad manufacturer. For gamepads with *digital* triggers, the value will always be either `0.0f` or `1.0f`, as a digital trigger does not register values in between based on the amount of pressure applied to the trigger. + +For example, if we were creating a racing game, the right trigger could be used for acceleration like the following: + +[!code-csharp[](./snippets/triggers.cs)] + +### GamePadState Methods + +The [**GamePadState**](xref:Microsoft.Xna.Framework.Input.GamePadState) struct also contains two methods that can be used to get information about the device's inputs as either being up or down: + +| Method | Description | +|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [**IsButtonDown(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonDown(Microsoft.Xna.Framework.Input.Buttons)) | Returns a value that indicates whether the specified button is down. Multiple [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) values can be given using the bitwise OR `|` operator. When multiple buttons are given, the return value indicates if all buttons specified are down, not just one of them. | +| [**IsButtonUp(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonUp(Microsoft.Xna.Framework.Input.Buttons)) | Returns a value that indicates whether the specified button is up. Multiple [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) values can be given using the bitwise OR `|` operator. When multiple buttons are given, the return value indicates if all buttons specified are up, not just one of them. | + +You can use the [**IsButtonDown(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonDown(Microsoft.Xna.Framework.Input.Buttons)) and [**IsButtonUp(Buttons)**](xref:Microsoft.Xna.Framework.Input.GamePadState.IsButtonUp(Microsoft.Xna.Framework.Input.Buttons)) methods to get the state of all buttons, including the DPad. The following is a complete list of all of the [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) enum values: + +- [**Buttons.A**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.B**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.Back**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.BigButton**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadDown**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadRight**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.DPadUp**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftShoulder**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftStick**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickDown**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickRight**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftThumbstickUp**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.LeftTrigger**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.None**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightShoulder**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStick**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickDown**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickLeft**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickRight**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightStickUp**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.RightTrigger**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.Start**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.X**](xref:Microsoft.Xna.Framework.Input.Buttons) +- [**Buttons.Y**](xref:Microsoft.Xna.Framework.Input.Buttons) + +> [!CAUTION] +> While you can use these methods to get the state of any of these button inputs, the state will only tell you if it is being pressed or released. For the actual thumbstick values and trigger values, you would need to use the properties instead. + +For example, if we wanted to check if the A button on the the first gamepad is pressed, you could use the following: + +[!code-csharp[](./snippets/isbuttondown.cs)] + +### GamePad Vibration + +Another capability of gamepads is haptic feedback through vibration motors. MonoGame allows you to control this feature using the [**GamePad.SetVibration**](xref:Microsoft.Xna.Framework.Input.GamePad.SetVibration(Microsoft.Xna.Framework.PlayerIndex,System.Single,System.Single)) method. This method takes three parameters: + +1. The [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) of the gamepad to vibrate. +2. The intensity of the left motor (from `0.0f` for no vibration to `1.0f` for maximum vibration). +3. The intensity of the right motor (using the same scale). + +Most modern gamepads have two vibration motors, a larger one (usually the left motor) for low-frequency rumble and a smaller one (usually the right motor) for high-frequency feedback. By controlling these independently, you can create various haptic effects: + +[!code-csharp[](./snippets/vibration.cs)] + +## TouchPanel Input + +For mobile devices such as Android/iOS phones and tablets, the primary input device is the touch panel screen. Touching a location on the screen is similar to clicking a location on your computer with a mouse. MonoGame provides the [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) class to handle touch input. + +The [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) class offers two ways of retrieving information about touch input: + +- [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) retrieves a [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) struct that contains [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) values for each point of touch on the touch panel. +- [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) retrieves a [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct that contains information about recent gestures that have been performed like a vertical or horizontal drag across the screen. + +### TouchCollection + +When calling [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) a [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) struct is returned. This collection contains a [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) value for each point of touch. + +#### TouchLocation + +Each [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) value in a touch collection contains the following properties: + +| Property | Type | Description | +|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [**Id**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Id) | `int` | The id of the touch location. | +| [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Position) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The position of the touch location. | +| [**Pressure**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Pressure) | `float` | The amount of pressure applied at the touch location. **(Only available for Android devices.)** | +| [**State**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | [**TouchLocationState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState) | The current state of the touch location. | + +The important properties of the location are the [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.Position) and the [**State**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) The position property will tell us the location of the touch event, and the state can be one of the following values: + +| State | Description | +|------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location position is invalid. | +| [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location position was updated or pressed at the same position. | +| [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location was pressed. | +| [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) | This touch location was released. | + +When the state is moved or pressed, then we know that location on the touch panel is being touched. So we can capture it and use it like the following: + +[!code-csharp[](./snippets/touchstate.cs)] + +> [!NOTE] +> Unlike mouse input which only tracks a single point, [**TouchPanel**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel) supports multiple simultaneous touch points. The [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection) contains all active touch points, which is why we loop through them in the sample above. + +The state of a touch location progresses through the states typically in order of: + +- [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State): Initial contact with the screen. +- [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) : Touch point moved while maintaining contact. +- [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State): Contact with screen ended. +- [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation.State) : Touch data is invalid (using when tracking data is lost). + +### GestureSample + +When calling [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) a [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct containing the information about recent gestures that have been performed is returned. The [**GestureSample**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample) struct contains the following properties: + +| Property | Type | Description | +|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------------------| +| [**Delta**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Delta) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the delta information about the first touch-point in the gesture sample. | +| [**Delta2**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Delta2) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the delta information about the second touch-point in the gesture sample. | +| [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.GestureType) | [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | Gets the type of the gesture. | +| [**Position**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Position) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the position of the first touch-point in the gesture sample. | +| [**Position2**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.Position2) | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | Gets the position of the second touch-point in the gesture sample. | + +> [!NOTE] +> Gestures have two delta properties and two position properties. This is because some gestures require multiple touch inputs to perform, such as performing a pinch to zoom in or out. You would need the location of both touch points to determine the correct zoom to apply during the gesture. + +To determine what type of gesture is performed, we can get that from the [**GestureType**](xref:Microsoft.Xna.Framework.Input.Touch.GestureSample.GestureType) property which will be one of the following values: + +| Gesture Type | Description | +|----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| +| [**DoubleTap**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user double tapped the device twice which is always preceded by a Tap gesture. | +| [**DragComplete**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | States completion of a drag gesture (VerticalDrag, HorizontalDrag, or FreeDrag). | +| [**Flick**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | States that a touch was combined with a quick swipe. | +| [**FreeDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a point and the performed a free-form drag. | +| [**Hold**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a single point for approximately one second. | +| [**HorizontalDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched the screen and performed either a left-to-right or right-to-left drag gesture. | +| [**None**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | No gesture. | +| [**Pinch**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user converged or diverged two touch-points on the screen which is like a two-finger drag. | +| [**PinchComplete**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | An in-progress pinch gesture was completed. | +| [**Tap**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched a single point. | +| [**VerticalDrag**](xref:Microsoft.Xna.Framework.Input.Touch.GestureType) | The user touched the screen and performed either a top-to-bottom or bottom-to-top drag gesture. | + +> [!IMPORTANT] +> Before gestures can be detected, they have to be enabled using [**TouchPanel.EnabledGestures**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.EnabledGestures). This can be done in [**Game.Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) like the following: +> +> [!code-csharp[](./snippets/enablegestures.cs)] + +The following is an example of using a gesture to detect horizontal and vertical drags: + +[!code-csharp[](./snippets/gestures.cs)] + +> [!IMPORTANT] +> Notice above that we use a `while` loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) as the condition for the loop. The reason we do this is because when a user performs a gesture, such as a horizontal drag across the screen, very quickly, what can often occurs is a series of multiple small drag gestures are registered and queued. +> +> Each time [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) is called, it will dequeue the next gesture. So to ensure that we handle the complete gesture, we loop the gesture queue until there are none left. + +## Implementing Input in Our Game + +For our game, we are going to implement keyboard and gamepad controls based on the following criteria: + +| Keyboard Input | Gamepad Input | Description | +|---------------------------|---------------------------------------------|--------------------------------------| +| [Keys.W] and [Keys.Up] | [Thumbstick.Left.Y] and [Buttons.DPadUp] | Moves the slime up the screen. | +| [Keys.S] and [Keys.Down] | [Thumbstick.Left.Y] and [Buttons.DPadDown] | Moves the slime down the screen | +| [Keys.A] and [Keys.Left] | [Thumbstick.Left.X] and [Buttons.DPadLeft] | Moves the slime left on the screen. | +| [Keys.D] and [Keys.Right] | [Thumbstick.Left.X] and [Buttons.DPadRight] | Moves the slime right on the screen. | +| [Keys.Space] | [Buttons.A] | Increased the speed of the slime. | + +Open *Game1.cs* and update it with the following: + +[!code-csharp[](./snippets/game1.cs?highlight=17-21,62-66,71-159,170)] + +The key changes made here are: + +1. The `_slimePosition` field was added to track the position of the slime as it moves. +2. The `MOVEMENT_SPEED` constant was added to use as the base multiplier for the movement speed. +3. The `CheckKeyboardInput` method was added which checks for input from the keyboard based on the input table above and moves the slime based on the keyboard input detected. +4. The `CheckGamePadInput` method was added which checks for input from the gamepad based on the input table above and moves the slime based the gamepad input detected. + + > [!NOTE] + > The gamepad implementation includes a priority system for directional input. The code prioritizes the analog thumbstick values over the digital DPad buttons. This design choice provides players with more nuanced control, as analog inputs allow for a variable movements speed based on how far the thumbstick is pushed, while DPad buttons only provide on/off input states. The code first checks if either thumbstick axis has a non-zero value, and only falls back to DPad input when the thumbstick is centered. + > + > To enhance player experience, the gamepad implementation also includes gamepad vibration when the speed boost is activated. Haptic feedback like this creates a more immersive experience by engaging additional senses for the player beyond just visual and auditory feedback. + +5. In [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) `CheckKeyboardInput` and `CheckGamePadInput` methods are called. +6. In [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), the slime is now drawn using `_slimePosition` as the position. + +Running the game now, you can move the slime around using the keyboard with the arrow keys or WASD keys. If you have a gamepad plugged in you can also use the DPad and left thumbstick. + +| ![Figure 10-1: The slime moving around based on device input](./videos/input-moving-slime.webm) | +|:-----------------------------------------------------------------------------------------------:| +| **Figure 10-1: The slime moving around based on device input** | + +## Conclusion + +In this chapter, you accomplished the following: + +- Handle keyboard input to detect key presses. +- Handle mouse input including button clicks and cursor position. +- Work with gamepad controls including buttons, thumbsticks, and vibration. +- Understand touch input for mobile devices including touch points and gestures. +- Implement movement controls using different input methods. +- Consider controller-specific details like coordinate systems and analog vs digital input. + +In the next chapter, we will learn how to track previous input states to handle single-press events and implement an input management system to simplify some of the complexity of handling input. + +## Test Your Knowledge + +1. Why do we store the result of `GetState` in a variable instead of calling it multiple times? + + :::question-answer + Storing the state in a variable is more efficient and ensures consistent input checking within a frame. Each `GetState` call polls the device, which can impact performance if called repeatedly. + ::: + +2. What is the main difference between how keyboard and mouse/gamepad button states are checked? + + :::question-answer + Keyboard input uses [**IsKeyUp**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyUp(Microsoft.Xna.Framework.Input.Keys))/[**IsKeyDown**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)) methods, while mouse and gamepad buttons return a [**ButtonState**](xref:Microsoft.Xna.Framework.Input.ButtonState) enum value (Pressed or Released). + ::: + +3. When using thumbstick values for movement, why do we multiply the Y value by -1? + + :::question-answer + The thumbstick Y-axis values (-1.0f down to 1.0f up) are inverted compared to MonoGame's screen coordinate system (Y increases downward). Multiplying by -1 aligns the thumbstick direction with screen movement. + ::: + +4. What is the difference between analog and digital trigger input on a gamepad? + + :::question-answer + Analog triggers provide values between 0.0f and 1.0f based on how far they are pressed, while digital triggers only report 0.0f (not pressed) or 1.0f (pressed). This affects how you handle trigger input in your game. + ::: + +5. What is the key difference between [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) and [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture)? + + :::question-answer + [**TouchPanel.GetState**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.GetState) returns information about current touch points on the screen, while [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) provides information about specific gesture patterns like taps, drags, and pinches that have been performed. + ::: + +6. Why do we use a while loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) when reading gestures? + + :::question-answer + Quick gestures can generate multiple gesture events that are queued. Using a while loop with [**TouchPanel.IsGestureAvailable**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.IsGestureAvailable) ensures we process all queued gestures, as [**TouchPanel.ReadGesture**](xref:Microsoft.Xna.Framework.Input.Touch.TouchPanel.ReadGesture) only returns one gesture at a time. + ::: + +7. How does touch input differ from mouse input in terms of handling multiple input points? + + :::question-answer + Touch input can handle multiple simultaneous touch points through the [**TouchCollection**](xref:Microsoft.Xna.Framework.Input.Touch.TouchCollection), while mouse input only tracks a single cursor position. This allows touch input to support features like multi-touch gestures that are not possible with a mouse. + ::: + +8. What are the different states a [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) can have and what do they indicate? + + :::question-answer + A [**TouchLocation**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocation) can have four states: + + - [**Pressed**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Initial contact with the screen + - [**Moved**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Touch point moved while maintaining contact + - [**Released**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Contact with the screen ended + - [**Invalid**](xref:Microsoft.Xna.Framework.Input.Touch.TouchLocationState): Touch data is not valid or tracking was lost + + ::: diff --git a/articles/tutorials/building_2d_games/10_handling_input/snippets/inputbuffer.cs b/articles/tutorials/building_2d_games/10_handling_input/snippets/inputbuffer.cs new file mode 100644 index 00000000..dec0fa1f --- /dev/null +++ b/articles/tutorials/building_2d_games/10_handling_input/snippets/inputbuffer.cs @@ -0,0 +1,40 @@ +// Use a queue directly for input buffering +private Queue _inputBuffer; +private const int MAX_BUFFER_SIZE = 2; + +// In initialization code: +_inputBuffer = new Queue(MAX_BUFFER_SIZE); + +// In the input handling code: +KeyboardState keyboard = Keyboard.GetState(); +Vector2 newDirection = Vector2.Zero; + +if(keyboard.IsKeyDown(Keys.Up)) +{ + newDirection = -Vector2.UnitY; +} +else if(keyboard.IsKeyDown(Keys.Down)) +{ + newDirection = Vector2.UnitY; +} +else if(keyboard.IsKeyDown(Keys.Left)) +{ + newDirection = -Vector2.UnitX; +} +else if(keyboard.IsKeyDown(Keys.Right)) +{ + newDirection = Vector2.UnitX; +} + +// Only add if a valid direction and does not exceed the buffer size +if(newDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE) +{ + _inputBuffer.Enqueue(newDirection); +} + +// In movement update code +if(_inputBuffer.COunt > 0) +{ + Vector2 nextDirection = _inputBuffer.Dequeue(); + _position += nextDirection * _speed; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_copy_head.png b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_copy_head.png new file mode 100644 index 00000000..7d4dd017 Binary files /dev/null and b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_copy_head.png differ diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_copy_head.svg b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_copy_head.svg new file mode 100644 index 00000000..73454a6d --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_copy_head.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_directions.png b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_directions.png new file mode 100644 index 00000000..a91550d6 Binary files /dev/null and b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_directions.png differ diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_directions.svg b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_directions.svg new file mode 100644 index 00000000..321ce18c --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_directions.svg @@ -0,0 +1,180 @@ + + diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_insert_head.png b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_insert_head.png new file mode 100644 index 00000000..c6528141 Binary files /dev/null and b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_insert_head.png differ diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_insert_head.svg b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_insert_head.svg new file mode 100644 index 00000000..2b99c430 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_insert_head.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_remove_tail.png b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_remove_tail.png new file mode 100644 index 00000000..0b996de3 Binary files /dev/null and b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_remove_tail.png differ diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_remove_tail.svg b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_remove_tail.svg new file mode 100644 index 00000000..a8c48909 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/images/snake_remove_tail.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/index.md b/articles/tutorials/building_2d_games/22_snake_game_mechanics/index.md new file mode 100644 index 00000000..15a7e5e7 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/index.md @@ -0,0 +1,452 @@ +--- +title: "Chapter 22: Snake Game Mechanics" +description: "Learn how to implement classic snake-like game mechanics and organize game objects into reusable components." +--- + +In the previous chapters, we've built all the fundamental systems needed for our game: [graphics](../07_optimizing_texture_rendering/index.md), [input](../11_input_management/index.md), [collision detection](../12_collision_detection/index.md), [audio](../15_audio_controller/index.md), [scene management](../17_scenes/index.md), and a [user interface](../19_user_interface/index.html). Now it's time to transform our demo into a complete experience by implementing classic snake-like game mechanics. Before we do that, we first need to define what mechanics make a snake game. + +In this chapter, you will: + +- Understand the core mechanics that define a classic snake-like game. +- Learn how to implement grid-based movement with timed intervals. +- Create a segmented character that grows when collecting objects. +- Implement a unified input controller for game actions. +- Build the `SlimeSegment` struct for storing segment data. +- Create the `Slime` class to manage all snake-like behavior for the slime. +- Create the `Bat` class as the collectable object. + +> [!NOTE] +> This chapter will not focus much on MonoGame itself, but rather our implementation of the mechanics to transform our current game into a snake-like game. + +## Understanding Snake Game Mechanics + +In a classic snake-like game, the mechanics follow a set of simple but engaging rules: + +1. The player controls a snake by telling it to move in one of four cardinal directions (up, down, left, and right). +2. The snake cannot reverse into itself, only moving forward or perpendicular to its current direction. +3. The actual movement of the snake occurs at regular timed intervals, creating a grid-based movement pattern. +4. When the snake eats food, it grows longer by adding a new segment to its tail. +5. If the snake collides with a wall or its own body, the game ends. + +The mechanics create an increasingly challenging experience as the snake grows longer, requiring planning and reflexes to avoid collision. + +### Directions + +In snake, players input a cardinal direction (up, down, left, and right), to indicate which direction the snake will move during the next movement cycle.  When direction input occurs, it must be checked against the current direction to determine if the move is valid. + +For example, if the snake is moving to the right, an invalid input would allow a player to move it to the left.  Doing so would cause the head of the snake to reverse direction and immediately collide with the first body segment. This means the only valid inputs are those where the next direction would be the same as the current direction or perpendicular to the current direction. + +| ![Figure 22-1: An example snake with four segments, the head segment highlighted in orange, moving to the right. Arrows show that the only valid movements for the head segment are up or down (perpendicular) or to continue to the right.](./images/snake_directions.png) | +| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| **Figure 22-1: An example snake with four segments, the head segment highlighted in orange, moving to the right. Arrows show that the only valid movements for the head segment are up or down (perpendicular) or to continue to the right.** | + +### Movement Cycle + +Instead of moving every update frame as a directional input is being pressed, the snake instead only moves during regular timed intervals.  A timer is used to determine how much time has passed since the last movement cycle, and when it reaches a set threshold, the next movement cycle occurs.  During this movement cycle, the snake should move forward in the direction that was input by the player between the last and current movement cycles.  This creates the grid-based movement system typically found in snake-like games. + +There are various methods for handling the movement, such as iterating through each segment of the snake and updating the position of that segment to move forward.  Methods such as this though are wasteful, since visually the only parts of the snake that move on the screen are the head and the tail.   + +Instead, a more common approach is to: + +1. Make a copy of the head segment. + + | ![Figure 22-2: From a snake with four segments, a copy of the head segment is made, represented by the orange block](./images/snake_copy_head.png) | + | :------------------------------------------------------------------------------------------------------------------------------------------------: | + | **Figure 22-2 From a snake with four segments, a copy of the head segment is made, represented by the orange block** | + +2. Update the properties of the copy so that it is positioned where the original head segment would have moved to. +3. Insert the copy at the front of the segment collection. + + | ![Figure 22-3: The copy of the head segment, represented by the orange block, is inserted at the front of the segment collection as the new head, which now makes it five segments (one too many)](./images/snake_insert_head.png) | + | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | + | **Figure 22-3: The copy of the head segment, represented by the orange block, is inserted at the front of the segment collection as the new head, which now makes it five segments (one too many)** | + +4. Remove the tail segment. + + | ![Figure 22-4: The tail segment of the snake is removed, bringing it back to the original four segments, giving the illusion that the entire snake moved forward ](./images/snake_remove_tail.png) | + | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | + | **Figure 22-4: The tail segment of the snake is removed, bringing it back to the original four segments, giving the illusion that the entire snake moved forward** | + +By doing this, no matter how many segments the snake body has, we only ever need to update two of them, the head and the tail. + +### Growth + +The next core mechanic for a snake-like game is the growth of the snake.  Typically, somewhere in the game is an objective for the snake to eat, such as an apple.  When the head of the snake reaches this objective, a new segment is added to the snake's body, making it longer, the player's score is increased, and a new objective is spawned at a random location within the game. + +This mechanic also acts as an artificial difficulty curve for the game.  As the body of the snake grows longer, it requires planning to avoid creating situations where the snake becomes trapped by its own body making it impossible to avoid a collision. + +### Game Over + +The challenge in a snake-like game is to avoid colliding with either a wall or another segment of the snake body.  Doing so will cause a game over condition as the snake can no longer continue moving forward. + +## Implementing Snake-Like Mechanics In Our Game + +Now that we have a foundational set of rules in place for the mechanics of a snake-like game, let's implement them into the current demo we've been building up. Our game will adapt these mechanics to fit them into our existing game structure: + +- Instead of a snake, we will use the slime and create a chain of slime segments that follow the slime at the front. +- The player will control the direction of the slime segment +- The bat will serve as the objective to acquire to grow the slime segment. +- Collisions with either the walls of the room or a slime segment will result in a game over state. + +As we implement these mechanics, we're also going to be creating classes that encapsulate the properties and functions of existing implementations in the game scene, such as the slime and the bat.  For example, currently, the game scene tracks fields for the `AnimatedSprite` and the position of the slime, as well as updating, input handling, and drawing the slime.  These can be moved into their dedicated classes encapsulating the functionality and also cleaning up the code in the game scene that has grown quite large. + +### The GameController Class + +Currently, we have two methods dedicated to handling input in the game scene, `CheckKeyboardInput` and `CheckGamePadInput`.  Both of these methods essentially perform the same logic across different input devices. This presents an opportunity to improve our code. + +To simplify input handling for the game, we can create a dedicated class that consolidates the input methods, providing a unified input profile for the game.  This pattern is widely used in game development to separate the "what" (game actions) from the "how" (specific input devices and buttons). + +In the *DungeonSlime* project (your main game project), create a new file named *GameController.cs* in the root directory and add the following code: + +[!code-csharp[](./snippets/gamecontroller.cs)] + +The `GameController` class implements an important design pattern in game development known as the "Input Abstraction" or "Command" pattern.  This pattern separates what happens in the game (the actions) from how players trigger those actions (the inputs). + +This separation provides several benefits, including: + +1. **Input Device Independence**: The game logic doesn't need to know which input device the player is using. Whether they're playing with a keyboard, gamepad, or touch screen, the game only cares that a "move up" action was triggered, not which specific button or key caused it. +2. **Simplified Input Handling**: Instead of checking multiple input combinations throughout the codebase, game objects can simply ask "Should I move up?" through a clean API call. +3. **Easy Rebinding**: If you want to add key rebinding features, you only need to modify the `GameController` class, not every piece of code that uses input. +4. **Consistent Input Logic**: The rules for determining if an action occurred (like checking if a button was just pressed version being held down) are defined in one place. +5. **Cross-Platform Compatibility**: When porting to different platforms with different input methods, you only need to update the `GameController` class to map the new input devices to your existing game actions. + +By implementing this pattern in our game, we are not only making our current input handling cleaner, but we are also establishing a foundation that would make it easier to add features like input customization or support for new input devices in the future. + +With our input handling system in place, now we can turn our attention to implementing the core mechanics of our snake-like game. First, we need to create a structure that will represent each segment of the slime's body. + +### The SlimeSegment Struct + +Next, we will need to implement a structure that can represent each segment of the slime.  This structure will store the position and movement data for each segment. + +In the DungeonSlime project (your main game project), create a new directory named *GameObjects*. We will be putting all of our code related to the objects within the game here. Next, create a new file named *SlimeSegment.cs* inside the *GameObjects* directory you just created and add the following code: + +[!code-csharp[](./snippets/slimesegment.cs)] + +This structure contains fields to track: + +- `At`: The current position of the segment. +- `To`: The position the segment will move to during the next movement cycle if it is the head segment. +- `Direction`: A normalized vector representing the direction the segment is moving in. +- `ReverseDirection`: A computed property that returns the opposite of the `Direction` property. + +> [!NOTE] +> We are implementing this as a struct rather than a class because SlimeSegment is a small, simple data container with value semantics. Structs are more efficient for small data structures since they're allocated on the stack rather than the heap, reducing garbage collection overhead. Since our game will potentially create many segments as the snake grows, using a struct can provide better performance, especially when we will be copying segment data during movement operations. + +> [!IMPORTANT] +> Structs work best with value types (like int, float, [**Vector2**](xref:Microsoft.Xna.Framework.Vector2)); using reference types in structs can cause boxing operations that negate the performance benefits. For more information on structs, refer to the [Structure Types - C# Reference](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct) documentation on Microsoft Learn. + +By tracking both the current (`At`) and target (`To`) positions, we can implement smooth visual movement between grid positions, creating more fluid animations than the traditional stutter step movement seen in classic snake games.  We will explore this concept a little further in this chapter. + +### The Slime Class + +Next, let's implement a class to encapsulate the properties and functionality of our snake-like slime. In the *GameObjects* directory of the DungeonSlime project (your main game project), create a new file named *Slime.cs* and add the following initial code: + +[!code-csharp[](./snippets/slime/definition.cs)] + +This code sets up the basic structure for our `Slime` class. We've added the necessary using statements to access MonoGame's framework components and placed the class in the `DungeonSlime.GameObjects` namespace to keep our code organized. The empty class will serve as our foundation, and we'll build it up piece by piece in the following sections. + +Now, we'll add several components to this class in sequence. Each section below should be added to the `Slime` class in the order presented. As we go through each part, the class will gradually take shape to handle all the snake-like behavior we need. + +> [!NOTE] +> When adding these sections one by one, you may see compiler errors until all sections are in place. This is normal, as some parts of the code will reference fields or methods that haven't been added yet. Once all sections are complete, these errors will resolve. + +#### Slime Fields + +Add the following fields to the `Slime` class: + +[!code-csharp[](./snippets/slime/fields.cs)] + +Let's break down what each of these fields is responsible for: + +- `s_movementTime`: This constant represents how long the slime waits between movement cycles (300ms). This creates the classic snake game's grid-based movement feel, where the snake moves at regular intervals rather than continuously. +- `_movementTime`: This field accumulates elapsed time until it reaches the movement threshold. When it does, the slime moves one grid cell and the timer resets. +- `_movementProgress`: This normalized value (0-1) represents progress between movement ticks and is used for visual interpolation. It allows us to smoothly animate the slime's movement between grid positions. +- `_nextDirection`: This stores the direction that will be applied to the head segment during the next movement cycle. +- `_stride`: This represents the total number of pixels the head segment should move during movement cycles. +- `_segments`: This collection holds all the `SlimeSegment` structures that make up the slime's body. The first segment is the head, and the rest form the trailing body. +- `_sprite`: This stores the `AnimatedSprite` that is used to draw each segment of the slime. + +These fields implement core snake-like mechanics - the timed interval movement, direction control, and the segmented body that forms the snake. + +#### Slime Events + +Next, add the following events to the `Slime` class after the fields: + +[!code-csharp[](./snippets/slime/events.cs)] + +This event will allow the `Slime` class to notify the game scene when the head of the slime collides with another segment, triggering a game over. + +#### Slime Constructor + +After the events, add the following constructor to the `Slime` class: + +[!code-csharp[](./snippets/slime/constructor.cs)] + +This is a simple constructor that requires the slime to be given the `AnimatedSprite` that will be used to draw each of the slime segments. + +#### Slime Initialization + +Add the following `Initialization` method to the `Slime` class after the constructor: + +[!code-csharp[](./snippets/slime/initialize.cs)] + +With this method, we can initialize, or reset the state of slime.  It: + +- Instantiates a new segment collection. +- Creates the initial head segment and positions it at the specific `startingPosition`. +- Sets the initial direction to be to the right. +- Initialize the movement timer to zero. + +#### Slime Input Handling + +Next, let's add the `HandleInput` method to process player input. Add the following method after the `Initialize` method: + +[!code-csharp[](./snippets/slime/handleinput.cs)] + +This method will: + +1. Determine if the player is attempting to change directions instead of directly moving the slime.  This direction change will be applied later during the movement cycle update. +2. Uses [**Vector2.Dot**](xref:xref:Microsoft.Xna.Framework.Vector2.Dot(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2)) to prevent the slime from reversing into itself, causing an immediate collision and game over state. +3. Updates the `_nextDirection` value only if the direction input is valid. + +> [!NOTE] +> The dot product measures how much two vectors point in the same direction. It is: +> +> - Positive if they are pointing in the same direction. +> - Negative if they are pointing in opposite directions. +> - Zero when they are perpendicular. +> +> By using dot product here, this effectively implements the classic snake-like game rule that you cannot turn 180° into yourself. + +#### Slime Movement Cycle + +To handle the snake-like movement cycle of the slime, we will create a method called `Move`. Add the following method to the `Slime` class after the `HandleInput` method: + +[!code-csharp[](./snippets/slime/move.cs)] + +This method performs the core snake-like movement cycle logic by: + +1. Copying the value of the current head segment. +2. Updating the copy's position (`At`) to where the head was moving to and updating the position it is moving to (`To`). +3. Insert the copy into the front of the segment collection and remove the tail. +4. Check if the head is now in the same position as any body segments, which would cause a collision and trigger a game over. + +> [!NOTE] +> By inserting a new head segment at the front of the chain and removing the last segment, this creates the illusion of the entire chain moving forward as one, even though we're only actually moving the head forward and removing the tail. +> +> This follows the common snake movement pattern as discussed in the [Understanding Snake Game Mechanics: Movement Cycle](#movement-cycle) section above. + +#### Slime Growth + +To handle the snake-like growth of the slime, we'll create a method called `Grow`. Add the following method to the `Slime` class after the `Move` method: + +[!code-csharp[](./snippets/slime/grow.cs)] + +Let's break down how this method works: + +1. First it creates a copy of the current tail value. +2. It then adjusts the values of the copy so that it is now positioned behind the current tail by using the `ReverseDirection` value of the tail. +3. Finally, it inserts this new tail into the segments collection. + +#### Slime Update + +With most of the core snake-like mechanics now added to the `Slime` class within their own methods, let's add the `Update` method. Add the following method to the `Slime` class after the `Grow` method: + +[!code-csharp[](./snippets/slime/update.cs)] + +This update method: + +1. Updates the slime's `AnimatedSprite` to ensure the sprite animations occur. +2. Calls `HandleInput` to check for player input +3. Increments the movement timer by the amount of time that has elapsed between the game's update cycles. +4. Performs a check to see if the movement timer has accumulated more time than the threshold to perform a movement cycle update.  If it has then +   1. The movement timer is reduced by the threshold time. +   2. The `Move` method is called to perform a movement cycle update. +5. Finally, the movement progress amount is calculated by dividing the number of seconds accumulated for the movement timer by the number of seconds for the threshold.  This gives us a normalized value between 0.0 and 1.0 that we can use for visual interpolation for fluid movement. + +> [!TIP] +> In games, frame rates can vary based on system performance, causing inconsistent update intervals.  If we simply reset the movement timer to zero after each movement cycle, we would lose any excess time that accumulated beyond the movement threshold. +> +> For example: +> +> - Our movement threshold is 200ms. +> - The game runs at 60fps (16.67ms per frame). +> - After 12 frames, we have accumulated 200.04ms. +> - If we reset to zero, we lose 0.04ms. +> - Over time, these small losses can add up and cause inconsistent movement. +> +> By subtracting the threshold instead of resetting to zero, we "bank" the excess time (0.06ms in this example) for the next movement cycle.  This ensures that: +> +> 1. Movement happens exactly at the intended frequency, maintaining consistent game speed. +> 2. The visual smoothness of movement remains intact even if the game occasionally drops frames. +> 3. Players experience the same game timing regardless of their hardware's performance. +> +> This technique is standard practice in game development, especially for timing-sensitive mechanics like rhythmic games, animations, and movement systems.  It is a simple solution that significantly improves gameplay consistency. + +#### Slime Draw + +We also need a method to handle drawing the slime and all of its segments. Add the following `Draw` method after the `Update` method to the `Slime` class: + +[!code-csharp[](./snippets/slime/draw.cs)] + +This draw method iterates each segment of the slime and calculates the visual position to draw each segment at by performing linear interpolation (lerp) to determine the position of the segment between its current position (`At`) and the position it is moving to (`To`) based on the `_movementProgress` calculation. + +> [!NOTE] +> [**Vector2.Lerp**](xref:Microsoft.Xna.Framework.Vector2.Lerp(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,System.Single)) performs a linear interpolation between two vectors, creating a smooth transition from start to end based on an amount parameter.  The formula is: +> +> $\text{result} = \text{start} + (\text{end} - \text{start}) \cdot \text{amount}$ +> +> Where the amount parameter ranges from 0.0 (returns the start position) to 1.0 (returns the end position).  Values between 0.0 and 1.0 give positions along the straight line between start and end. +> +> In our game, this is used to create a visual fluid movement illusion by interpolating between a segment's current position (`At`) and its target position (`To`) based on the elapsed time, rather than having the segments abruptly jump from one grid position to the next after each movement update. + +#### Slime Bounds + +For the game scene to detect collisions between the slime and other elements (walls or the bat), we need a method to calculate the current collision bounds. Add the following method to the `Slime` class after the `Draw` method: + +[!code-csharp[](./snippets/slime/getbounds.cs)] + +This method takes the current head segment (the first segment in our collection), calculates its visual position using linear interpolation, and then creates a `Circle` value to represent its collision boundary. Using the interpolated position ensures that collision detection aligns with what the player sees on screen. + +> [!NOTE] +> We only need collision bounds for the slime's head for interactions with the bat and walls, as this matches the classic snake game mechanic where only the head's collisions matter for gameplay. For detecting collisions between the head and body segments, we use a simpler position-based check in the `Move` method since those positions are always aligned to the grid. + +With all these methods in place, our Slime class now fully implements the snake-like mechanics we need. It handles movement on a grid, prevents invalid direction changes, detects self-collisions, and provides smooth visual movement between grid positions. This encapsulation allows us to manage all slime-related behavior in one place while exposing only the necessary interfaces to the game scene. + +Now that we have our player-controlled character implemented, let's create the object that the slime will try to collect; the bat. + +### The Bat Class + +In the *GameObjects* directory of the DungeonSlime project (your main game project), create a new file named *Bat.cs* and add the following initial code: + +[!code-csharp[](./snippets/bat/definition.cs)] + +This code establishes the foundation for our `Bat` class. We have included the necessary using statements for MonoGame components, audio functionality, and our library references. The class is placed in the same `DungeonSlime.GameObjects` namespace as our Slime class to maintain a consistent organization. + +Now we will build this class step by step, adding all the functionality needed for the bat to serve as the collectible object in our game. Add each of the following sections to the `Bat` class in the order they are presented. + +> [!NOTE] +> As with the Slime class, you may encounter compiler errors until all sections are in place. These errors will be resolved once all components of the class have been added. + +#### Bat Fields + +Add the following fields to the `Bat` class: + +[!code-csharp[](./snippets/bat/fields.cs)] + +Let's break down what each of these fields is responsible for: + +- `MOVEMENT_SPEED`: This constant represents the factor to multiply the velocity vector by to determine how fast the bat is moving. +- `_velocity`: A vector that defines the direction and how much in that direction to update the position of the bat each update cycle. +- `_sprite`: This stores the `AnimatedSprite` that is used to draw the bat. +- `_bounceSoundEffect`: This store the [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) to play when the bat is told to bounce. + +#### Bat Properties + +Next, add the following property to the `Bat` class after the fields: + +[!code-csharp[](./snippets/bat/properties.cs)] + +This property exposes the position of the bat so it can be used for calculations in the game scene when determining where to place the bat after the slime eats it. + +#### Bat Constructor + +After the property, dd the following constructor to the `Bat` class: + +[!code-csharp[](./snippets/bat/constructor.cs)] + +This is a simple constructor that requires the bat to be given the `AnimatedSprite` that will be used to draw the bat and the [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) to be played when the bat bounces off a wall. + +#### Bat Randomize Velocity + +Currently, we have the `AssignRandomVelocity` method in the `GameScene` that we call to randomize the velocity of the bat after it has been eaten by the slime. Let's take this method out of the `GameScene` class and put it into the `Bat` class itself. Add the following method to the `Bat` class after the constructor: + +[!code-csharp[](./snippets/bat/randomizevelocity.cs)] + +#### Bat Bounce + +We are also going to take the logic from the `GameScene` class that bounces the bat off the walls and move it into a dedicated method in the `Bat` class. Add the following method to the `Bat` class after the `RandomizeVelocity` method: + +[!code-csharp[](./snippets/bat/bounce.cs)] + +This method only takes a single parameter, the [normal vector](../12_collision_detection/index.md#bounce-collision-response) of the surface the bat is bouncing against.  Based on the X and Y components of the normal vector, we can determine which wall the bat bounced against and adjust the position of the bat so that it doesn't stick to the wall. + +#### Bat Bounds + +Similar to the [`Slime` class](#slime-bounds), for the game scene to detect collision between the bat and other elements, we need a method to calculate the current collision bounds of the bat.  Add the following method to the `Bat` class after the `Bounce` method: + +[!code-csharp[](./snippets/bat/getbounds.cs)] + +#### Bat Update + +The `Bat` class will also need to be updated. Add the following `Update` method to the `Bat` class after the `GetBounds` method: + +[!code-csharp[](./snippets/bat/update.cs)] + +This method simply updates the bat's `AnimatedSprite` to ensure animations occur and adjusts the position of the bat based on the current velocity. + +> [!NOTE] +> The continuous movement of the bat contrasts with the grid-based interval movement of the slime, creating different gameplay dynamics for the player to consider.  This makes catching the bat challenging without requiring any complex behaviors. + +#### Bat Draw + +Finally, we need a method to draw the bat. Add the following `Draw` method to the `Bat` class after the `Update` method: + +[!code-csharp[](./snippets/bat/draw.cs)] + +This method simply draws the bat's `AnimatedSprite` at the bat's current position. + +With the `Bat` class complete, we've now encapsulated all the behavior needed for the collectible element in our game. The bat moves continuously around the screen and can bounce off walls, adding a twist on the classic snake-like mechanic by creating a target for the player to chase. + +## Conclusion + +In this chapter, we have learned about and implemented the core mechanics of a class snake-like game. We created: + +- A [`GameController`](#the-gamecontroller-class) class that provides a unified input interface, separating game actions from specific input devices. +- A [`SlimeSegment`](#the-slimesegment-struct) struct to efficiently store and manage individual segments of our snake-like character. +- A [`Slime`](#the-slime-class) class that implements grid-based movement, segment management and self-collision detection. +- A [`Bat`](#the-bat-class) class that serves as the collectible object with continuous movement and wall bouncing. + +These implementations encapsulate the core gameplay mechanics into reusable, maintainable objects. In the next chapter, we will build on these mechanics by updating the `GameScene` to implement game state management and a new UI element for the game over state to create a complete game experience. + +## Test Your Knowledge + +1. Why must a snake-like game prevent the player from reversing direction? + + :::question-answer + Preventing reverse movement is necessary because it would cause an immediate collision between the snake's head and the first body segment, resulting in an unfair game over. + ::: + +2. How does the movement cycle for a snake work, and why is it more efficient than updating each segment individually? + + :::question-answer + The snake movement cycle works by: + + 1. Creating a copy of the head segment. + 2. Positioning the copy one grid cell ahead in the current direction + 3. Inserting this copy at the front of the segment collection + 4. Removing the last segment. + + This approach is more efficient because it only requires manipulating two segments (adding a new head and removing the tail) regardless of how long the snake becomes, rather than iterating through and updating every segment individually. + ::: + +3. What are the benefits of using the Input Abstraction pattern implemented in the `GameController` class? + + :::question-answer + The Input Abstraction pattern provides several benefits: + + - Input device independence, allowing the game to handle keyboard, gamepad, or other inputs through a unified interface + - Simplified input handling through clean API calls rather than checking multiple input combinations + - Easier implementation of key rebinding features by only needing to modify the GameController class + - Consistent input logic defined in a single location + - Better cross-platform compatibility by centralizing platform-specific input handling + + ::: + +4. How does the implementation use [**Vector2.Lerp**](xref:Microsoft.Xna.Framework.Vector2.Lerp(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,System.Single)) to create smooth visual movement, and why is this important? + + :::question-answer + The implementation uses [**Vector2.Lerp**](xref:Microsoft.Xna.Framework.Vector2.Lerp(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,System.Single)) to interpolate between a segment's current position (`At`) and its target position (`To`) based on a normalized movement progress value. This creates smooth visual movement by drawing the segments at intermediate positions between grid points rather than abruptly jumping from one grid position to the next. + + This is important because it provides more fluid animation while maintaining the logical grid-based movement, enhancing the visual quality of the game without changing the core mechanics. + ::: diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/bounce.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/bounce.cs new file mode 100644 index 00000000..87275f52 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/bounce.cs @@ -0,0 +1,32 @@ +/// +/// Handles a bounce event when the bat collides with a wall or boundary. +/// +/// The normal vector of the surface the bat is bouncing against. +public void Bounce(Vector2 normal) +{ + Vector2 newPosition = Position; + + // Adjust the position based on the normal to prevent sticking to walls. + if(normal.X != 0) + { + // We are bouncing off a vertical wall (left/right). + // Move slightly away from the wall in the direction of the normal. + newPosition.X += normal.X * (_sprite.Width * 0.1f); + } + + if(normal.Y != 0) + { + // We are bouncing off a horizontal wall (top/bottom). + // Move slightly way from the wall in the direction of the normal. + newPosition.Y += normal.Y * (_sprite.Height * 0.1f); + } + + // Apply the new position + Position = newPosition; + + // Apply reflection based on the normal. + _velocity = Vector2.Reflect(_velocity, normal); + + // Play the bounce sound effect. + Core.Audio.PlaySoundEffect(_bounceSoundEffect); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/constructor.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/constructor.cs new file mode 100644 index 00000000..1112cbcf --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/constructor.cs @@ -0,0 +1,10 @@ +/// +/// Creates a new Bat using the specified animated sprite and sound effect. +/// +/// The AnimatedSprite ot use when drawing the bat. +/// The sound effect to play when the bat bounces off a wall. +public Bat(AnimatedSprite sprite, SoundEffect bounceSoundEffect) +{ + _sprite = sprite; + _bounceSoundEffect = bounceSoundEffect; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/definition.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/definition.cs new file mode 100644 index 00000000..7bb9721c --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/definition.cs @@ -0,0 +1,12 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using MonoGameLibrary; +using MonoGameLibrary.Graphics; + +namespace DungeonSlime.GameObjects; + +public class Bat +{ + +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/draw.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/draw.cs new file mode 100644 index 00000000..904ded21 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/draw.cs @@ -0,0 +1,7 @@ +/// +/// Draws the bat. +/// +public void Draw() +{ + _sprite.Draw(Core.SpriteBatch, Position); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/fields.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/fields.cs new file mode 100644 index 00000000..3039ab77 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/fields.cs @@ -0,0 +1,11 @@ +private const float MOVEMENT_SPEED = 5.0f; + +// The velocity of the bat that defines the direction and how much in that +// direction to update the bats position each update cycle. +private Vector2 _velocity; + +// The AnimatedSprite used when drawing the bat. +private AnimatedSprite _sprite; + +// The sound effect to play when the bat bounces off the edge of the room. +private SoundEffect _bounceSoundEffect; \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/getbounds.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/getbounds.cs new file mode 100644 index 00000000..1cf7f921 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/getbounds.cs @@ -0,0 +1,12 @@ +/// +/// Returns a Circle value that represents collision bounds of the bat. +/// +/// A Circle value. +public Circle GetBounds() +{ + int x = (int)(Position.X + _sprite.Width * 0.5f); + int y = (int)(Position.Y + _sprite.Height * 0.5f); + int radius = (int)(_sprite.Width * 0.25f); + + return new Circle(x, y, radius); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/properties.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/properties.cs new file mode 100644 index 00000000..095e1de9 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/properties.cs @@ -0,0 +1,4 @@ +/// +/// Gets or Sets the position of the bat. +/// +public Vector2 Position { get; set;} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/randomizevelocity.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/randomizevelocity.cs new file mode 100644 index 00000000..1fe80487 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/randomizevelocity.cs @@ -0,0 +1,17 @@ +/// +/// Randomizes the velocity of the bat. +/// +public void RandomizeVelocity() +{ + // Generate a random angle + float angle = (float)(Random.Shared.NextDouble() * MathHelper.TwoPi); + + // Convert the angle to a direction vector + float x = (float)Math.Cos(angle); + float y = (float)Math.Sin(angle); + Vector2 direction = new Vector2(x, y); + + // Multiply the direction vector by the movement speed to get the + // final velocity + _velocity = direction * MOVEMENT_SPEED; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/update.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/update.cs new file mode 100644 index 00000000..94a87d81 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/bat/update.cs @@ -0,0 +1,12 @@ +/// +/// Updates the bat. +/// +/// A snapshot of the timing values for the current update cycle. +public void Update(GameTime gameTime) +{ + // Update the animated sprite + _sprite.Update(gameTime); + + // Update the position of the bat based on the velocity. + Position += _velocity; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/gamecontroller.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/gamecontroller.cs new file mode 100644 index 00000000..aa9a7d10 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/gamecontroller.cs @@ -0,0 +1,79 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary; +using MonoGameLibrary.Input; + +namespace DungeonSlime; + +/// +/// Provides a game-specific input abstraction that maps physical inputs +/// to game actions, bridging our input system with game-specific functionality. +/// +public static class GameController +{ + private static KeyboardInfo s_keyboard => Core.Input.Keyboard; + private static GamePadInfo s_gamePad => Core.Input.GamePads[(int)PlayerIndex.One]; + + /// + /// Returns true if the player has triggered the "move up" action. + /// + public static bool MoveUp() + { + return s_keyboard.WasKeyJustPressed(Keys.Up) || + s_keyboard.WasKeyJustPressed(Keys.W) || + s_gamePad.WasButtonJustPressed(Buttons.DPadUp) || + s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickUp); + } + + /// + /// Returns true if the player has triggered the "move down" action. + /// + public static bool MoveDown() + { + return s_keyboard.WasKeyJustPressed(Keys.Down) || + s_keyboard.WasKeyJustPressed(Keys.S) || + s_gamePad.WasButtonJustPressed(Buttons.DPadDown) || + s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickDown); + } + + /// + /// Returns true if the player has triggered the "move left" action. + /// + public static bool MoveLeft() + { + return s_keyboard.WasKeyJustPressed(Keys.Left) || + s_keyboard.WasKeyJustPressed(Keys.A) || + s_gamePad.WasButtonJustPressed(Buttons.DPadLeft) || + s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickLeft); + } + + /// + /// Returns true if the player has triggered the "move right" action. + /// + public static bool MoveRight() + { + return s_keyboard.WasKeyJustPressed(Keys.Right) || + s_keyboard.WasKeyJustPressed(Keys.D) || + s_gamePad.WasButtonJustPressed(Buttons.DPadRight) || + s_gamePad.WasButtonJustPressed(Buttons.LeftThumbstickRight); + } + + /// + /// Returns true if the player has triggered the "pause" action. + /// + public static bool Pause() + { + return s_keyboard.WasKeyJustPressed(Keys.Escape) || + s_gamePad.WasButtonJustPressed(Buttons.Start); + } + + /// + /// Returns true if the player has triggered the "action" button, + /// typically used for menu confirmation. + /// + public static bool Action() + { + return s_keyboard.WasKeyJustPressed(Keys.Enter) || + s_gamePad.WasButtonJustPressed(Buttons.A); + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/constructor.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/constructor.cs new file mode 100644 index 00000000..937eb66d --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/constructor.cs @@ -0,0 +1,8 @@ +/// +/// Creates a new Slime using the specified animated sprite. +/// +/// The AnimatedSprite to use when drawing the slime. +public Slime(AnimatedSprite sprite) +{ + _sprite = sprite; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/definition.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/definition.cs new file mode 100644 index 00000000..3a62e9a2 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/definition.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using MonoGameLibrary; +using MonoGameLibrary.Graphics; + +namespace DungeonSlime.GameObjects; + +public class Slime +{ + +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/draw.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/draw.cs new file mode 100644 index 00000000..6b8a122b --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/draw.cs @@ -0,0 +1,18 @@ +/// +/// Draws the slime. +/// +public void Draw() +{ + // Iterate through each segment and draw it + foreach (SlimeSegment segment in _segments) + { + // Calculate the visual position of the segment at the moment by + // lerping between its "at" and "to" position by the movement + // offset lerp amount + Vector2 pos = Vector2.Lerp(segment.At, segment.To, _movementProgress); + + // Draw the slime sprite at the calculated visual position of this + // segment + _sprite.Draw(Core.SpriteBatch, pos); + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/events.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/events.cs new file mode 100644 index 00000000..7c475406 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/events.cs @@ -0,0 +1,5 @@ +/// +/// Event that is raised if it is detected that the head segment of the slime +/// has collided with a body segment. +/// +public event EventHandler BodyCollision; \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/fields.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/fields.cs new file mode 100644 index 00000000..08dd4bca --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/fields.cs @@ -0,0 +1,22 @@ +// A constant value that represents the amount of time to wait between +// movement updates. +private static readonly TimeSpan s_movementTime = TimeSpan.FromMilliseconds(200); + +// The amount of time that has elapsed since the last movement update. +private TimeSpan _movementTimer; + +// Normalized value (0-1) representing progress between movement ticks for visual interpolation +private float _movementProgress; + +// The next direction to apply to the head of the slime chain during the +// next movement update. +private Vector2 _nextDirection; + +// The number of pixels to move the head segment during the movement cycle. +private float _stride; + +// Tracks the segments of the slime chain. +private List _segments; + +// The AnimatedSprite used when drawing each slime segment +private AnimatedSprite _sprite; \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/getbounds.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/getbounds.cs new file mode 100644 index 00000000..3ee0d033 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/getbounds.cs @@ -0,0 +1,22 @@ +/// +/// Returns a Circle value that represents collision bounds of the slime. +/// +/// A Circle value. +public Circle GetBounds() +{ + SlimeSegment head = _segments[0]; + + // Calculate the visual position of the head at the moment of this + // method call by lerping between the "at" and "to" position by the + // movement offset lerp amount + Vector2 pos = Vector2.Lerp(head.At, head.To, _movementProgress); + + // Create the bounds using the calculated visual position of the head. + Circle bounds = new Circle( + (int)(pos.X + (_sprite.Width * 0.5f)), + (int)(pos.Y + (_sprite.Height * 0.5f)), + (int)(_sprite.Width * 0.5f) + ); + + return bounds; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/grow.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/grow.cs new file mode 100644 index 00000000..c10cf5a8 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/grow.cs @@ -0,0 +1,18 @@ +/// +/// Informs the slime to grow by one segment. +/// +public void Grow() +{ + // Capture the value of the tail segment + SlimeSegment tail = _segments[_segments.Count - 1]; + + // Create a new tail segment that is positioned a grid cell in the + // reverse direction from the tail moving to the tail. + SlimeSegment newTail = new SlimeSegment(); + newTail.At = tail.To + tail.ReverseDirection * _stride; + newTail.To = tail.At; + newTail.Direction = Vector2.Normalize(tail.At - newTail.At); + + // Add the new tail segment + _segments.Add(newTail); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/handleinput.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/handleinput.cs new file mode 100644 index 00000000..9a51f27b --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/handleinput.cs @@ -0,0 +1,29 @@ +private void HandleInput() +{ + Vector2 potentialNextDirection = _nextDirection; + + if (GameController.MoveUp()) + { + potentialNextDirection = -Vector2.UnitY; + } + else if (GameController.MoveDown()) + { + potentialNextDirection = Vector2.UnitY; + } + else if (GameController.MoveLeft()) + { + potentialNextDirection = -Vector2.UnitX; + } + else if (GameController.MoveRight()) + { + potentialNextDirection = Vector2.UnitX; + } + + // Only allow direction change if it is not reversing the current + // direction. This prevents the slime from backing into itself. + float dot = Vector2.Dot(potentialNextDirection, _segments[0].Direction); + if (dot >= 0) + { + _nextDirection = potentialNextDirection; + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/initialize.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/initialize.cs new file mode 100644 index 00000000..3321a5bc --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/initialize.cs @@ -0,0 +1,29 @@ +/// +/// Initializes the slime, can be used to reset it back to an initial state. +/// +/// The position the slime should start at. +/// The total number of pixels to move the head segment during each movement cycle. +public void Initialize(Vector2 startingPosition, float stride) +{ + // Initialize the segment collection. + _segments = new List(); + + // Set the stride + _stride = stride; + + // Create the initial head of the slime chain. + SlimeSegment head = new SlimeSegment(); + head.At = startingPosition; + head.To = startingPosition + new Vector2(_stride, 0); + head.Direction = Vector2.UnitX; + + // Add it to the segment collection. + _segments.Add(head); + + // Set the initial next direction as the same direction the head is + // moving. + _nextDirection = head.Direction; + + // Zero out the movement timer. + _movementTimer = TimeSpan.Zero; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/move.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/move.cs new file mode 100644 index 00000000..d3b6c5ec --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/move.cs @@ -0,0 +1,42 @@ +private void Move() +{ + // Capture the value of the head segment + SlimeSegment head = _segments[0]; + + // Update the direction the head is supposed to move in to the + // next direction cached. + head.Direction = _nextDirection; + + // Update the head's "at" position to be where it was moving "to" + head.At = head.To; + + // Update the head's "to" position to the next tile in the direction + // it is moving. + head.To = head.At + head.Direction * _stride; + + // Insert the new adjusted value for the head at the front of the + // segments and remove the tail segment. This effectively moves + // the entire chain forward without needing to loop through every + // segment and update its "at" and "to" positions. + _segments.Insert(0, head); + _segments.RemoveAt(_segments.Count - 1); + + // Iterate through all of the segments except the head and check + // if they are at the same position as the head. If they are, then + // the head is colliding with a body segment and a body collision + // has occurred. + for (int i = 1; i < _segments.Count; i++) + { + SlimeSegment segment = _segments[i]; + + if (head.At == segment.At) + { + if(BodyCollision != null) + { + BodyCollision.Invoke(this, EventArgs.Empty); + } + + return; + } + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/update.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/update.cs new file mode 100644 index 00000000..4d9bec68 --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slime/update.cs @@ -0,0 +1,26 @@ +/// +/// Updates the slime. +/// +/// A snapshot of the timing values for the current update cycle. +public void Update(GameTime gameTime) +{ + // Update the animated sprite. + _sprite.Update(gameTime); + + // Handle any player input + HandleInput(); + + // Increment the movement timer by the frame elapsed time. + _movementTimer += gameTime.ElapsedGameTime; + + // If the movement timer has accumulated enough time to be greater than + // the movement time threshold, then perform a full movement. + if (_movementTimer >= s_movementTime) + { + _movementTimer -= s_movementTime; + Move(); + } + + // Update the movement lerp offset amount + _movementProgress = (float)(_movementTimer.TotalSeconds / s_movementTime.TotalSeconds); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slimesegment.cs b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slimesegment.cs new file mode 100644 index 00000000..c79d8c4e --- /dev/null +++ b/articles/tutorials/building_2d_games/22_snake_game_mechanics/snippets/slimesegment.cs @@ -0,0 +1,26 @@ +using Microsoft.Xna.Framework; + +namespace DungeonSlime.GameObjects + +public struct SlimeSegment +{ + /// + /// The position this slime segment is at before the movement cycle occurs. + /// + public Vector2 At; + + /// + /// The position this slime segment should move to during the next movement cycle. + /// + public Vector2 To; + + /// + /// The direction this slime segment is moving. + /// + public Vector2 Direction; + + /// + /// The opposite direction this slime segment is moving. + /// + public Vector2 ReverseDirection => new Vector2(-Direction.X, -Direction.Y); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/index.md b/articles/tutorials/building_2d_games/23_completing_the_game/index.md new file mode 100644 index 00000000..c977a9ee --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/index.md @@ -0,0 +1,406 @@ +--- +title: "Chapter 23: Completing the Game" +description: "Finalize game mechanics by updating our current demo into a snake-like inspired game." +--- + +In [Chapter 22](../22_snake_game_mechanics/index.md) we implemented the core mechanics of a snake-like game by creating the [`Slime`](../22_snake_game_mechanics/index.md#the-slime-class), [`Bat`](../22_snake_game_mechanics/index.md#the-bat-class) and [`GameController`](../22_snake_game_mechanics/index.md#the-gamecontroller-class) classes. While these classes handle the foundational gameplay, a complete game needs additional elements to provide player feedback, manage game states, and create a polished experience. + +In this chapter, you will: + +- Create a dedicated UI class to manage the UI for the game scene. +- Implement pause and game over screens with appropriate controls. +- Refactor the `GameScene` class to coordinate all game elements. +- Add game state management to handle playing, paused, and game over conditions +- Implement input buffering to improve control responsiveness +- Connect all elements to create a complete, playable game. + +## The GameSceneUI Class + +Currently, the `GameScene` class contains the methods for initializing and creating the pause menu.  However, now that we have a defined condition for game over, we need to create a game-over menu as well.  To do this, we will take the opportunity to refactor the current code and pull the UI-specific code into its own class. + +In the *UI* directory of the game project, create a new file named *GameSceneUI* and add the following initial code: + +[!code-csharp[](./snippets/gamesceneui/definition.cs)] + +This code establishes the foundation for our `GameSceneUI` class, which inherits from Gum's `ContainerRuntime` class. This inheritance means our UI class is itself a UI container that can hold and manage other UI elements. We've included all necessary using statements for MonoGame, Gum UI components, and our library references. + +Let's build out this class by adding each section in sequence. Follow the order below to create the complete UI management system for our game scene. + +> [!NOTE] +> You may see compiler errors as you add these sections one by one. This is expected because some parts of the code will reference fields, properties, or methods that we haven't added yet. Once all sections are in place, these errors will resolve. + +### GameSceneUI Fields + +Add the following fields to the `GameSceneUI` class: + +[!code-csharp[](./snippets/gamesceneui/fields.cs)] + +Let's break down what each of these fields is responsible for: + +- `s_scoreFormat`: A string format template used to display the player's score with leading zeros. +- `_uiSoundEffect`: Stores the sound effect played for UI interactions like button clicks and focus changes. +- `_pausePanel`: The panel containing the UI elements shown when the game is paused. +- `_resumeButton`: A reference to the resume button, allowing us to set focus on it when the pause panel is shown. +- `_gameOverPanel`: The panel containing the UI elements shown when a game over occurs. +- `_retryButton`: A reference to the retry button, allowing us to set focus to it when the game over panel is shown. +- `_scoreText`: The text display showing the player's current score. + +### GameSceneUI Events + +After the fields, add the following events to the `GameSceneUI` class: + +[!code-csharp[](./snippets/gamesceneui/events.cs)] + +These events allow the `GameSceneUI` class to notify the `GameScene` when important UI actions occur: + +- `ResumeButtonClick`: Triggered when the player clicks the Resume button on the pause panel. +- `QuitButtonClick`: Triggered when the player clicks the Quit button on either panel. +- `RetryButtonClick`: Triggered when the player clicks the Retry button on the game over panel. + +### GameSceneUI Constructor + +Add the following constructor to the `GameSceneUI` class after the events: + +[!code-csharp[](./snippets/gamesceneui/constructor.cs)] + +This constructor initializes all UI components: + +1. Set the container to fill the entire screen. +2. Adds itself to Gum's root element. +3. Loads necessary assets (sound effect and texture atlas). +4. Creates and adds child elements in the correct order. + +### GameSceneUI UI Creation Methods + +To keep the code more organized, we will create separate functions to build the individual UI elements that will be managed by the `GameSceneUI` class. + +#### Creating the Score Text + +To display the player's score, we will begin by adding a method to create a `TextRuntime` element. Add the following method to the `GameSceneUI` after the constructor: + +[!code-csharp[](./snippets/gamesceneui/createscoretext.cs)] + +#### Creating the Pause Panel + +Next, we will add a method to create a `Panel` element that is shown when the game is paused, including the "Resume" and "Quit" buttons. Add the following method to the `GameSceneUI` class after the `CreateScoreText` method: + +[!code-csharp[](./snippets/gamesceneui/createpausepanel.cs)] + +#### Creating the Game Over Panel + +Finally, we will add a method to create a `Panel` element that is shown when a game over occurs, including the "Retry" and "Quit" buttons. Add the following method to the `GameSceneUI` class after the `CreatePausePanel` method: + +[!code-csharp[](./snippets/gamesceneui/creategameoverpanel.cs)] + +Both the pause panel and the game over panel use event handlers for their buttons. Let's add those next. + +### GameSceneUI Event Handlers + +After the `CreateGameOverPanel` method, add the following method to the `GameSceneUI` class: + +[!code-csharp[](./snippets/gamesceneui/eventhandlers.cs)] + +These event handlers provide audio feedback and appropriate UI updates when buttons are clicked or UI elements receive focus. + +### GameSceneUI Public Methods + +Finally, add the following public methods to the `GameSceneUI` class after the `OnElementGotFocus` method: + +[!code-csharp[](./snippets/gamesceneui/publicmethods.cs)] + +These public methods provide the interface for the `GameScene` to: + +- Update the score display. +- Show or hide the pause menu. +- Show or hide the game over menu. +- Update and draw the UI components. + +With the `GameSceneUI` class complete, we now have a fully encapsulated UI system that can handle displaying game information (score), providing feedback for game states (pause, game over), and processing user interactions (button clicks). This separation of UI logic from game logic will make our codebase much easier to maintain and extend. + +Now that we have all our specialized components ready, let's refactor the GameScene class to coordinate between them and manage the overall game flow. + +## Refactoring The GameScene Class + +Now that we have created the encapsulated [`Slime`](../22_snake_game_mechanics/index.md#the-slime-class), [`Bat`](../22_snake_game_mechanics/index.md#the-bat-class), and [`GameSceneUI`](#the-gamesceneui-class) classes, we can refactor the `GameScene` class to leverage these new components.  This will make our code more maintainable and allow us to focus on the game logic within the scene itself.  We will rebuild the `GameScene` class to coordinate the interactions between the components. + +In the *Scenes* directory of the DungeonSlime project (your main game project), open the *GameScene.cs* file and replace the code with the following initial code: + +[!code-csharp[](./snippets/gamescene/definition.cs)] + +This code provides the foundation for our refactored `GameScene` class. We have included all the necessary using statements to reference our new game object classes and UI components. The class will now focus on managing the game state and coordinating between our specialized component classes rather than implementing all the functionality directly. + +The `GameScene` class now contains the following key fields: + +- `GameState`: An enum that defines the different states that the game can be in (playing, paused, or game over). +- `_slime`: A reference to the slime (snake-like player character) instance. +- `_bat`: A reference to the bat (food) instance. +- `_tilemap`: The tilemap that defines the level layout. +- `_roomBounds`: A rectangle defining the playable area within the walls. +- `_collectSoundEffect`: The sound effect played when the slime eats a bat. +- `_score`: Tracks the player's current score. +- `_ui`: A reference to the game scene UI component. +- `_state`: The current state of the game represented by the `GameState` enum. + +Now we will add the various methods needed to complete the `GameScene` class. Add each section in the sequence presented below. This will build up the scene's functionality step by step. + +> [!NOTE] +> As with previous classes, you might encounter compiler errors until all sections are in place. These errors will be resolved once all components of the class have been added. + +### GameScene Initialize Method + +To set up the scene, add the following `Initialize` method after the fields in te `GameScene` class: + +[!code-csharp[](./snippets/gamescene/initialize.cs)] + +This method sets up the initial state of the game scene: + +1. Disables the "exit on escape" behavior so we can use the escape key for pausing. +2. Calculate the playable area within the tilemap walls. +3. Subscribes to the slime's body collision event to detect when the player collides with itself triggering a game over state. +4. Initialize the UI components. +5. Set up a new game. + +### GameScene InitializeUI Method + +The `Initialize` method we just added calls a method to initialize the user interface for the scene. Let's add that method now. Add the following method after the `Initialize` method in the `GameScene` class: + +[!code-csharp[](./snippets/gamescene/initializeui.cs)] + +This method creates the UI components and subscribes to its events to respond to button clicks. + +### GameScene UI Event Handlers + +In the `InitializeUI` method we just added, we subscribe to the events from the `GameSceneUI` class that are triggered when buttons are clicked. Now we need to add those methods that would be called when the events are triggered. Add the following methods to the `GameScene` class after the `InitializeUI` method: + +[!code-csharp[](./snippets/gamescene/eventhandlers.cs)] + +These methods respond to the UI events: + +- `OnResumeButtonClicked`: Resumes the game from a paused state. +- `OnRetryButtonClicked`: Restarts the game after a game over. +- `OnQuitButtonClicked`: Quits the game by returning to the title scene. + +### GameScene InitializeNewGame Method + +In the `Initialize` method we added above, it also makes a call to an `InitializeNewGame` method. Let's add this now. Add the following method to the `GameScene` class after the `OnQuitButtonClicked` method: + +[!code-csharp[](./snippets/gamescene/initializenewgame.cs)] + +This method will: + +1. Position the slime in the center of the map. +2. Initialize the slime with its starting position and movement stride. +3. Randomize the bat's velocity and position it away from the slime. +4. Reset the player's score. +5. Set the game state to "Playing". + +### GameScene LoadContent Method + +Next, we need to add the method to load game assets for the scene. Add the following method to the `GameScene` class after the `InitializeNewGame` method: + +[!code-csharp[](./snippets/gamescene/loadcontent.cs)] + +This method loads all necessary assets for the game scene: + +1. The texture atlas containing the sprite graphics +2. The tilemap that defines the level layout. +3. The animated sprites for the slime and bat. +4. Sound effects for the bat bouncing and collecting. + +### GameScene Update Method + +Next, to update the scene add the following method to the `GameScene` class after the `LoadContent` method: + +[!code-csharp[](./snippets/gamescene/update.cs)] + +This method updates the scene in each frame to: + +1. Always update the UI, regardless of game state. +2. Return early if the game is over. +3. Check for pause input and toggle the pause state if needed. +4. Return early if the game is paused. +5. Update the slime and bat. +6. Check for collisions between the game objects. + +### GameScene CollisionChecks Method + +In the `Update` method we just added, it makes a call to a `CollisionChecks` method to handle the collision detection and response. Let's add that method now. Add the following method to the `GameScene` class after the `Update` method: + +[!code-csharp[](./snippets/gamescene/collisionchecks.cs)] + +This method checks for three types of collisions: + +1. Slime-Bat collision: The slime "eats" the bat, gains points, grows, and the bat respawns. +2. Slime-Wall collision: Triggers a game over if the slime hits a wall. +3. Bat-Wall collision: Causes the bat to bounce off the walls. + +### GameScene PositionBatAwayFromSlime Method + +The `CollisionCheck` method makes a call to `PositionBatAwayFromSlime`. Previously, when we needed to set the position of the bat when it respawns, we simply chose a random tile within the tilemap to move it to. By choosing a completely random location, it could be on top fo the head segment of the slime, forcing an instant collision, or it could spawn very close to the head segment, which adds not challenge for the player. + +To ensure the bat appears in a random, but strategic location, we can instead set it to position away from the slime on the opposite side of the room. Add the following method to the `GameScene` class after the `CollisionCheck` method: + +[!code-csharp[](./snippets/gamescene/positionbatawayfromslime.cs)] + +This method positions the bat after it's been eaten: + +1. Determines which wall (top, bottom, left, or right) is furthest from the slime. +2. Places the bat near that wall, making it more challenging for the player to reach. + +### GameScene Event Handler and Game State Methods + +Next, we will add some of the missing methods being called from above that handle game events and state changes. Add the following methods to the `GameScene` class after the `PositionBatAwayFromSlime` method: + +[!code-csharp[](./snippets/gamescene/statechanges.cs)] + +These methods handle specific game events: + +- `OnSlimeBodyCollision`: Called when the slime collides with itself, triggering a game over. +- `TogglePause`: Switches between paused and playing states. +- `GameOver`: Called when a game over condition is met, showing the game over UI. + +### GameScene Draw Method + +Finally, we need a method to draw the scene. Add the following method to the `GameScene `class after the `GameOver` method. + +[!code-csharp[](./snippets/gamescene/draw.cs)] + +This method handles drawing the scene by: + +1. Clearing the screen. +2. Drawing the tilemap as the background. +3. Drawing the slime and bat sprites. +4. Drawing the UI elements on top. + +By refactoring our game into these encapsulated components, we have created a more maintainable codebase with a clear separation of concerns: + +- The `Slime` class handles snake-like movement and growth. +- The `Bat` class manages its movement and bouncing. +- The `GameSceneUI` class manages all UI components. +- The `GameScene` class coordinates between these components and manages the game state. + +This architecture makes it easier to add new features or fix bugs, as changes to one component are less likely to affect others. + +## Adding Input Buffering to the Slime Class + +The game at this point is now playable. If you test it out though, you may notice a small issue with inputs. As we discussed in [Chapter 10](../10_handling_input/index.md#input-buffering), in games where movement updates happen at fixed intervals, inputs can sometimes feel unresponsive, especially when trying to make multiple inputs in succession. + +For instance, if a player wants to navigate a tight corner by pressing up and then immediately left, pressing these keys in rapid succession often results in only the second input being registered. When this happens, the slime will only turn left without first moving upward, missing the intended two-part movement completely. This occurs because the second input overwrites the first one before the game has a chance to process it, leading to frustrating gameplay. + +Let's implement the input buffering technique we introduced in [Chapter 10](../10_handling_input/index.md#input-buffering) to solve this problem in our `Slime` class. + +### Implementing Input Buffering in the Slime Class + +To add input buffering for the `Slime` class, we will begin by adding the necessary fields to store our input queue. In the *GameObjects* directory of the *DungeonSlime* project (your main game project), open the *Slime.cs* file and add the following fields after the `_sprite` field: + +[!code-csharp[](./snippets/slime/fields.cs)] + +The queue will store the directional vectors (up, down, left, right) that we will apply to the slime's movement in the order they were received. + +Next, we need to initialize the queue. In the `Slime` class, locate the `Initialize` method and and update it to the following: + +[!code-csharp[](./snippets/slime/initialize.cs?highlight=30-31)] + +Next, we need to update the input handling method to store the inputs in the queue instead of immediately overwriting the `_nextDirection` field. In the `Slime` class, locate the `HandleInput` method and update it to the following + +[!code-csharp[](./snippets/slime/handleinput.cs?highlight=3,22-38)] + +1. The `potentialNewDirection` is now given the initial value of [**Vector2.Zero**](xref:Microsoft.Xna.Framework.Vector2.Zero). +2. A check is made to see if the player has pressed a direction key and if the input buffer is not already at maximum capacity. +3. If a new direction key is pressed and the buffer has space: + 1. The validation is made using [**Vector2.Dot**](xref:Microsoft.Xna.Framework.Vector2.Dot(Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2)) just like before to ensure it is a valid direction + 2. If it is a valid direciton, then it is added to the queue. + +Finally, we need to modifiy how we apply the movement direction during the movement update cycle. In the `Slime` class, locate the `Move` method and update it to the following: + +[!code-csharp[](./snippets/slime/move.cs?highlight=3-7)] + +The key change here is that we now dequeue a direction from the input buffer rather than directly using the `_nextDirection` value. This ensures we process inputs in the order they were received, preserving the player's intent. + +With these changes in place, our game now supports input buffering. This small enhancement improves how the game feels to play, particularly when making rapid directional changes. + +Players will notice: + +- When navigating a corner, they can quickly press up followed by left (or any other valid combination), and both inputs will be respected +- The game feels more responsive since it remembers inputs between movement update cycles +- Complex maneuvers are easier to execute since timing is more forgiving + +The difference might seem subtle, but it significantly reduces frustration during gameplay. + +## Putting It All Together + +With all of these components now in place, our Dungeon Slime game has transformed from a simple demo built on learning MonoGame concepts into a complete snake-like game experience.  The player controls the slime that moves through the dungeon, consuming bats to grow longer.  If the slime collides with the wall or its own body, the game ends. + +Let's see how it all looks and plays: + +| ![Figure 23-1: Gameplay demonstration of the completed Dungeon Slime game showing the snake-like slime growing as it eats bats and a game over when colliding with the wall ](./videos/gameplay.webm) | +| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| **Figure 23-1: Gameplay demonstration of the completed Dungeon Slime game showing the snake-like slime growing as it eats bats and a game over when colliding with the wall** | + +1. The game starts with a single slime segment in the center of the room. +2. The player controls the direction of the slime by using the keyboard (arrow keys or WASD) or by using a game pad (DPad or left thumbstick). +3. The slime moves at regular intervals, creating a grid-based movement pattern. +4. When the slime eats a bat, it grows longer by adding a new segment to its tail. +5. The bat respawns at a strategic location after being eaten. +6. The player's score increases with each bat consumed. +7. If the slime collides with a wall or its own body, the game over panel appears. +8. On the game over panel, the player can choose to retry or return to the title scene. + +With these mechanics implemented, Dungeon Slime is now a complete game with clear objectives, escalating difficulty, and a game feedback loop. + +## Conclusion + +In this chapter, we have transformed our technical demo into a complete game by integrating UI systems with game mechanics. We have accomplished several important goals: + +- Created a dedicated [`GameSceneUI`](#the-gamesceneui-class) class to manage the game's user interface. +- Implemented pause and game over screens that provide clear feedback to the player. +- Refactored the `GameScene` class to coordinate all game components. +- Added game state management to handle different gameplay conditions. +- Enhanced player control through input buffering for more responsive gameplay +- Connected all of the elements to create a complete playable game. + +The refactoring process we undertook demonstrates an important game development principle: separating concerns into specialized components makes code more maintainable and easier to extend. The `Slime` class manages snake-like behavior, the `Bat` class handles movement and collision response, and the `GameSceneUI` class encapsulates all UI-related functionality. + +## Test Your Knowledge + +1. How does the game handle different states (playing, paused, game over), and why is this state management important? + + :::question-answer + The game uses an enum (`GameState`) to track its current state and implements different behavior based on that state: + + - During the `Playing` state, the game updates all objects and checks for collisions + - During the `Paused` state, the game shows the pause menu and stops updating game objects + - During the `GameOver` state, the game shows the game over menu and prevents further gameplay + + This state management is important because it: + + - Prevents inappropriate updates during non-gameplay states + - Creates a clear flow between different game conditions + - Simplifies conditional logic by using explicit states rather than multiple boolean flags + - Makes the game's behavior more predictable and easier to debug + + ::: + +2. Why is it important to position the bat away from the slime after it's been eaten rather than at a completely random location? + + :::question-answer + Positioning the bat away from the slime after it's been eaten rather than at a completely random location is important because: + + - It prevents unfair situations where the bat might spawn right on top of the slime causing an immediate collision + - It creates a more strategic gameplay experience by forcing the player to navigate toward the bat + - It ensures the player faces an appropriate level of challenge that increases as the slime grows longer + - It prevents potential frustration from random spawns that might be either too easy or too difficult to reach + - It creates a more balanced and predictable game experience while still maintaining variety + ::: + +3. What problem does input buffering solve and how does our implementation address it? + + :::question-answer + Input buffering solves the timing disconnect between when players press buttons and when the game can actually process those inputs in games with fixed movement cycles. Without buffering, inputs that occur between movement cycles are lost, especially when players make rapid sequential inputs like navigating corners. + + Our implementation addresses this by: + + - Using a queue data structure to store up to two directional inputs + - Processing inputs in First-In-First-Out order to preserve the player's intended sequence + - Validating each input against the previous one to prevent impossible movements + ::: diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/collisionchecks.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/collisionchecks.cs new file mode 100644 index 00000000..7fc83597 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/collisionchecks.cs @@ -0,0 +1,63 @@ +private void CollisionChecks() +{ + // Capture the current bounds of the slime and bat + Circle slimeBounds = _slime.GetBounds(); + Circle batBounds = _bat.GetBounds(); + + // FIrst perform a collision check to see if the slime is colliding with + // the bat, which means the slime eats the bat. + if (slimeBounds.Intersects(batBounds)) + { + // Move the bat to a new position away from the slime. + PositionBatAwayFromSlime(); + + // Randomize the velocity of the bat. + _bat.RandomizeVelocity(); + + // Tell the slime to grow. + _slime.Grow(); + + // Increment the score. + _score += 100; + + // Update the score display on the UI. + _ui.UpdateScoreText(_score); + + // Play the collect sound effect + Core.Audio.PlaySoundEffect(_collectSoundEffect); + } + + // Next check if the slime is colliding with the wall by validating if + // it is within the bounds of the room. If it is outside the room + // bounds, then it collided with a wall which triggers a game over. + if (slimeBounds.Top < _roomBounds.Top || + slimeBounds.Bottom > _roomBounds.Bottom || + slimeBounds.Left < _roomBounds.Left || + slimeBounds.Right > _roomBounds.Right) + { + GameOver(); + return; + } + + // Finally, check if the bat is colliding with a wall by validating if + // it is within the bounds of the room. If it is outside the room + // bounds, then it collided with a wall, and the bat should bounce + // off of that wall. + if (batBounds.Top < _roomBounds.Top) + { + _bat.Bounce(Vector2.UnitY); + } + else if (batBounds.Bottom > _roomBounds.Bottom) + { + _bat.Bounce(-Vector2.UnitY); + } + + if (batBounds.Left < _roomBounds.Left) + { + _bat.Bounce(Vector2.UnitX); + } + else if (batBounds.Right > _roomBounds.Right) + { + _bat.Bounce(-Vector2.UnitX); + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/definition.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/definition.cs new file mode 100644 index 00000000..a2ee52a1 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/definition.cs @@ -0,0 +1,44 @@ +using System; +using DungeonSlime.GameObjects; +using DungeonSlime.UI; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Graphics; +using MonoGameGum; +using MonoGameLibrary; +using MonoGameLibrary.Graphics; +using MonoGameLibrary.Scenes; + +namespace DungeonSlime.Scenes; + +public class GameScene : Scene +{ + private enum GameState + { + Playing, + Paused, + GameOver + } + + // Reference to the slime. + private Slime _slime; + + // Reference to the bat. + private Bat _bat; + + // Defines the tilemap to draw. + private Tilemap _tilemap; + + // Defines the bounds of the room that the slime and bat are contained within. + private Rectangle _roomBounds; + + // The sound effect to play when the slime eats a bat. + private SoundEffect _collectSoundEffect; + + // Tracks the players score. + private int _score; + + private GameSceneUI _ui; + + private GameState _state; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/draw.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/draw.cs new file mode 100644 index 00000000..d27a31dd --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/draw.cs @@ -0,0 +1,23 @@ +public override void Draw(GameTime gameTime) +{ + // Clear the back buffer. + Core.GraphicsDevice.Clear(Color.CornflowerBlue); + + // Begin the sprite batch to prepare for rendering. + Core.SpriteBatch.Begin(samplerState: SamplerState.PointClamp); + + // Draw the tilemap + _tilemap.Draw(Core.SpriteBatch); + + // Draw the slime. + _slime.Draw(); + + // Draw the bat. + _bat.Draw(); + + // Always end the sprite batch when finished. + Core.SpriteBatch.End(); + + // Draw the UI + _ui.Draw(); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/eventhandlers.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/eventhandlers.cs new file mode 100644 index 00000000..806f7391 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/eventhandlers.cs @@ -0,0 +1,17 @@ +private void OnResumeButtonClicked(object sender, EventArgs args) +{ + // Change the game state back to playing + _state = GameState.Playing; +} + +private void OnRetryButtonClicked(object sender, EventArgs args) +{ + // Player has chosen to retry, so initialize a new game + InitializeNewGame(); +} + +private void OnQuitButtonClicked(object sender, EventArgs args) +{ + // Player has chosen to quit, so return back to the title scene + Core.ChangeScene(new TitleScene()); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initialize.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initialize.cs new file mode 100644 index 00000000..ed935d06 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initialize.cs @@ -0,0 +1,30 @@ +public override void Initialize() +{ + // LoadContent is called during base.Initialize(). + base.Initialize(); + + // During the game scene, we want to disable exit on escape. Instead, + // the escape key will be used to return back to the title screen + Core.ExitOnEscape = false; + + // Create the room bounds by getting the bounds of the screen then + // using the Inflate method to "Deflate" the bounds by the width and + // height of a tile so that the bounds only covers the inside room of + // the dungeon tilemap. + _roomBounds = Core.GraphicsDevice.PresentationParameters.Bounds; + _roomBounds.Inflate(-_tilemap.TileWidth, -_tilemap.TileHeight); + + // Subscribe to the slime's BodyCollision event so that a game over + // can be triggered when this event is raised. + _slime.BodyCollision += OnSlimeBodyCollision; + + // Create any UI elements from the root element created in previous + // scenes + GumService.Default.Root.Children.Clear(); + + // Initialize the user interface for the game scene. + InitializeUI(); + + // Initialize a new game to be played. + InitializeNewGame(); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initializenewgame.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initializenewgame.cs new file mode 100644 index 00000000..cd6e1ade --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initializenewgame.cs @@ -0,0 +1,21 @@ +private void InitializeNewGame() +{ + // Calculate the position for the slime, which will be at the center + // tile of the tile map. + Vector2 slimePos = new Vector2(); + slimePos.X = (_tilemap.Columns / 2) * _tilemap.TileWidth; + slimePos.Y = (_tilemap.Rows / 2) * _tilemap.TileHeight; + + // Initialize the slime + _slime.Initialize(slimePos, _tilemap.TileWidth); + + // Initialize the bat + _bat.RandomizeVelocity(); + PositionBatAwayFromSlime(); + + // Reset the score + _score = 0; + + // Set the game state to playing + _state = GameState.Playing; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initializeui.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initializeui.cs new file mode 100644 index 00000000..3ba4f071 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/initializeui.cs @@ -0,0 +1,14 @@ +private void InitializeUI() +{ + // Clear out any previous UI element incase we came here + // from a different scene. + GumService.Default.Root.Children.Clear(); + + // Create the game scene ui instance. + _ui = new GameSceneUI(); + + // Subscribe to the events from the game scene ui. + _ui.ResumeButtonClick += OnResumeButtonClicked; + _ui.RetryButtonClick += OnRetryButtonClicked; + _ui.QuitButtonClick += OnQuitButtonClicked; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/loadcontent.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/loadcontent.cs new file mode 100644 index 00000000..58b2bb92 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/loadcontent.cs @@ -0,0 +1,29 @@ +public override void LoadContent() +{ + // Create the texture atlas from the XML configuration file + TextureAtlas atlas = TextureAtlas.FromFile(Core.Content, "images/atlas-definition.xml"); + + // Create the tilemap from the XML configuration file. + _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml"); + _tilemap.Scale = new Vector2(4.0f, 4.0f); + + // Create the animated sprite for the slime from the atlas. + AnimatedSprite slimeAnimation = atlas.CreateAnimatedSprite("slime-animation"); + slimeAnimation.Scale = new Vector2(4.0f, 4.0f); + + // Create the slime + _slime = new Slime(slimeAnimation); + + // Create the animated sprite for the bat from the atlas. + AnimatedSprite batAnimation = atlas.CreateAnimatedSprite("bat-animation"); + batAnimation.Scale = new Vector2(4.0f, 4.0f); + + // Load the bounce sound effect for the bat + SoundEffect bounceSoundEffect = Content.Load("audio/bounce"); + + // Create the bat + _bat = new Bat(batAnimation, bounceSoundEffect); + + // Load the collect sound effect + _collectSoundEffect = Content.Load("audio/collect"); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/positionbatawayfromslime.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/positionbatawayfromslime.cs new file mode 100644 index 00000000..effc8599 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/positionbatawayfromslime.cs @@ -0,0 +1,76 @@ +private void PositionBatAwayFromSlime() +{ + // Calculate the position that is in the center of the bounds + // of the room. + float roomCenterX = _roomBounds.X + _roomBounds.Width * 0.5f; + float roomCenterY = _roomBounds.Y + _roomBounds.Height * 0.5f; + Vector2 roomCenter = new Vector2(roomCenterX, roomCenterY); + + // Get the bounds of the slime and calculate the center position + Circle slimeBounds = _slime.GetBounds(); + Vector2 slimeCenter = new Vector2(slimeBounds.X, slimeBounds.Y); + + // Calculate the distance vector from the center of the room to the + // center of the slime. + Vector2 centerToSlime = slimeCenter - roomCenter; + + // Get the bounds of the bat + Circle batBounds =_bat.GetBounds(); + + // Calculate the amount of padding we will add to the new position of + // the bat to ensure it is not sticking to walls + int padding = batBounds.Radius * 2; + + // Calculate the new position of the bat by finding which component of + // the center to slime vector (X or Y) is larger and in which direction. + Vector2 newBatPosition = Vector2.Zero; + if (Math.Abs(centerToSlime.X) > Math.Abs(centerToSlime.Y)) + { + // The slime is closer to either the left or right wall, so the Y + // position will be a random position between the top and bottom + // walls. + newBatPosition.Y = Random.Shared.Next( + _roomBounds.Top + padding, + _roomBounds.Bottom - padding + ); + + if (centerToSlime.X > 0) + { + // The slime is closer to the right side wall, so place the + // bat on the left side wall + newBatPosition.X = _roomBounds.Left + padding; + } + else + { + // The slime is closer ot the left side wall, so place the + // bat on the right side wall. + newBatPosition.X = _roomBounds.Right - padding * 2; + } + } + else + { + // The slime is closer to either the top or bottom wall, so the X + // position will be a random position between the left and right + // walls. + newBatPosition.X = Random.Shared.Next( + _roomBounds.Left + padding, + _roomBounds.Right - padding + ); + + if (centerToSlime.Y > 0) + { + // The slime is closer to the top wall, so place the bat on the + // bottom wall + newBatPosition.Y = _roomBounds.Top + padding; + } + else + { + // The slime is closer to the bottom wall, so place the bat on + // the top wall. + newBatPosition.Y = _roomBounds.Bottom - padding * 2; + } + } + + // Assign the new bat position + _bat.Position = newBatPosition; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/statechanges.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/statechanges.cs new file mode 100644 index 00000000..1d8f7d8a --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/statechanges.cs @@ -0,0 +1,33 @@ +private void OnSlimeBodyCollision(object sender, EventArgs args) +{ + GameOver(); +} + +private void TogglePause() +{ + if (_state == GameState.Paused) + { + // We're now unpausing the game, so hide the pause panel + _ui.HidePausePanel(); + + // And set the state back to playing + _state = GameState.Playing; + } + else + { + // We're now pausing the game, so show the pause panel + _ui.ShowPausePanel(); + + // And set the state to paused + _state = GameState.Paused; + } +} + +private void GameOver() +{ + // Show the game over panel + _ui.ShowGameOverPanel(); + + // Set the game state to game over + _state = GameState.GameOver; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/update.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/update.cs new file mode 100644 index 00000000..5db7f901 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamescene/update.cs @@ -0,0 +1,33 @@ +public override void Update(GameTime gameTime) +{ + // Ensure the UI is always updated + _ui.Update(gameTime); + + // If the game is in a game over state, immediately return back + // here + if (_state == GameState.GameOver) + { + return; + } + + // If the pause button is pressed, toggle the pause state + if (GameController.Pause()) + { + TogglePause(); + } + + // At this point, if the game is paused, just return back early + if (_state == GameState.Paused) + { + return; + } + + // Update the slime; + _slime.Update(gameTime); + + // Update the bat; + _bat.Update(gameTime); + + // Perform collision checks + CollisionChecks(); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/constructor.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/constructor.cs new file mode 100644 index 00000000..554c2bfd --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/constructor.cs @@ -0,0 +1,33 @@ +public GameSceneUI() +{ + // The game scene UI inherits from ContainerRuntime, so we set its + // doc to fill so it fills the entire screen. + Dock(Gum.Wireframe.Dock.Fill); + + // Add it to the root element. + this.AddToRoot(); + + // Get a reference to the content manager that was registered with the + // GumService when it was original initialized. + ContentManager content = GumService.Default.ContentLoader.XnaContentManager; + + // Use that content manager to load the sound effect and atlas for the + // user interface elements + _uiSoundEffect = content.Load("audio/ui"); + TextureAtlas atlas = TextureAtlas.FromFile(content, "images/atlas-definition.xml"); + + // Create the text that will display the players score and add it as + // a child to this container. + _scoreText = CreateScoreText(); + AddChild(_scoreText); + + // Create the Pause panel that is displayed when the game is paused and + // add it as a child to this container + _pausePanel = CreatePausePanel(atlas); + AddChild(_pausePanel.Visual); + + // Create the Game Over panel that is displayed when a game over occurs + // and add it as a child to this container + _gameOverPanel = CreateGameOverPanel(atlas); + AddChild(_gameOverPanel.Visual); +} diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/creategameoverpanel.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/creategameoverpanel.cs new file mode 100644 index 00000000..310d7e10 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/creategameoverpanel.cs @@ -0,0 +1,56 @@ +private Panel CreateGameOverPanel(TextureAtlas atlas) +{ + Panel panel = new Panel(); + panel.Anchor(Gum.Wireframe.Anchor.Center); + panel.Visual.WidthUnits = DimensionUnitType.Absolute; + panel.Visual.HeightUnits = DimensionUnitType.Absolute; + panel.Visual.Width = 264.0f; + panel.Visual.Height = 70.0f; + panel.IsVisible = false; + + TextureRegion backgroundRegion = atlas.GetRegion("panel-background"); + + NineSliceRuntime background = new NineSliceRuntime(); + background.Dock(Gum.Wireframe.Dock.Fill); + background.Texture = backgroundRegion.Texture; + background.TextureAddress = TextureAddress.Custom; + background.TextureHeight = backgroundRegion.Height; + background.TextureWidth = backgroundRegion.Width; + background.TextureTop = backgroundRegion.SourceRectangle.Top; + background.TextureLeft = backgroundRegion.SourceRectangle.Left; + panel.AddChild(background); + + TextRuntime text = new TextRuntime(); + text.Text = "GAME OVER"; + text.WidthUnits = DimensionUnitType.RelativeToChildren; + text.UseCustomFont = true; + text.CustomFontFile = "fonts/04b_30.fnt"; + text.FontScale = 0.5f; + text.X = 10.0f; + text.Y = 10.0f; + panel.AddChild(text); + + _retryButton = new AnimatedButton(atlas); + _retryButton.Text = "RETRY"; + _retryButton.Anchor(Gum.Wireframe.Anchor.BottomLeft); + _retryButton.Visual.X = 9.0f; + _retryButton.Visual.Y = -9.0f; + + _retryButton.Click += OnRetryButtonClicked; + _retryButton.GotFocus += OnElementGotFocus; + + panel.AddChild(_retryButton); + + AnimatedButton quitButton = new AnimatedButton(atlas); + quitButton.Text = "QUIT"; + quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight); + quitButton.Visual.X = -9.0f; + quitButton.Visual.Y = -9.0f; + + quitButton.Click += OnQuitButtonClicked; + quitButton.GotFocus += OnElementGotFocus; + + panel.AddChild(quitButton); + + return panel; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/createpausepanel.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/createpausepanel.cs new file mode 100644 index 00000000..8123d1a3 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/createpausepanel.cs @@ -0,0 +1,55 @@ +private Panel CreatePausePanel(TextureAtlas atlas) +{ + Panel panel = new Panel(); + panel.Anchor(Gum.Wireframe.Anchor.Center); + panel.Visual.WidthUnits = DimensionUnitType.Absolute; + panel.Visual.HeightUnits = DimensionUnitType.Absolute; + panel.Visual.Width = 264.0f; + panel.Visual.Height = 70.0f; + panel.IsVisible = false; + + TextureRegion backgroundRegion = atlas.GetRegion("panel-background"); + + NineSliceRuntime background = new NineSliceRuntime(); + background.Dock(Gum.Wireframe.Dock.Fill); + background.Texture = backgroundRegion.Texture; + background.TextureAddress = TextureAddress.Custom; + background.TextureHeight = backgroundRegion.Height; + background.TextureWidth = backgroundRegion.Width; + background.TextureTop = backgroundRegion.SourceRectangle.Top; + background.TextureLeft = backgroundRegion.SourceRectangle.Left; + panel.AddChild(background); + + TextRuntime text = new TextRuntime(); + text.Text = "PAUSED"; + text.UseCustomFont = true; + text.CustomFontFile = "fonts/04b_30.fnt"; + text.FontScale = 0.5f; + text.X = 10.0f; + text.Y = 10.0f; + panel.AddChild(text); + + _resumeButton = new AnimatedButton(atlas); + _resumeButton.Text = "RESUME"; + _resumeButton.Anchor(Gum.Wireframe.Anchor.BottomLeft); + _resumeButton.Visual.X = 9.0f; + _resumeButton.Visual.Y = -9.0f; + + _resumeButton.Click += OnResumeButtonClicked; + _resumeButton.GotFocus += OnElementGotFocus; + + panel.AddChild(_resumeButton); + + AnimatedButton quitButton = new AnimatedButton(atlas); + quitButton.Text = "QUIT"; + quitButton.Anchor(Gum.Wireframe.Anchor.BottomRight); + quitButton.Visual.X = -9.0f; + quitButton.Visual.Y = -9.0f; + + quitButton.Click += OnQuitButtonClicked; + quitButton.GotFocus += OnElementGotFocus; + + panel.AddChild(quitButton); + + return panel; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/createscoretext.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/createscoretext.cs new file mode 100644 index 00000000..5f29fedf --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/createscoretext.cs @@ -0,0 +1,14 @@ +private TextRuntime CreateScoreText() +{ + TextRuntime text = new TextRuntime(); + text.Anchor(Gum.Wireframe.Anchor.TopLeft); + text.WidthUnits = DimensionUnitType.RelativeToChildren; + text.X = 20.0f; + text.Y = 5.0f; + text.UseCustomFont = true; + text.CustomFontFile = @"fonts/04b_30.fnt"; + text.FontScale = 0.25f; + text.Text = string.Format(s_scoreFormat, 0); + + return text; +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/definition.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/definition.cs new file mode 100644 index 00000000..3e6f5a1e --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/definition.cs @@ -0,0 +1,18 @@ +using System; +using Gum.DataTypes; +using Gum.Managers; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Content; +using MonoGameGum; +using MonoGameGum.Forms.Controls; +using MonoGameGum.GueDeriving; +using MonoGameLibrary; +using MonoGameLibrary.Graphics; + +namespace DungeonSlime.UI; + +public class GameSceneUI : ContainerRuntime +{ + +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/eventhandlers.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/eventhandlers.cs new file mode 100644 index 00000000..9e704b25 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/eventhandlers.cs @@ -0,0 +1,52 @@ +private void OnResumeButtonClicked(object sender, EventArgs args) +{ + // Button was clicked, play the ui sound effect for auditory feedback. + Core.Audio.PlaySoundEffect(_uiSoundEffect); + + // Since the resume button was clicked, we need to hide the pause panel. + HidePausePanel(); + + // Invoke the ResumeButtonClick event + if(ResumeButtonClick != null) + { + ResumeButtonClick(sender, args); + } +} + +private void OnRetryButtonClicked(object sender, EventArgs args) +{ + // Button was clicked, play the ui sound effect for auditory feedback. + Core.Audio.PlaySoundEffect(_uiSoundEffect); + + // Since the retry button was clicked, we need to hide the game over panel. + HideGameOverPanel(); + + // Invoke the RetryButtonClick event. + if(RetryButtonClick != null) + { + RetryButtonClick(sender, args); + } +} + +private void OnQuitButtonClicked(object sender, EventArgs args) +{ + // Button was clicked, play the ui sound effect for auditory feedback. + Core.Audio.PlaySoundEffect(_uiSoundEffect); + + // Both panels have a quit button, so hide both panels + HidePausePanel(); + HideGameOverPanel(); + + // Invoke the QuitButtonClick event. + if(QuitButtonClick != null) + { + QuitButtonClick(sender, args); + } +} + +private void OnElementGotFocus(object sender, EventArgs args) +{ + // A ui element that can receive focus has received focus, play the + // ui sound effect for auditory feedback. + Core.Audio.PlaySoundEffect(_uiSoundEffect); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/events.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/events.cs new file mode 100644 index 00000000..fde63334 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/events.cs @@ -0,0 +1,15 @@ +/// +/// Event invoked when the Resume button on the Pause panel is clicked. +/// +public event EventHandler ResumeButtonClick; + +/// +/// Event invoked when the Quit button on either the Pause panel or the +/// Game Over panel is clicked. +/// +public event EventHandler QuitButtonClick; + +/// +/// Event invoked when the Retry button on the Game Over panel is clicked. +/// +public event EventHandler RetryButtonClick; \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/fields.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/fields.cs new file mode 100644 index 00000000..6781fe07 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/fields.cs @@ -0,0 +1,22 @@ +// The string format to use when updating the text for the score display. +private static readonly string s_scoreFormat = "SCORE: {0:D6}"; + +// The sound effect to play for auditory feedback of the user interface. +private SoundEffect _uiSoundEffect; + +// The pause panel +private Panel _pausePanel; + +// The resume button on the pause panel. Field is used to track reference so +// focus can be set when the pause panel is shown. +private AnimatedButton _resumeButton; + +// The game over panel. +private Panel _gameOverPanel; + +// The retry button on the game over panel. Field is used to track reference +// so focus can be set when the game over panel is shown. +private AnimatedButton _retryButton; + +// The text runtime used to display the players score on the game screen. +private TextRuntime _scoreText; diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/publicmethods.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/publicmethods.cs new file mode 100644 index 00000000..f131d01f --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/gamesceneui/publicmethods.cs @@ -0,0 +1,69 @@ +/// +/// Updates the text on the score display. +/// +/// The score to display. +public void UpdateScoreText(int score) +{ + _scoreText.Text = string.Format(s_scoreFormat, score); +} + +/// +/// Tells the game scene ui to show the pause panel. +/// +public void ShowPausePanel() +{ + _pausePanel.IsVisible = true; + + // Give the resume button focus for keyboard/gamepad input. + _resumeButton.IsFocused = true; + + // Ensure the game over panel isn't visible. + _gameOverPanel.IsVisible = false; +} + +/// +/// Tells the game scene ui to hide the pause panel. +/// +public void HidePausePanel() +{ + _pausePanel.IsVisible = false; +} + +/// +/// Tells the game scene ui to show the game over panel. +/// +public void ShowGameOverPanel() +{ + _gameOverPanel.IsVisible = true; + + // Give the retry button focus for keyboard/gamepad input. + _retryButton.IsFocused =true; + + // Ensure the pause panel isn't visible. + _pausePanel.IsVisible = false; +} + +/// +/// Tells the game scene ui to hide the game over panel. +/// +public void HideGameOverPanel() +{ + _gameOverPanel.IsVisible = false; +} + +/// +/// Updates the game scene ui. +/// +/// A snapshot of the timing values for the current update cycle. +public void Update(GameTime gameTime) +{ + GumService.Default.Update(gameTime); +} + +/// +/// Draws the game scene ui. +/// +public void Draw() +{ + GumService.Default.Draw(); +} diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/fields.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/fields.cs new file mode 100644 index 00000000..73d9235e --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/fields.cs @@ -0,0 +1,5 @@ +// Buffer to queue inputs input by player during input polling. +private Queue _inputBuffer; + +// The maximum size of the buffer queue. +private const int MAX_BUFFER_SIZE = 2; \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/handleinput.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/handleinput.cs new file mode 100644 index 00000000..dfc27684 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/handleinput.cs @@ -0,0 +1,39 @@ +private void HandleInput() +{ + Vector2 potentialNextDirection = Vector2.Zero; + + if (GameController.MoveUp()) + { + potentialNextDirection = -Vector2.UnitY; + } + else if (GameController.MoveDown()) + { + potentialNextDirection = Vector2.UnitY; + } + else if (GameController.MoveLeft()) + { + potentialNextDirection = -Vector2.UnitX; + } + else if (GameController.MoveRight()) + { + potentialNextDirection = Vector2.UnitX; + } + + // If a new direction was input, consider adding it to the buffer + if (potentialNextDirection != Vector2.Zero && _inputBuffer.Count < MAX_BUFFER_SIZE) + { + // If the buffer is empty, validate against the current direction; + // otherwise, validate against the last buffered direction + Vector2 validateAgainst = _inputBuffer.Count > 0 ? + _inputBuffer.Last() : + _segments[0].Direction; + + // Only allow direction change if it is not reversing the current + // direction. This prevents th slime from backing into itself + float dot = Vector2.Dot(potentialNextDirection, validateAgainst); + if (dot >= 0) + { + _inputBuffer.Enqueue(potentialNextDirection); + } + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/initialize.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/initialize.cs new file mode 100644 index 00000000..b11543ba --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/initialize.cs @@ -0,0 +1,32 @@ +/// +/// Initializes the slime, can be used to reset it back to an initial state. +/// +/// The position the slime should start at. +/// The total number of pixels to move the head segment during each movement cycle. +public void Initialize(Vector2 startingPosition, float stride) +{ + // Initialize the segment collection. + _segments = new List(); + + // Set the stride + _stride = stride; + + // Create the initial head of the slime chain. + SlimeSegment head = new SlimeSegment(); + head.At = startingPosition; + head.To = startingPosition + new Vector2(_stride, 0); + head.Direction = Vector2.UnitX; + + // Add it to the segment collection. + _segments.Add(head); + + // Set the initial next direction as the same direction the head is + // moving. + _nextDirection = head.Direction; + + // Zero out the movement timer. + _movementTimer = TimeSpan.Zero; + + // initialize the input buffer. + _inputBuffer = new Queue(MAX_BUFFER_SIZE); +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/move.cs b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/move.cs new file mode 100644 index 00000000..d6b42516 --- /dev/null +++ b/articles/tutorials/building_2d_games/23_completing_the_game/snippets/slime/move.cs @@ -0,0 +1,48 @@ +private void Move() +{ + // Get the next direction from the input buffer if one is available + if (_inputBuffer.Count > 0) + { + _nextDirection = _inputBuffer.Dequeue(); + } + + // Capture the value of the head segment + SlimeSegment head = _segments[0]; + + // Update the direction the head is supposed to move in to the + // next direction cached. + head.Direction = _nextDirection; + + // Update the head's "at" position to be where it was moving "to" + head.At = head.To; + + // Update the head's "to" position to the next tile in the direction + // it is moving. + head.To = head.At + head.Direction * _stride; + + // Insert the new adjusted value for the head at the front of the + // segments and remove the tail segment. This effectively moves + // the entire chain forward without needing to loop through every + // segment and update its "at" and "to" positions. + _segments.Insert(0, head); + _segments.RemoveAt(_segments.Count - 1); + + // Iterate through all of the segments except the head and check + // if they are at the same position as the head. If they are, then + // the head is colliding with a body segment and a body collision + // has occurred. + for (int i = 1; i < _segments.Count; i++) + { + SlimeSegment segment = _segments[i]; + + if (head.At == segment.At) + { + if (BodyCollision != null) + { + BodyCollision.Invoke(this, EventArgs.Empty); + } + + return; + } + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/23_completing_the_game/videos/gameplay.webm b/articles/tutorials/building_2d_games/23_completing_the_game/videos/gameplay.webm new file mode 100644 index 00000000..09fb8113 Binary files /dev/null and b/articles/tutorials/building_2d_games/23_completing_the_game/videos/gameplay.webm differ diff --git a/articles/tutorials/building_2d_games/index.md b/articles/tutorials/building_2d_games/index.md index 950abec2..440717ac 100644 --- a/articles/tutorials/building_2d_games/index.md +++ b/articles/tutorials/building_2d_games/index.md @@ -19,6 +19,7 @@ This documentation will introduce game development concepts using the MonoGame f > [!CAUTION] > This is currently a work in progress and is not finished. + | Chapter | Summary | Source Files | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | [Chapter 01: What Is MonoGame](01_what_is_monogame/index.md) | Learn about the history of MonoGame and explore the features it provides to developers when creating games. | [01-What-Is-MonoGame](https://github.com/shyfox-studio/learn-monogame-2d/tree/main/src/01-What-Is-MonoGame/) | @@ -41,6 +42,8 @@ This documentation will introduce game development concepts using the MonoGame f | [Chapter 19: User Interface Fundamentals](19_user_interface_fundamentals/index.md) | Learn the core principles of game user interface design. | [19-User-Interface-Fundamentals](https://github.com/shyfox-studio/learn-monogame-2d/tree/main/src/19-User-Interface-Fundamentals/) | | [Chapter 20: Implementing UI with Gum](20_implementing_ui_with_gum/index.md) | Learn how to integrate and use the Gum UI framework to create functional menus, buttons, and sliders for your MonoGame projects. | [20-Implementing-UI-With-Gum](https://github.com/shyfox-studio/learn-monogame-2d/tree/main/src/20-Implementing-UI-With-Gum/) | | [Chapter 21: Customizing Gum UI](21_customizing_gum_ui/index.md) | Learn how to create custom UI components with animations and visual styling in Gum. | [21-Customizing-Gum-UI](https://github.com/shyfox-studio/learn-monogame-2d/tree/main/src/21-Customizing-Gum-UI/) | +| [Chapter 22: Snake Game Mechanics](22_snake_game_mechanics/index.md) | Learn how to implement classic snake-like game mechanics and organize game objects into reusable components. | [22-Snake-Game-Mechanics](https://github.com/shyfox-studio/learn-monogame-2d/tree/main/src/22-Snake-Game-Mechanics/) | +| [Chapter 23: Completing the Game](23_completing_the_game/index.md) | Finalize game mechanics by updating our current demo into a snake-like inspired game. | [23-Completing-The-Game](https://github.com/shyfox-studio/learn-monogame-2d/tree/main/src/23-Completing-The-Game/) | ## Conventions Used in This Documentation