diff --git a/articles/getting_started/5_adding_basic_code.md b/articles/getting_started/5_adding_basic_code.md index 918385ba..b6fc27ed 100644 --- a/articles/getting_started/5_adding_basic_code.md +++ b/articles/getting_started/5_adding_basic_code.md @@ -319,6 +319,6 @@ We recommend browsing through the [Getting to know MonoGame](../getting_to_know/ ## Further Reading -Check out the [Tutorials section](../tutorials.md) for many more helpful guides and tutorials on building games with MonoGame. We have an expansive library of helpful content, all provided by other MonoGame developers in the community. +Check out the [Tutorials section](../tutorials/index.md) for many more helpful guides and tutorials on building games with MonoGame. We have an expansive library of helpful content, all provided by other MonoGame developers in the community. Additionally, be sure to check out the official [MonoGame Samples](../samples.md) page for fully built sample projects built with MonoGame and targeting our most common platforms. diff --git a/articles/help_and_support.md b/articles/help_and_support.md index b5a41498..d6d6ea25 100644 --- a/articles/help_and_support.md +++ b/articles/help_and_support.md @@ -5,7 +5,7 @@ description: Where to get help and support when using MonoGame. # Help and Support -There is a wealth of [community created content, blogs and tutorials](tutorials.md) available. +There is a wealth of [community created content, blogs and tutorials](tutorials/index.md) available. If you want to find an answer to a more specific problem, you can ask it on our [GitHub Discussions](https://github.com/MonoGame/MonoGame/discussions) page. diff --git a/articles/toc.yml b/articles/toc.yml index 2e3c54fd..8eea0b28 100644 --- a/articles/toc.yml +++ b/articles/toc.yml @@ -1,5 +1,5 @@ - name: Introduction - href: + href: - name: Roadmap href: /roadmap/ - name: What's New @@ -7,107 +7,135 @@ - name: Getting Started href: getting_started/index.md items: - - name: Introduction - href: getting_started/ - - name: Supported platforms - href: getting_started/platforms.md - - name: 1. Setting up your OS for development - items: - - name: Windows - href: getting_started/1_setting_up_your_os_for_development_windows.md - - name: macOS - href: getting_started/1_setting_up_your_os_for_development_macos.md - - name: Ubuntu 20.04 - href: getting_started/1_setting_up_your_os_for_development_ubuntu.md - - name: 2. Choosing your IDE - items: - - name: Visual Studio for Windows - href: getting_started/2_choosing_your_ide_visual_studio.md - - name: Visual Studio Code - href: getting_started/2_choosing_your_ide_vscode.md - - name: Rider - href: getting_started/2_choosing_your_ide_rider.md - - name: 3. Understanding the Code - href: getting_started/3_understanding_the_code.md - - name: 4. Adding Content - href: getting_started/4_adding_content.md - - name: 5. Adding Basic Code - href: getting_started/5_adding_basic_code.md - - name: Packaging - href: getting_started/packaging_games.md - - name: Preparing for consoles - href: getting_started/preparing_for_consoles.md - - name: Using Development Nuget Packages - href: getting_started/using_development_nuget_packages.md - - name: Tools - items: - name: Introduction - href: getting_started/tools/ - - name: MGCB - href: getting_started/tools/mgcb.md - - name: MGCB Editor - href: getting_started/tools/mgcb_editor.md - - name: MGFXC - href: getting_started/tools/mgfxc.md - - name: Content Pipeline - items: - - name: Introduction - href: getting_started/content_pipeline/index.md - - name: Why use the Content Pipeline - href: getting_started/content_pipeline/why_content_pipeline.md - - name: Using MGCB Editor - href: getting_started/content_pipeline/using_mgcb_editor.md - - name: Custom Effects - href: getting_started/content_pipeline/custom_effects.md - - name: TrueType fonts - href: getting_started/content_pipeline/adding_ttf_fonts.md - - name: Localization - href: getting_started/content_pipeline/localization.md + href: getting_started/ + - name: Supported platforms + href: getting_started/platforms.md + - name: 1. Setting up your OS for development + items: + - name: Windows + href: getting_started/1_setting_up_your_os_for_development_windows.md + - name: macOS + href: getting_started/1_setting_up_your_os_for_development_macos.md + - name: Ubuntu 20.04 + href: getting_started/1_setting_up_your_os_for_development_ubuntu.md + - name: 2. Choosing your IDE + items: + - name: Visual Studio for Windows + href: getting_started/2_choosing_your_ide_visual_studio.md + - name: Visual Studio Code + href: getting_started/2_choosing_your_ide_vscode.md + - name: Rider + href: getting_started/2_choosing_your_ide_rider.md + - name: 3. Understanding the Code + href: getting_started/3_understanding_the_code.md + - name: 4. Adding Content + href: getting_started/4_adding_content.md + - name: 5. Adding Basic Code + href: getting_started/5_adding_basic_code.md + - name: Packaging + href: getting_started/packaging_games.md + - name: Preparing for consoles + href: getting_started/preparing_for_consoles.md + - name: Using Development Nuget Packages + href: getting_started/using_development_nuget_packages.md + - name: Tools + items: + - name: Introduction + href: getting_started/tools/ + - name: MGCB + href: getting_started/tools/mgcb.md + - name: MGCB Editor + href: getting_started/tools/mgcb_editor.md + - name: MGFXC + href: getting_started/tools/mgfxc.md + - name: Content Pipeline + items: + - name: Introduction + href: getting_started/content_pipeline/index.md + - name: Why use the Content Pipeline + href: getting_started/content_pipeline/why_content_pipeline.md + - name: Using MGCB Editor + href: getting_started/content_pipeline/using_mgcb_editor.md + - name: Custom Effects + href: getting_started/content_pipeline/custom_effects.md + - name: TrueType fonts + href: getting_started/content_pipeline/adding_ttf_fonts.md + - name: Localization + href: getting_started/content_pipeline/localization.md - name: Getting to know MonoGame href: getting_to_know/ items: - - name: What is - href: getting_to_know/whatis/ - items: - - name: Audio - href: getting_to_know/whatis/audio/ - - name: Content Pipeline - href: getting_to_know/whatis/content_pipeline/ - - name: Graphics - href: getting_to_know/whatis/graphics/ - - name: Input - href: getting_to_know/whatis/input/ - - name: The Game Loop - href: getting_to_know/whatis/game_loop/ - - name: Vector / Matrix / Quaternions - href: getting_to_know/whatis/vector_matrix_quat/ - - name: MonoGame Class Library - href: getting_to_know/whatis/monogame_class_library/ - - name: How to - href: getting_to_know/howto/ - items: - - name: Audio - href: getting_to_know/howto/audio/ - - name: Content Pipeline - href: getting_to_know/howto/content_pipeline/ - - name: Graphics - href: getting_to_know/howto/graphics/ - - name: Input - href: getting_to_know/howto/input/ + - name: What is + href: getting_to_know/whatis/ + items: + - name: Audio + href: getting_to_know/whatis/audio/ + - name: Content Pipeline + href: getting_to_know/whatis/content_pipeline/ + - name: Graphics + href: getting_to_know/whatis/graphics/ + - name: Input + href: getting_to_know/whatis/input/ + - name: The Game Loop + href: getting_to_know/whatis/game_loop/ + - name: Vector / Matrix / Quaternions + href: getting_to_know/whatis/vector_matrix_quat/ + - name: MonoGame Class Library + href: getting_to_know/whatis/monogame_class_library/ + - name: How to + href: getting_to_know/howto/ + items: + - name: Audio + href: getting_to_know/howto/audio/ + - name: Content Pipeline + href: getting_to_know/howto/content_pipeline/ + - name: Graphics + href: getting_to_know/howto/graphics/ + - name: Input + href: getting_to_know/howto/input/ - name: Migration items: - - name: Migrating from XNA - href: migration/migrate_xna.md - - name: Migrating from 3.7 - href: migration/migrate_37.md - - name: Migrating from 3.8.0 - href: migration/migrate_38.md - - name: Updating Versions - href: migration/updating_versions.md + - name: Migrating from XNA + href: migration/migrate_xna.md + - name: Migrating from 3.7 + href: migration/migrate_37.md + - name: Migrating from 3.8.0 + href: migration/migrate_38.md + - name: Updating Versions + href: migration/updating_versions.md - name: Samples and Demos href: samples.md -- name: Community Tutorials - href: tutorials.md +- name: Tutorials + href: tutorials/ + items: + - name: Building 2D Games + href: tutorials/building_2d_games/ + items: + - name: "01: What Is MonoGame?" + href: tutorials/building_2d_games/01_what_is_monogame/ + - name: "02: Getting Started" + href: tutorials/building_2d_games/02_getting_started/ + - name: "03: The Game1 File" + href: tutorials/building_2d_games/03_the_game1_file/ + - name: "04: Content Pipeline" + href: tutorials/building_2d_games/04_content_pipeline/ + - name: "05: Working with Textures" + href: tutorials/building_2d_games/05_working_with_textures/ + - name: "06: Optimizing Texture Rendering" + href: tutorials/building_2d_games/06_optimizing_texture_rendering/ + - name: "07: The Sprite Class" + href: tutorials/building_2d_games/07_the_sprite_class/ + - name: "08: The AnimatedSprite Class" + href: tutorials/building_2d_games/08_the_animatedsprite_class/ + - name: "09: Handling Input" + href: tutorials/building_2d_games/09_handling_input/ + - name: "10: Input Management" + href: tutorials/building_2d_games/10_input_management/ + - name: "11: Collision Detection" + href: tutorials/building_2d_games/11_collision_detection/ + - name: "12: Sound Effects and Music" + href: tutorials/building_2d_games/12_soundeffects_and_music/ - name: Console Access href: console_access.md - name: Help and Support diff --git a/articles/tutorials/building_2d_games/01_what_is_monogame/images/celeste.png b/articles/tutorials/building_2d_games/01_what_is_monogame/images/celeste.png new file mode 100644 index 00000000..c9bd51d8 Binary files /dev/null and b/articles/tutorials/building_2d_games/01_what_is_monogame/images/celeste.png differ diff --git a/articles/tutorials/building_2d_games/01_what_is_monogame/images/sor4.jpg b/articles/tutorials/building_2d_games/01_what_is_monogame/images/sor4.jpg new file mode 100644 index 00000000..5f6a4710 Binary files /dev/null and b/articles/tutorials/building_2d_games/01_what_is_monogame/images/sor4.jpg differ diff --git a/articles/tutorials/building_2d_games/01_what_is_monogame/images/stardew-valley.png b/articles/tutorials/building_2d_games/01_what_is_monogame/images/stardew-valley.png new file mode 100644 index 00000000..e8be681e Binary files /dev/null and b/articles/tutorials/building_2d_games/01_what_is_monogame/images/stardew-valley.png differ diff --git a/articles/tutorials/building_2d_games/01_what_is_monogame/index.md b/articles/tutorials/building_2d_games/01_what_is_monogame/index.md new file mode 100644 index 00000000..de33189b --- /dev/null +++ b/articles/tutorials/building_2d_games/01_what_is_monogame/index.md @@ -0,0 +1,91 @@ +--- +title: "Chapter 01: What is MonoGame" +description: Learn about the history of MonoGame and explore the features it provides developers when creating games. +--- + +## A Brief History + +In 2006, Microsoft released a game development framework named *XNA Game Studio* to facilitate game development for Windows PC and the Xbox 360 console. It revolutionized game development for indie creators by bringing a simplified approach to building games and offering a set of tools that lowered the entry barrier for aspiring game developers. Out of XNA Game Studio came critically acclaimed titles such as [Bastion](https://www.supergiantgames.com/games/bastion/) and [Terraria](https://terraria.org/). In 2008, XNA was expanded to support development for both the Zune and Windows Phone. + +> [!NOTE] +> +> Fun fact, provided by community member stromkos, The release of XNA 3.0 in 2008, which added the support for Windows Phone, is also the release that specified the default window resolution of 800x480 for new projects as this was the preferred resolution on Windows Phone. [It is still the default resolution used in MonoGame projects today](https://github.com/MonoGame/MonoGame/blob/8b35cf50783777507cd6b21828ed0109b3b07b50/MonoGame.Framework/GraphicsDeviceManager.cs#L44). + +As XNA become more popular, the need for cross-platform development started to grow. In 2009, [José Antonio Leal de Farias](https://github.com/jalf) introduced *XNA Touch*, an open-source project that aimed to make games with XNA playable on iOS devices. This marked the beginning of what would later become MonoGame. [Dominique Louis](https://github.com/CartBlanche) came on board in 2009 and soon took over as full-time project lead, driving its initial development and expansion. The project attracted other developers such as [Tom Spilman](https://github.com/tomspilman), who were interested in expanding the scope of the project, as well as its reach. + +The official first release of MonoGame occurred in 2011, as an open source version of XNA. While it still had the same familiar API as XNA, the cross-platform support was expanded to include Windows, macOS, Linux, iOS, Android, Xbox, and PlayStation. Despite Microsoft discontinuing XNA in 2013, MonoGame continued to grow and develop. Maintenance of the project was given to [Steve Williams](https://github.com/KonajuGames) and [Tom Spilman](https://github.com/tomspilman) in 2014. In order to direct its future development and undertaking, the [MonoGame Foundation](https://monogame.net/about/) was formed on September 29th, 2023. + +Today, it is a mature, cross-platform framework, that is built with the spirit of preserving XNA but adopting modern game development practices. Some popular titles created using MonoGame include [Celeste](https://store.steampowered.com/app/504230/Celeste/), [Stardew Valley](https://store.steampowered.com/app/413150/Stardew\_Valley/), and [Streets of Rage 4](https://store.steampowered.com/app/985890/Streets\_of\_Rage\_4/). + +| ![Figure 1-1: Celeste](./images/celeste.png) | ![Figure 1-2: Stardew Valley](./images/stardew-valley.png) | +| :---: | :---: | +| **Figure 1-1 Celeste.** | **Figure 1-2: Stardew Valley** | +| ![Figure 1-3: Streets of Rage 4](./images/sor4.jpg) | | +| **Figure 1-3: Streets of Rage 4** | | + +## Features + +MonoGame, following in the footsteps of XNA, is a "bring your own tools" framework. It provides developers the basic blocks to design the game, engines, and/or tools. As a code-first approach to game development, MonoGame does not include any pre-built editors or interfaces; instead, it gives developers the freedom to create their own working environment. + +### API + +At its core, MonoGame offers a set of libraries and APIs to handle common game development tasks. These include: + +1. **Graphics Rendering**: 2D and 3D rendering are supported through the graphics API offered by MonoGame. This API provides sprite batching for 2D graphics, a flexible 3D pipeline, and shaders for custom visuals and effects. +2. **Input Handling**: Input from keyboard, mouse, gamepads, and touchscreens are supported, allowing for development of games for any platform and different styles of play. +3. **Audio**: A comprehensive audio system that can be used to create sound effects as well as play, music with included support for many audio formats. +4. **Content Pipeline**: An out-of-the-box workflow for importing and processing game assets such as textures, models, and audio, compiling them to a format that is optimal for the game's target platform. +5. **Math Library**: A math library specifically optimized for game development, providing essential mathematical functions and operations. + +### Cross Platform + +One of the main advantages of MonoGame is its cross-platform support. Games built with MonoGame are compatible with a variety of platforms, including: + +* **Desktop**: Windows, macOS, and Linux. +* **Mobile**: iOS and Android. +* **Consoles**: Xbox, PlayStation, and Nintendo Switch [(with appropriate license)](https://docs.monogame.net/articles/console\_access.html). + +By providing cross-platform support, developers can target multiple platforms from a single code base, significantly reducing development time and resources needed for porting. + +### Programming Language Support + +MonoGame is designed and built in C#. It is the official programming language supported in documentation, samples, and community discussion. However, MonoGame is not exclusively tied to C#. As a .NET library, MonoGame can be used with any .NET-compatible language including Visual Basic and F#. + +> [!CAUTION] +> While the alternative .NET languages can be used, community support may be limited outside the scope of C#. + +Regardless of which .NET language used, developers should have a foundational understanding of the language and programming concepts such as: + +* Object-oriented programming. +* Data types and structures. +* Control flow and loops. +* Error handling and debugging. + +## See Also + +* [About MonoGame | MonoGame](https://monogame.net/about) + +## Test Your Knowledge + +1. Name one of the advantages of using the MonoGame framework to develop games. + +
+ Question 1 Answer + + > Any of the following are advantages of using the MonoGame + > 1. It provides cross-platform support, allowing developers to target multiple platforms from a single code base. + > + > 2. It offers a set of libraries and APIs common for game development tasks, such as graphics rendering, input handling, audio, and content management + > + > 3. It is a "bring your own tools" framework, giving developers flexibility in their working environment. + +

+ +2. What programming languages can be used when creating a game with MonoGame? + +
+ Question 2 Answer + + > The primary language used is C#, which is the same language that the MonoGame framework is developed in. However, any .NET language can be used, such as F# or Visual Basic. + +

diff --git a/articles/tutorials/building_2d_games/02_getting_started/images/cornflower-blue.png b/articles/tutorials/building_2d_games/02_getting_started/images/cornflower-blue.png new file mode 100644 index 00000000..aadceb01 Binary files /dev/null and b/articles/tutorials/building_2d_games/02_getting_started/images/cornflower-blue.png differ diff --git a/articles/tutorials/building_2d_games/02_getting_started/images/vscode.png b/articles/tutorials/building_2d_games/02_getting_started/images/vscode.png new file mode 100644 index 00000000..ebdcf23e Binary files /dev/null and b/articles/tutorials/building_2d_games/02_getting_started/images/vscode.png differ diff --git a/articles/tutorials/building_2d_games/02_getting_started/index.md b/articles/tutorials/building_2d_games/02_getting_started/index.md new file mode 100644 index 00000000..a4a4d843 --- /dev/null +++ b/articles/tutorials/building_2d_games/02_getting_started/index.md @@ -0,0 +1,225 @@ +--- +title: "Chapter 02: Getting Started" +description: Setup your development environment for dotnet development and MonoGame using Visual Studio Code as your IDE. +--- + +Unlike game engines, MonoGame is a *framework*. This means it does not come as a standalone program that you download an install with a graphical user interface used to create games. Instead, MonoGame integrates into the standard .NET development workflow, offering a code-first approach to game development. This approach offers several advantages + +* **Flexibility**: Developers are not locked into using a specific editor or interface, allowing them to use their preferred development tools. +* **Integration**: As a .NET library itself, MonoGame can easily integrate with other .NET libraries and tools. +* **Cross-platform Development**: Since C# is cross-platform, and MonoGame is cross-platform, developers can develop MonoGame projects on Windows, macOS, or Linux, with only slight differences in the setup process for each operating system. +* **Version Control Friendly**: The code-first approach makes it easier to use version control systems like Git for you game projects. + +While the environment setup process is similar to the standard setup process for C# development, there are some MonoGame specific steps. These can vary slightly depending on your operating system and the *Integrated Development Environment* (IDE). + +## Installing the .NET SDK + +The first thing we need to do is install the .NET *Software Development Kit* (SDK). At the time of this writing, MonoGame targets the .NET 8.0 SDK. To install it, follow the instructions based on your operating system below + +### [Windows](#tab/windows) +1. Open a web browser and navigate to https://dotnet.microsoft.com/en-us/download. +2. Click the *Download .NET SDK x64* button to start the download of the .NET SDK Installer. +3. Once the download finishes, run the installer + +### [macOS](#tab/macos) +1. Open a web browser and navigate to https://dotnet.microsoft.com/en-us/download. +2. Click the *Download .NET SDK x64 (Intel)* button start the download of the .NET SDK Installer +3. Once the download finishes, run the installer. + +> [!NOTE] +> For the time being, MonoGame requires that you install the **Intel** version even if you are using an Apple Silicon (M1/M2) Mac. For Apple Silicon Macs, it also requires that [Rosetta](https://support.apple.com/en-us/HT211861) is enabled. + +### [Linux](#tab/linux) +1. Open a new *Terminal* window +2. Enter the following command to install the .NET SDK + +```sh +sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0 +``` + +--- + +## Install Additional Workloads (Optional) + +After installing the .NET SDK, if you intend to target mobile devices such as Android or iOS, you will also need to install the corresponding mobile workloads. To do this, open a *Command Prompt* or *Terminal* window and enter the following commands + +```sh +dotnet workload install ios +dotnet workload install android +``` + +## Install MonoGame Project Templates + +MonoGame provides project templates that can be installed to create new projects that are pre-configured to target the current version of MonoGame as a base to begin creating games. As of this writing, the current version of MonoGame targeted is 3.8.2.1105. To install the MonoGame templates, open a *Command Prompt* or *Terminal* window and enter the following command + +```sh +dotnet new install MonoGame.Templates.CSharp +``` + +## Installing Visual Studio Code + +*Visual Studio Code* (VSCode) is a free, light weight editor. Depending on the programming language you are using, it's just a matter of installing the correct extension to support that language. VSCode is also cross-platform, meaning you can use it for development on Windows, macOS, and Linux. To ensure that all readers can follow this tutorial regardless of operating system, we'll be using VSCode as our IDE. + +To install VSCode, follow the instructions for your operating system below: + +### [Windows](#tab/windows) + +1. Open a browser and navigate to https://code.visualstudio.com/. +2. Click the *Download for Windows* button to start the download of the installer. +3. Once the download finishes, run the installer. + +### [macOS](#tab/macos) + +1. Open a web browser and navigate to https://code.visualstudio.com/. +2. Click the *Download for macOS* button to start the download of the *.zip* archive. +3. Once the download finishes, double click the *.zip* archive to extract the *Visual Studio Code.app* application package +4. Drag-and-drop the *Visual Studio Code.app* application package into your *Application* directory to make it available in the macOS *LaunchPad*. + +### [Linux](#tab/linux) + +1. Open a web browser and navigate to https://code.visualstudio.com/. +2. Click the *.deb* download button to download the package for Debian based Linux distributions, or the *.rpm* download button for Red Hat based Linux distributions. +3. Once the download finishes, open the package downloaded to install. + +--- + +## Install the C# Dev Kit Extension + +For C# development using VSCode, it's recommended to use the official *C# Dev Kit* extension provided by Microsoft. Installing this extension will add additional features to VSCode such as a project system and *Solution Explorer* for C# projects. It also provides code editing features such as syntax highlighting, code completion, code navigation, refactoring, NuGet package management, and debugging tools. + +To install the C# Dev Kit extension, perform the following: + +1. Launch the Visual Studio Code application. +2. Open the *Extensions Panel* by clicking the icon in the *Activity Bar* on the left or choosing *View > Extensions* from the top menu. +3. Enter `C#` in the *Search Box* +4. Click install for the *C# Dev Kit* extension. + +> [!NOTE] +> When you search `C#` in the *Extension Panel* you may notice there is the C# Dev Kit extension and a base standard C# extension. When installing the C# Dev Kit extension, the base extension will also be installed as a requirement. + +## Installing the "MonoGame for VSCode" Extension + +Throughout this tutorial, we'll be using the MonoGame Content Builder (MGCB) Editor to add content to the game. MonoGame offers an official extension for Visual Studio 2022 that allows you to double-click the *Content.mgcb* file to automatically open it in the MGCB Editor. While there is no official tool for VSCode, there is a an extension developed by community member r88 to provide similar functionality and is regularly used by the MonoGame developers themselves. We'll be using that extension throughout this tutorial. + +To install it, with VSCode open: + +1. Open the *Extensions Panel* by clicking the icon in the *Activity Bar* on the left or choosing *View > Extensions* from the top menu. +2. Enter `MonoGame for VSCode` as in the *Search Box* +3. Click install for the *MonoGame for VSCode* extension by r88. + +## Setup WINE for Effect Compilation (macOS and Linux Only) + +*Effect* (shader) compilation requires access to DirectX. This means it will not work natively on macOS and Linux systems, but it can be used through [WINE](https://www.winehq.org/). MonoGame provides a setup script that can be executed to setup the WINE environment. Below you can find the steps based on your operating system. To do this, follow the instructions for your operating system below: + +### [Windows](#tab/windows) + +> [!NOTE] +> Setting up WINE for effect compilation is not required for Windows + +### [macOS](#tab/macos) + +Open a new *Terminal* window and enter execute the following commands: + +```sh +brew install p7zip +brew install --cask wine-stable +wget -qO- https://monogame.net/downloads/net8_mgfxc_wine_setup.sh | bash +``` + +> [!NOTE] +> After performing these steps, a new directory called *.winemonogame* will be created in your home directory. If you ever wish to undo the setup this script performed, you can just simply delete this directory. + +### [Linux](#tab/linux) + +Open a new *Terminal* window and execute the following commands: + +```sh +sudo apt-get update && sudo apt-get install -y curl p7zip-full wine64 +wget -qO- https://monogame.net/downloads/net8_mgfxc_wine_setup.sh | bash +``` + +> [!NOTE] +> After performing these steps, a new directory called *.winemonogame* will be created in your home directory. If you ever wish to undo the setup this script performed, you can just simply delete this directory. + +--- + +## Creating Your First MonoGame Application + +Now that you have your development environment setup, it's time to create your first MonoGame application. + +1. Launch the VSCode application +2. Open the *Command Palette* by clicking *View > Command Palette* or by using the keyboard shortcut `CTRL+SHIFT+P`. +3. Type `.NET New Project` in the *Command Palette* and choose the *.NET New Project* command +4. Next you'll be shown a list of the available .NET project templates. Enter `MonoGame` into the prompt to filter the project templates to only show the MonoGame ones, then choose the *MonoGame Cross-Platform Desktop Application* project template. +5. After choosing the template, a dialog window will appear asking you to choose a location to save the project. +6. Next you'll be prompted to enter a name for the project. Enter the name `MonoGameSnake`. +7. Finally, select the *Create Project* prompt. + +After selecting *Create Project*, a new C# project will be created based on the MonoGame template we choose and opened automatically in VSCode. + +| ![Figure 2-1: A new MonoGame project after being created in Visual Studio Code](./images/vscode.png) | +| :---: | +| **Figure 2-1: A new MonoGame project after being created in Visual Studio Code** | + +Now that we have the project created, press the `F5` key on your keyboard, or choose *Run > Start Debugging* from the top menu. If prompted for a configuration, choose *C#*. The project will compile and run, displaying a screen similar to the following + +| ![Figure 2-2: The default MonoGame cornflower blue game window](./images/cornflower-blue.png) | +| :---: | +| **Figure 2-2: The default MonoGame cornflower blue game window** | + +Be amazed, the default MonoGame Cornflower Blue game window. You have just created your very first MonoGame application. While there isn't much happening here visually, there is a log going on behind the scenes that the MonoGame framework is handling for you. When you ran the application, the following occurred: + +1. The application started +2. The game window was created and graphics were initialized +3. A loop is entered which performs the following over and over until the game is told to exit: + 1. The game is updated + 2. The game is rendered to the window + +You can exit the game at any time by pressing the `Esc` key on your keyboard. + +> [!NOTE] +> Above, I mentioned that a loop is entered. This is commonly referred to as the *game loop*, which we'll discuss in more detail in the next chapter. The reason the application enters this loop is because game applications work differently than a traditional desktop application like your web browser. +> +> Desktop application are event based, meaning once loaded, the do not do much at all while waiting for input from the user, and then it response to that input event and redraws the window if needed based on the interaction. +> +> In games, things are always happening such as objects moving around like the player or particles. The handle this, games implement a loop structure that runs continuously, first calling a method to update the game logic, and then a draw method to render the current frame, until it has been told to exit. + +## Conclusion + +Let's review what you accomplished in this chapter: + +* You setup your operating system to develop .NET applications by installing the .NET SDK +* You install the MonoGame project templates. +* You installed VSCode and the necessary extension to develop C# applications with VSCode +* You created and ran your first MonoGame project. + +Now that your development environment is setup and ready to go, you can dive in and start building your first game. In the next chapter, we'll cover the contents of the *Game1.cs* file that was included in the MonoGame project you just created. + +## Test Your Knowledge + +1. What version of the .NET SDK is currently targeted by MonoGame applications? + +
+ Question 1 Answer + + > .NET 8.0 + +

+ +2. What is the current version of MonoGame? + +
+ Question 2 Answer + + > 3.8.2.1105 + +

+ +3. What is the color of the game window when you run a MonoGame project for the first time? + +
+ Question 3 Answer + + > Cornflower Blue + +

\ No newline at end of file diff --git a/articles/tutorials/building_2d_games/03_the_game1_file/images/monogame-lifecycle.png b/articles/tutorials/building_2d_games/03_the_game1_file/images/monogame-lifecycle.png new file mode 100644 index 00000000..0e748306 Binary files /dev/null and b/articles/tutorials/building_2d_games/03_the_game1_file/images/monogame-lifecycle.png differ diff --git a/articles/tutorials/building_2d_games/03_the_game1_file/images/monogame-lifecycle.svg b/articles/tutorials/building_2d_games/03_the_game1_file/images/monogame-lifecycle.svg new file mode 100644 index 00000000..ab108733 --- /dev/null +++ b/articles/tutorials/building_2d_games/03_the_game1_file/images/monogame-lifecycle.svg @@ -0,0 +1,465 @@ + + + +Game LoopYesLoadContent()Initialize()UpdateExit?DrawExit()No diff --git a/articles/tutorials/building_2d_games/03_the_game1_file/images/solitaire.webp b/articles/tutorials/building_2d_games/03_the_game1_file/images/solitaire.webp new file mode 100644 index 00000000..3d32c291 Binary files /dev/null and b/articles/tutorials/building_2d_games/03_the_game1_file/images/solitaire.webp differ diff --git a/articles/tutorials/building_2d_games/03_the_game1_file/index.md b/articles/tutorials/building_2d_games/03_the_game1_file/index.md new file mode 100644 index 00000000..58e3c1d1 --- /dev/null +++ b/articles/tutorials/building_2d_games/03_the_game1_file/index.md @@ -0,0 +1,215 @@ +--- +title: "Chapter 03: The Game1 File" +description: Explore the contents of the Game1 file generated when creating a new MonoGame project. +--- + +After you created a new MonoGame project using the *MonoGame Cross-Platform Desktop Application* template in [Chapter 02](../02_getting_started/index.md#creating-your-first-monogame-application), you will notice the generated files and project structure that serve as a starting point for your game application. While MonoGame offers different templates based on target platform, all projects will contain the *Game1.cs* file. + +> [!TIP] +> For an in-depth look at all files created in a MonoGame project when using the MonoGame templates, refer to [Appendix 02: MonoGame Project Overview](#). + +## Exploring the Game1 Class + +At the core of a MonoGame project is the [**Game**](xref:Microsoft.Xna.Framework.Game) class. This class handles the initialization of graphics services, initialization of the game, loading content, updating, and rendering the game. When you create a new Monogame project, this [**Game**](xref:Microsoft.Xna.Framework.Game) class is implemented as the `Game1` class that you can customize as needed for your specific game. + +> [!TIP] +> While the default template names the class `Game1`, you're free to rename it to something more appropriate for your project. However, for consistency, the documentation will continue to refer to it as `Game1`. + +Locate the *Game1.cs* file that was generated when you created the MonoGame project and open it. The default content will be: + +```cs +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + base.Draw(gameTime); + } +} + +``` + +This class provides the following structure: +1. **Graphics and Rendering**: The class declares two core graphics components; the [**GraphicsDeviceManager**](xref:Microsoft.Xna.Framework.GraphicsDeviceManager) for interacting with the Graphics Processing Unit (GPU) and the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) for 2D rendering. +2. **Initialization**: The constructor and [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method handle the game's setup sequence. +3. **Content Loading**: The [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method manages game asset loading during startup. +4. **Game Loop**: The *game loop* consists of the [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) method for game logic and the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method for rendering, running continuously until the game is told to exit. + +Figure 3-1 below shows the lifecycle of a MonoGame game including the [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) and [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) methods that make up the *game loop*. + +| ![Figure 3-1: Lifecycle of a MonoGame game](./images/monogame-lifecycle.png) | +| :---: | +| **Figure 3-1: Lifecycle of a MonoGame game** | + +## Graphics and Rendering +The graphics pipeline in monogame starts with two components: the [**GraphicsDeviceManager**](xref:Microsoft.Xna.Framework.GraphicsDeviceManager) and [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch). + +```cs +private GraphicsDeviceManager _graphics; +private SpriteBatch _spriteBatch; +``` + +The [**GraphicsDeviceManager**](xref:Microsoft.Xna.Framework.GraphicsDeviceManager) initializes and the connection to the graphics hardware. It handles tasks such as setting the screen resolution, toggling between fullscreen and windowed mode, and managing the [**GraphicsDevice**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice), which is the interface between your game and the Graphics Processing Unit (GPU) the game is running on. The [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) optimizes 2D rendering by batching similar draw calls together, improving draw performance when rendering multiple sprites. + +## Initialization + +MonoGame's initialization process for your game follows a specific sequence. The constructor runs first, which handles basic setup like creating the [**GraphicsDeviceManager**](xref:Microsoft.Xna.Framework.GraphicsDeviceManager), setting the content directory, and the visibility of the mouse. + +```cs +public Game1() +{ + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; +} +``` + +After that, the [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method executes, providing a dedicated place for additional configuration and initializations. + +```cs +protected override void Initialize() +{ + base.Initialize(); +} +``` + +This separation allows you to perform setup tasks in a logical order; core systems in the constructor and game-specific initializations in the [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method. The call to `base.Initialize()` should never be removed, as this is where the graphics device is initialized for the target platform. + +> [!TIP] +> You may be wondering why there is an [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method instead of performing all initializations in the constructor. The [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method is a `virtual` method that is overridden, and [it is advised to not call overridable methods from within a constructor](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2214), as this can lead to unexpected states in object constructor when called. Additionally, when the constructor is called, the base constructor will instantiate properties and services based on the target platform that may be needed first before performing initializations for the game itself. + +## Content Loading +The [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method serves as the place for asset management. Here you can load textures, sound effects, music, and other game assets. We will cover loading assets in the coming chapters as we discuss each asset type that can be loaded. In a new project, the only task it performs is initializing a new instance of the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch). + +```cs +protected override void LoadContent() +{ + _spriteBatch = new SpriteBatch(GraphicsDevice); +} +``` + +This method is only call once during the startup of the game, but *when* it is called can be a little confusing at first. In the [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method shown above, when the `base.Initialize` call is executed, the final task it performs is calling the [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method. This means any initializations you need to perform that have a dependency on assets being loaded should be done *after* the `base.Initialize` call and not *before* it. + +## The Game Loop + +MonoGame implements a *game loop* by calling [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) and [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) over and over until the game is told to exit. Recall at the end of [Chapter 02](../02_getting_started/index.md#creating-your-first-monogame-application) when you ran the project for the first time, I mentioned that there is a lot going on behind the scenes? This game loop is what I was referring to. + +MonoGame is executing the [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) method and then the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method 60 times per second. + +```cs +protected override void Update(GameTime gameTime) +{ + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); +} + +protected override void Draw(GameTime gameTime) +{ + GraphicsDevice.Clear(Color.CornflowerBlue); + + base.Draw(gameTime); +} +``` + +The [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) method at the moment is not doing much, only checking for input from a controller or keyboard to determine if the game should exit. However, the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method is doing more than what it appears to at first glance. + +The first line is executing the [**Clear**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color)) method of the [**GraphicsDevice**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice) property using the color [**CornflowerBlue**](xref:Microsoft.Xna.Framework.Color.CornflowerBlue). Recall that the [**GraphicsDevice**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice) object is your direct interface between the game and what is rendered to the screen. Every time the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method is called, this line of code of erasing the contents of the game window and refilling it with the color specified. Without clearing the contents of the screen first, every draw call would draw the new frame render over top of the previous render, and you'd end up with something like the old solitaire win screen + +| ![Figure 3-2: Windows XP Solitaire Win Screen](./images/solitaire.webp) | +| :---: | +| **Figure 3-2: Windows XP Solitaire Win Screen** | + +While this can make for a neat effect, it is not something you want all the time. So, the screen is cleared and refilled with a solid color. You can test this yourself by modifying the code to use a different color, such as [**Color.MonoGameOrange**](xref:Microsoft.Xna.Framework.Color.MonoGameOrange), then running the game. (yes, there is a MonoGame Orange color). + +Each time the game loops completes and the game is drawn to the screen, we call this a *frame*. So if MonoGame is running the game loop at 60 frames per second, that means it is performing and update and a render of each frame in 16ms. Notice that both the [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) and the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) methods both receive a parameter of the type [**GameTime**](xref:Microsoft.Xna.Framework.GameTime). The [**GameTime**](xref:Microsoft.Xna.Framework.GameTime) parameter provides a snapshot of the timing values for the game, including the amount of time that it took for the previous frame to execute. This is commonly referred to as the *delta time*. + +*Delta time* allows you to track time accurately for things such as animations and events based on *game time* and not the speed of the processor (CPU) on the machine running the game. While in ideal circumstances, the delta time will always be 16ms, there are any number of things that could cause a temporary slow down or hiccup in a frame, and using the delta time ensures that timing based events are always correct. + +## Conclusion + +Here is a review of what was accomplished in this chapter: + +- You read through the default code provided in a *Game1.cs* file created by a MonoGame template. +- You learned about the lifecycle of a MonoGame game project. +- You learned what a game loop is and how it is implemented in MonoGame. + +In the next chapter, you will start working with sprites and learn how to load and render them. + +## See Also + +This chapter briefly touched on the *Game1.cs* file and the [**Game**](xref:Microsoft.Xna.Framework.Game) class. For an in-depth detailed discussion of all files created in a MonoGame project, including a full overview of the order of execution for a MonoGame game, see [Appendix 02: MonoGame Project Overview](#). + +## Test Your Knowledge + +1. Can the `Game1` class be renamed or is it required to be called `Game1` + +
+ Question 1 Answer + + > It is not a requirement that it be called `Game1`. This is just the default name given to it by the templates when creating a new MonoGame game project. + +

+ +2. What is the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) used for? + +
+ Question 2 Answer + + > The [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) provides an optimized method of rendering 2D graphics, like sprites, onto the screen + +

+ +3. When is the [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method executed and why is it important to know this? + +
+ Question 3 Answer + + > [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) is executed during the `base.Initialize()` method call within the [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) method. It is important to know this because anything being initialized that is dependent on content loaded should be done **after** the `base.Initialize()` call and not **before**. + +

+ +4. How does MonoGame provide a *delta time* value? + +
+ Question 4 Answer + + > Through the [**GameTime**](xref:Microsoft.Xna.Framework.GameTime) parameter that is given to both the [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) and the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) methods. + +

diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/Game1.cs b/articles/tutorials/building_2d_games/04_content_pipeline/Game1.cs new file mode 100644 index 00000000..5d40f178 --- /dev/null +++ b/articles/tutorials/building_2d_games/04_content_pipeline/Game1.cs @@ -0,0 +1,56 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + private Texture2D _logo; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + + // TODO: use this.Content to load your game content here + _logo = Content.Load("images/logo"); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + // TODO: Add your update logic here + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + // TODO: Add your drawing code here + _spriteBatch.Begin(); + _spriteBatch.Draw(_logo, Vector2.Zero, Color.White); + _spriteBatch.End(); + + base.Draw(gameTime); + } +} diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/add-file-popup.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/add-file-popup.png new file mode 100644 index 00000000..f0726e8f Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/add-file-popup.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/content-pipeline-workflow-full.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/content-pipeline-workflow-full.png new file mode 100644 index 00000000..b505bff1 Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/content-pipeline-workflow-full.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/content-pipeline-workflow-full.svg b/articles/tutorials/building_2d_games/04_content_pipeline/images/content-pipeline-workflow-full.svg new file mode 100644 index 00000000..e6afb6a5 --- /dev/null +++ b/articles/tutorials/building_2d_games/04_content_pipeline/images/content-pipeline-workflow-full.svg @@ -0,0 +1,1258 @@ + + + +MonoGame Content Pipeline WorkflowRuntime PhaseBuild PhaseCreate the source files you'll use in your game including images, audio, fonts, effects, and models.Use the MonoGame Content Builder (MGCB) Editor to edit your content project by adding and/or removing the source files to be used in your game.Development PhaseMGCB EditorMGCB ToolMonoGame.Content.Builder.TasksContent.mgcbAudioImagesAaFontsEffectsModelsSource FilesXnbXnbAaXnbXnbXnbCompiled AssetsWhen performing a build of your project, the MonoGame.Content.Builder.Tasks reference will automatically compile the source files defined in the Content.mgcb content project file into XNB files using the MonoGame Content Builder (MGCB) Tool.The compiled assets are then automatically copied from your content project build directory to your game's build directoryAt runtime in the game, load the compiled assets using the ContentManager objectWhen saving the content project in the MGCB Editor, it will generate the Content.mgcb content project fileContentManagerHigh-Level View diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/logo-drawn.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/logo-drawn.png new file mode 100644 index 00000000..483916e2 Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/logo-drawn.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/logo.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/logo.png new file mode 100644 index 00000000..1509036c Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/logo.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-editor-icon.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-editor-icon.png new file mode 100644 index 00000000..d630100c Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-editor-icon.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-editor.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-editor.png new file mode 100644 index 00000000..35b6ce01 Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-editor.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-logo-added.png b/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-logo-added.png new file mode 100644 index 00000000..a2855741 Binary files /dev/null and b/articles/tutorials/building_2d_games/04_content_pipeline/images/mgcb-logo-added.png differ diff --git a/articles/tutorials/building_2d_games/04_content_pipeline/index.md b/articles/tutorials/building_2d_games/04_content_pipeline/index.md new file mode 100644 index 00000000..26ca51e1 --- /dev/null +++ b/articles/tutorials/building_2d_games/04_content_pipeline/index.md @@ -0,0 +1,223 @@ +--- +title: "Chapter 04: Content Pipeline" +description: Learn the advantages of using the Content Pipeline to load assets and go through the processes of loading your first asset +--- + +Every game has assets; images to represent the visual graphics to players, audio to provide sound effects and background music, fonts to render text with, and much more. These assets start out as raw files (e.g. *.png* image files or *.mp3* audio files), which you'll need to load into the game to use. + +## Loading Assets + +Loading assets can be done during runtime directly from file, or it can be loaded through the **Content Pipeline** Both of these methods are two sides of the same coin and there are trade offs to each approach. + +For instance, to load an image file directly at runtime, you would need to: + +1. Add the image file to your project. +2. Configure the project to copy the image file on build to the build output directory. +3. Load the image file as a texture at runtime using the [**Texture2D.FromFile**](xref:Microsoft.Xna.Framework.Graphics.Texture2D.FromFile(Microsoft.Xna.Framework.Graphics.GraphicsDevice,System.String)) method. + +> [!IMPORTANT] +> A big disadvantage to loading an image file as a texture directly, is when that when it loads it, it does so in its compressed format such as *.png* or *.jpg*. These compression formats are not understood by a Graphics Processing Unit (GPU); they will need to be decompressed into raw bytes as a format the GPU does understand before it can store the data. Doing this can potentially leave a larger memory footprint for your assets. You will also need to handle how different compression formats work on the platform you are targeting such as desktops, mobile, and consoles. +> +> Alternatively, as we'll explore below, using the **Content Pipeline** handles this for you automatically. + +On the other side of this coin, MonoGame offers the **Content Pipeline**; a workflow for managing assets. The workflow is made up of a set of tools and utilities that are automatically added by default when you create a new MonoGame project using the MonoGame project templates. To use this workflow, you need to: + +1. Add the asset file to your content project (*Content.mgcb* file) using the MGCB Editor. +2. Perform a project build. Doing this, the *MonoGame.Content.Builder.Tasks* NuGet reference will compile the assets defined in the content project, optimized for the target platform, and automatically copy them to the game project build directory. +3. Load the compiled asset at runtime using the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager). + +The following image illustrates this workflow: + +| ![Figure 4-1: MonoGame Content Pipeline Workflow](./images/content-pipeline-workflow-full.png) | +| :---: | +| **FigFigure 4-1: MonoGame Content Pipeline Workflowure** | + +For the same amount of steps, you also get the benefit of the assets being pre-processed and compiled to an optimized format for the target platform. For instance, image files can be compiled using [DXT compression](https://en.wikipedia.org/wiki/S3\_Texture\_Compression), which is a format that is understood by GPUs without needing to be decompressed first, reducing the memory footprint. + +> [!NOTE] +> For more information on the benefits of compiling assets and what optimizations it can offer, see the [Content Pipeline](../../../getting_started/content_pipeline/index.md) documentation. + +For this tutorial series, we are going to focus on using the content pipeline workflow to load assets. Doing this will get you as the developer accustomed to using the content pipeline tools and also give the benefits of having assets precompiled to optimized formats. + +To get started and to walk through the process, we'll start with loading an image file. Right-click the following image of the MonoGame logo and save it named *logo.png* somewhere on your on your computer, such as your desktop. + +| ![Figure 4-2: MonoGame Horizontal Logo](./images/logo.png) | +| :---: | +| **Figure 4-2: MonoGame Horizontal Logo** | + +## Opening the MGCB Editor + +The *MonoGame Content Builder Editor (MGCB Editor)* is a GUI tool that can be used to edit your content project. The content project is the *Content.mgcb* file in your game project directory. This file can be edited manually by hand, however it's much easier to use the MGCB Editor instead. To open the *Content.mgcb* content project file in the MGCB Editor, perform the following based on your editor: + +### [Visual Studio Code](#tab/vscode) + +To open the *Content.mgcb* content project file in the MGCB Editor with Visual Studio Code, you can use the *MonoGame for VSCode* extension. You should have installed this extension in [Chapter 02](../02_getting_started/index.md#installing-the-monogame-for-vscode-extension). With this extension install, anytime you have a code file open, you will see the MonoGame logo in the top-right of the code window like below: + +| ![Figure 4-3: MonoGame for VSCode extension icon](./images/mgcb-editor-icon.png) | +| :---: | +| **Figure 4-3: MonoGame for VSCode extension icon** | + +Clicking the MonoGame logo here will open the *Content.mgcb* content project file from the current project in the MGCB Editor. + +### [Visual Studio 2022](#tab/vs2022) + +To open the *Content.mgcb* content project file in the MGCB Editor with Visual Studio 2022, you can use the *MonoGame Framework C# project templates* extension. Despite the name, this extension does more than just install the MonoGame project templates. With this extension installed, simply double-click the *Content.mgcb* content project file in the Solution Explorer panel and it will open it in the MGCB Editor. + +### [dotnet CLI](#tab/dotnetcli) + +To open the *Content.mgcb* content project file in the MGCB Editor using the dotnet CLI commands, perform the following: + +1. Open a new Command Prompt or Terminal window in the same directory as your game project's *.csproj* file. +2. Enter the command `dotnet mgcb-editor ./Content/Content.mgcb` + +--- + +Once you have the *Content.mgcb* content project file for your game project opened in the MGCB Editor, you should see a window similar to the following: + +| ![Figure 4-4: MonoGame Content Builder Editor (MGCB Editor) Window](./images/mgcb-editor.png) | +| :---: | +| **Figure 4-4: MonoGame Content Builder Editor (MGCB Editor) Window** | + +## Adding Assets in the MGCB Editor + +Now that the *Content.mgcb* content project file is opened in the MGCB Editor, we can add the assets to it that we want to include in our game. For now, we're just going to add the image file of the MonoGame logo that you downloaded previously. To do this, perform the following: + +1. Right-click on the *Content* node in the *Project* panel on the left. +2. Select *Add > New Folder*. +3. Name the folder `images`. +4. Right-click on the new *images* folder +5. Select *Add > Existing Item*. +6. Navigate to the *logo.png* file you downloaded and choose it + +After adding an existing file, you will be prompted with a pop-up asking if you would like to *Copy the file to the directory* or *Add a link*. + +| ![Figure 4-5: Add Existing File Popup](./images/add-file-popup.png) | +| :---: | +| **Figure 4-5: Add Existing File Popup** | + +For the purposes of this tutorial, choose the *Copy the file to the directory* option, then click the *Add* button. When adding existing files in the future, the choice between copying the file and adding a link can make a big difference: + +- **Copy the file to the directory**: Choosing this will make a literal copy of the selected file and put the copy inside the Content directory of your project. This means any changes in the original source file will not be reflected in the copy. + +- **Add a link**: Choosing this will instead add a reference to the source file without making a copy. This means changes made in the source file will be reflected on each build. However, the link is stored as a relative link, with the path being relative to the *Content.mgcb* file. So if the source file moves, or you move the project, you'll need to re-add the link. + +After adding the *logo.png* file, your project node should look similar to the following: + +| ![Figure 4-6: Logo image added to the MGCB Editor](./images/mgcb-logo-added.png) | +| :---: | +| **Figure 4-6: Logo image added to the MGCB Editor** | + +After changes have been made in the MGBC Editor, ensure that you save the changes. They are not automatically saved, though you will be warned if you close the editor and haven't saved changes. You can tell that changes have not been saved by looking at the title bar of the MGCB editor window. If it has an '*' at the end of the window title, this means changes have not been saved. To save changes, you can perform one of the following: + +- Choose *File > Save* from the top menu. +- Click the *Save* icon in the top tool bar. +- Use the `CTRL+S` keyboard shortcut. + +Save the changes and then close the MGCB Editor. + +## Understanding Content Paths +The folder structure you create in the MGCB Editor directly affects how you load content in your game. When you perform a build of your game project, the *MonoGame.Content.Builder.Tasks* NuGet package reference will: + +1. Compile the image into an optimized format in the **content project's** output directory (typically *ProjectRoot/Content/bin/Platform/Content*) as an *.xnb* file. +2. Copy the compiled assets to your **game's** output directory (typically *ProjectRoot/bin/Debug/net8.0/Content* or *ProjectRoot/bin/Release/net8.0/Content*). + +For example, if your content project contains: +```sh +Content/ + ├── images/ + │ └── logo.png + └── sounds/ + └── music.mp3 +``` + +then when the tasks first compiles the assets, they will be output to: + +```sh +ProjectRoot/ + └── Content/ + └── bin/ + └── DesktopGL/ + └── Content/ + ├── images/ + │ └── logo.xnb + └── sounds/ + └── music.xnb +``` + +Then after compiling them and copying them to the game projects output directory, it will look like the following: + +```sh +ProjectRoot/ + └── bin/ + └── Debug/ + └── net8.0/ + └── Content/ + ├── images/ + │ └── logo.xnb + └── sounds/ + └── music.xnb +``` + +When the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager) is used to load these assets, it looks for them relative to its [**RootDirectory**](xref:Microsoft.Xna.Framework.Content.ContentManager.RootDirectory) property. By default, this is set to `"Content"` in the `Game1` constructor to match where the compiled assets are copied. The path used to load an asset must match its location relative to the [**RootDirectory**](xref:Microsoft.Xna.Framework.Content.ContentManager.RootDirectory), minus any extension. For example, to load the above assets, the paths would be `"images/logo"` and `"sounds/music"`. + +## Loading Content In Game + +To load assets that have been processed through the content pipeline, we can use the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager). The [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager) not only loads the asset for us, but als provides methods of managing the asset once loaded. For instance, when an asset is loaded the first time, the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager) internally caches the loaded asset. Loading that same asset later will return the cached asset instead of having to perform another disk read to load the asset again. + +The [**Game**](xref:Microsoft.Xna.Framework.Game) class provides the [**Content**](xref:Microsoft.Xna.Framework.Game.Content) property which is ready to use instance of the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager). We can use this to load content using the [**ContentManager.Load<T>**](xref:Microsoft.Xna.Framework.Content.ContentManager.Load``1(System.String)) method. This method takes two parts: + +1. `T` Type Reference: The content type we are loading. +2. `assetName` Parameter: A string path that matches the content path of the asset to load. As mentioned in the [Understanding Content Paths](#understanding-content-paths) section, the content path is relative to the [**ContentManager.RootDirectory**](xref:Microsoft.Xna.Framework.Content.ContentManager.RootDirectory), minus the extension. For instance, we added our image to the *images* folder in the content project, the content path for it will be `"images/logo"`. + +Let's update the game now to load the image file using the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager). First, open the *Game1.cs* file in your project and replace the contents with the following: + +[!code-csharp[](./Game1.cs?highlight=11,32,50-52)] + +The key changes we made here are + +- The `_logo` member was added to store a reference to the logo texture once we load it. +- In [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent), the logo texture is loaded using [**ContentManager.Load**](xref:Microsoft.Xna.Framework.Content.ContentManager.Load``1(System.String)). +- In [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), the logo is rendered using the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch). + + > [!NOTE] + > We'll go more into detail about the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) in the next chapter. + +Running the game now will show the MonoGame logo displayed in the upper-left corner of the game window. + +| ![Figure 4-7: The MonoGame logo drawn to the game window](./images/logo-drawn.png) | +| :---: | +| **Figure 4-7: The MonoGame logo drawn to the game window** | + +## Conclusion + +Let's review what you accomplished in this chapter: + +- You learned about the advantages of loading assets using the **Content Pipeline**. +- You added an image file asset to the *Content.mgcb* content project using the MGCB Editor. +- You learned about the **Content Pipeline** workflow and how MonoGame automates the process for you. +- You loaded the image file asset using the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager) + +In the next chapter, we'll go more into detail on working with textures and the various options available when rendering them. + +## Test Your Knowledge + +1. What are the two main ways of loading a texture, and what are the pros and cons of each approach? + +
+ Question 1 Answer + + > The two main ways to load a texture in MonoGame are: + > + > 1. Directly from file using [**Texture2D.FromFile**](xref:Microsoft.Xna.Framework.Graphics.Texture2D.FromFile(Microsoft.Xna.Framework.Graphics.GraphicsDevice,System.String)). This method requires manually setting up file copying, offers no pre-processing benefits, and can have a higher memory footprint. + > + > 2. Using the content pipeline with [**Content.Load**](xref:Microsoft.Xna.Framework.Content.ContentManager.Load``1(System.String)). Using the content pipeline optimizes textures into formats for the target platform(s), automatically handles compiling and copying assets during build, and reduces memory footprint, but requires additional setup using the MGCB Editor. +

+ +2. During the MonoGame content pipeline workflow, assets are compiled and then copied to the project output directory. What is responsible for performing this task? + +
+ Question 2 Answer + + > The *MonoGame.Content.Builder.Tasks* NuGet reference. + +

diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/icon-on-top-of-wordmark.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/icon-on-top-of-wordmark.png new file mode 100644 index 00000000..87096041 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/icon-on-top-of-wordmark.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/icon-wordmark-centered.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/icon-wordmark-centered.png new file mode 100644 index 00000000..e94d989c Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/icon-wordmark-centered.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-centered.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-centered.png new file mode 100644 index 00000000..a815839a Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-centered.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-drawn.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-drawn.png new file mode 100644 index 00000000..483916e2 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-drawn.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-flipped-horizontally-and-vertically.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-flipped-horizontally-and-vertically.png new file mode 100644 index 00000000..45b8fe28 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-flipped-horizontally-and-vertically.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-flipped-horizontally.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-flipped-horizontally.png new file mode 100644 index 00000000..52f3a409 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-flipped-horizontally.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-green-tint.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-green-tint.png new file mode 100644 index 00000000..3838cb1b Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-green-tint.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-half-transparency.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-half-transparency.png new file mode 100644 index 00000000..5259b758 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-half-transparency.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-off-center.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-off-center.png new file mode 100644 index 00000000..8cab62f2 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-off-center.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-rotated-centered.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-rotated-centered.png new file mode 100644 index 00000000..262fb941 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-rotated-centered.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-rotated-offcenter.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-rotated-offcenter.png new file mode 100644 index 00000000..707a714e Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-rotated-offcenter.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x-0.5x.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x-0.5x.png new file mode 100644 index 00000000..592f336f Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x-0.5x.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x-zero-origin.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x-zero-origin.png new file mode 100644 index 00000000..e178c2ef Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x-zero-origin.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x.png new file mode 100644 index 00000000..039de6d9 Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-scaled-1.5x.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-texture-regions.drawio b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-texture-regions.drawio new file mode 100644 index 00000000..f34ad94b --- /dev/null +++ b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-texture-regions.drawio @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-texture-regions.png b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-texture-regions.png new file mode 100644 index 00000000..09eb566c Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/logo-texture-regions.png differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/images/top-left-origin-rotation-example.gif b/articles/tutorials/building_2d_games/05_working_with_textures/images/top-left-origin-rotation-example.gif new file mode 100644 index 00000000..76c3779f Binary files /dev/null and b/articles/tutorials/building_2d_games/05_working_with_textures/images/top-left-origin-rotation-example.gif differ diff --git a/articles/tutorials/building_2d_games/05_working_with_textures/index.md b/articles/tutorials/building_2d_games/05_working_with_textures/index.md new file mode 100644 index 00000000..8468443e --- /dev/null +++ b/articles/tutorials/building_2d_games/05_working_with_textures/index.md @@ -0,0 +1,560 @@ +--- +title: "Chapter 05: Working with Textures" +description: Learn how to load and render textures using the MonoGame content pipeline and SpriteBatch. +--- + +Textures are images that are used in your game to represent the visual graphics to the player, commonly referred to as *Sprites*. In [Chapter 04](../04_content_pipeline/index.md#loading-assets), you went through the steps of using the **Content Pipeline** to load the MonoGame *logo.png* texture and rendering it to the screen. + +In this chapter, you will: + +- Learn how to render a texture with the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch). +- Explorer how to manipulate the way the texture is rendered using the parameters of the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color)) method. + +## Drawing a Texture + +When rendering in MonoGame, *render states*, properties of the [**GraphicsDevice**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice) that affect how rendering is performed, need to be set. When rendering 2D sprites, the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) class simplifies rendering by managing these render states for you. + +> [!IMPORTANT] +> Although the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) makes it easier to manage the render states for the [**GraphicsDevice**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice), it can also change states that you may have set manually, such as when you are performing 3D rendering. Keep this in mind when mixing 2D and 3D rendering. + +Three methods are are used when rendering with the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch): + +1. [**SpriteBatch.Begin**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Begin(Microsoft.Xna.Framework.Graphics.SpriteSortMode,Microsoft.Xna.Framework.Graphics.BlendState,Microsoft.Xna.Framework.Graphics.SamplerState,Microsoft.Xna.Framework.Graphics.DepthStencilState,Microsoft.Xna.Framework.Graphics.RasterizerState,Microsoft.Xna.Framework.Graphics.Effect,System.Nullable{Microsoft.Xna.Framework.Matrix})) prepares the Graphics Device for rendering, including the render states. +2. [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Rectangle,Microsoft.Xna.Framework.Color)) tells the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) what to render. This is usually called multiple times before [**SpriteBatch.End**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.End) and batches the draw calls for efficiency. +3. [**SpriteBatch.End**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.End) submits the draw calls that were batched to the graphics device to be rendered. + +> [!NOTE] +> The order of method calls when rendering using the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch) is important. [**SpriteBatch.Begin**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Begin(Microsoft.Xna.Framework.Graphics.SpriteSortMode,Microsoft.Xna.Framework.Graphics.BlendState,Microsoft.Xna.Framework.Graphics.SamplerState,Microsoft.Xna.Framework.Graphics.DepthStencilState,Microsoft.Xna.Framework.Graphics.RasterizerState,Microsoft.Xna.Framework.Graphics.Effect,System.Nullable{Microsoft.Xna.Framework.Matrix})) must be called before any [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Rectangle,Microsoft.Xna.Framework.Color)) calls are made. When finished, [**SpriteBatch.End**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.End) must be called before another [**SpriteBatch.Begin**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Begin(Microsoft.Xna.Framework.Graphics.SpriteSortMode,Microsoft.Xna.Framework.Graphics.BlendState,Microsoft.Xna.Framework.Graphics.SamplerState,Microsoft.Xna.Framework.Graphics.DepthStencilState,Microsoft.Xna.Framework.Graphics.RasterizerState,Microsoft.Xna.Framework.Graphics.Effect,System.Nullable{Microsoft.Xna.Framework.Matrix})) can be called. If these methods are called out of order, an exception will be thrown. + +As mentioned in [Chapter 03](../03_the_game1_file/index.md#the-game-loop), all rendering should be done inside the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method. The [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method's responsibility is to render the game state that was calculated in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)); it should not contain any game logic or complex calculations. + +At the end of [Chapter 04](../04_content_pipeline/index.md#loading-assets), you added the following code to the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) in the *Game1.cs* file: + +```cs +_spriteBatch.Begin(); +_spriteBatch.Draw(_logo, Vector2.Zero, Color.White); +_spriteBatch.End(); +``` + +These lines initialize the [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch), draw the logo at [**Vector2.Zero**](xref:Microsoft.Xna.Framework.Vector2.Zero) (0, 0), and complete the batch. When you ran the game and the logo appeared in the window's upper-left corner: + +| ![Figure 5-1: The MonoGame logo drawn to the game window](./images/logo-drawn.png) | +| :---: | +| **Figure 5-1: The MonoGame logo drawn to the game window** | + +The [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color)) method we just used can be given the following parameters: + +| Parameter | Type | Description | +|------------|------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| *texture* | [**Texture2D**](xref:Microsoft.Xna.Framework.Graphics.Texture2D) | The [**Texture2D**](xref:Microsoft.Xna.Framework.Graphics.Texture2D) to draw. | +| *position* | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The X and Y coordinates at which the texture will be rendered, with the texture's origin being the upper-left corner of the image. | +| *color* | [**Color**](xref:Microsoft.Xna.Framework.Color) | The color mask (tint) to apply to the image drawn. Specifying [**Color.White**](xref:Microsoft.Xna.Framework.Color.White) will render the texture with no tint. | + +Try adjusting the position and color parameters and see how they can affect the image being drawn. + +MonoGame uses a coordinate system where (0, 0) is at the screen's upper-left corner. X values increase moving right, and Y values increase moving down. Understanding this, let's try to center the logo on the game window. + +To center content on the screen, we need to find the window's center point. We can access this using the [**Window.ClientBounds**](xref:Microsoft.Xna.Framework.GameWindow.ClientBounds) property from the [**Game**](xref:Microsoft.Xna.Framework.Game) class, which represents the rectangular bounds of the game window. [**Window.ClientBounds**](xref:Microsoft.Xna.Framework.GameWindow.ClientBounds) exposes both [**Width**](xref:Microsoft.Xna.Framework.Rectangle.Width) and [**Height**](xref:Microsoft.Xna.Framework.Rectangle.Height) properties for the window's dimensions in pixels. By dividing these dimensions in half, we can can calculate the window's center coordinates. Let's update our [**Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Rectangle,Microsoft.Xna.Framework.Color)) method to use this: + +```cs +_spriteBatch.Draw(_logo, new Vector2(Window.ClientBounds.Width, Window.ClientBounds.Height) * 0.5f, Color.White); +``` + +> [!TIP] +> In the example above, we multiply the [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) created by `0.5f` to halve the value instead of dividing it by `2.0f`. If you are not used to seeing this, it might seem strange at first, but it is actually an optimization technique. CPUs are able to perform multiplication operations much faster than division operations and reading `* 0.5f` is easily understood to be the same thing as `/ 2.0f` when reading. + +We have now set the position to half the window's dimensions, which should center the logo. Let's run the game to see the result. + +| ![Figure 5-2: Attempting to draw the MonoGame logo centered on the game window](./images/logo-off-center.png) | +| :---: | +| **Figure 5-2: Attempting to draw the MonoGame logo centered on the game window** | + +The logo is not centered as we expected it to be. Even though we set the *position* parameter to the center of the game window, the texture starts drawing from its *origin*, which is the upper-left corner in this example. So when we set the position to the screen's center, we are actually placing the logo's upper-left corner at that point, not its center. + +One way to correct this is to subtract half the width and height of the texture from the game window's center position like so: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + (Window.ClientBounds.Width * 0.5f) - (_logo.Width * 0.5f), + (Window.ClientBounds.Height * 0.5f) - (_logo.Height * 0.5f)), + Color.White); // color +``` + +This offsets the position so that it correctly centers the image to the game window. + +| ![Figure 5-3: The MonoGame logo drawn centered on the game window](./images/logo-centered.png) | +| :---: | +| **Figure 5-3: The MonoGame logo drawn centered on the game window** | + +While this works, there is a better approach. There is a different overload of the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color)) method that provides additional parameters for complete control over the draw operation. Update your code to: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + (Window.ClientBounds.Width * 0.5f) - (_logo.Width * 0.5f), + (Window.ClientBounds.Height * 0.5f) - (_logo.Height * 0.5f)), + null, // sourceRectangle + Color.White, // color + 0.0f, // rotation + Vector2.Zero, // origin + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth +``` + +This overload produces the same centered result but exposes all parameters that control rendering for a draw operation. Unlike engines that abstract much of these details away, MonoGame provides explicit control for a flexible custom rendering pipeline. Here is what each parameter does: + +| Parameter | Type | Description | +|-------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| *texture* | [**Texture2D**](xref:Microsoft.Xna.Framework.Graphics.Texture2D) | The [**Texture2D**](xref:Microsoft.Xna.Framework.Graphics.Texture2D) to draw. | +| *position* | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The X and Y coordinate position at which the texture will be rendered, relative to the *origin* parameter. | +| *sourceRectangle* | [**Rectangle**](xref:Microsoft.Xna.Framework.Rectangle) | An optional region within the texture to be rendered in order to draw only a portion of the texture. Specifying `null` will render the entire texture. | +| *color* | [**Color**](xref:Microsoft.Xna.Framework.Color) | The color mask (tint) to apply to the image drawn. Specifying [**Color.White**](xref:Microsoft.Xna.Framework.Color.White) will render the texture with no tint. | +| *rotation* | `float` | The amount of rotation, in radians, to apply to the texture when rendering. Specifying `0.0f` will render the image with no rotation. | +| *origin* | [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) | The X and Y coordinate origin point of the texture when rendering. This will affect the offset of the texture when rendered as well being the origin in which the texture is rotated around and scaled from. | +| *scale* | `float` | The amount to scale the image across the x- and y-axes. Specifying `1.0f` will render the image at its default size with no scaling. | +| *effects* | [**SpriteEffects**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects) | A [**SpriteEffects**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects) enum value to that specifies if the texture should be rendered flipped across the horizontal axis, the vertical axis, or both axes. | +| *layerDepth* | `float` | Specifies the depth at which the texture is rendered. Textures with a higher layer depth value are drawn on top of those with a lower layer depth value. **Note: This value will only apply when using `SpriteSortMode.FrontToBack` or \`SpriteSortMode.BackToFront. We'll cover this in a moment.** | + +### Rotation + +First let's explore the `rotation` parameter. This value is the amount of rotation to apply to the sprite when rendering it. Let's rotate the texture 90° to make it vertical. Since rotation is measured in radians, not degrees, we can use the built-in math library in MonoGame to make the conversion for us by calling [**MathHelper.ToRadians**](xref:Microsoft.Xna.Framework.MathHelper.ToRadians(System.Single)). Update the code to: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + (Window.ClientBounds.Width * 0.5f) - (_logo.Width * 0.5f), + (Window.ClientBounds.Height * 0.5f) - (_logo.Height * 0.5f)), + null, // sourceRectangle + Color.White, // color + MathHelper.ToRadians(90), // rotation + Vector2.Zero, // origin + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth +``` + +Running the code now shows the rotated image, but not in the expected position: + +| ![Figure 5-4: Attempting to draw the MonoGame logo rotated 90° and centered on the game window](./images/logo-rotated-offcenter.png) | +| :---: | +| **Figure 5-4: Attempting to draw the MonoGame logo rotated 90° and centered on the game window** | + +The reason the sprite did not rotate as expected is because of the `origin` parameter. + +### Origin + +The `origin` parameter specifies the point of origin in which the sprite is rendered from, rotated from, and scaled from. By default, if no origin is set, it will be [**Vector2.Zero**](xref:Microsoft.Xna.Framework.Vector2.Zero), the upper-left corner of the sprite. To visualize this, see Figure 5-4 below. The red square represents where the origin is for the sprite, and we can see how it's rotated around this origin point. + +| ![Figure 5-5: Demonstration of how a sprite is rotated around its origin](./images/top-left-origin-rotation-example.gif) | +| :---: | +| **Figure 5-5: Demonstration of how a sprite is rotated around its origin** | + +To resolve the rotation issue we had, we only need to change the `origin` parameter so that instead of defaulting to the upper-left corner of the sprite, it is set to the center of the sprite. When doing this, we need to set the values based on the sprites width and height, so the center origin will be half the width and height of the sprite. Update the code to: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White, // color + MathHelper.ToRadians(90), // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth +``` + +By moving the sprite's origin point to its center, this not only corrects the point of rotation, but also eliminates the need to offset the position by half the sprite's dimensions. Running the game now shows the log properly centered and rotated 90°. + +| ![Figure 5-6: The MonoGame logo drawn rotated 90° and centered on the game window](./images/logo-rotated-centered.png) | +| :---: | +| **Figure 5-6: The MonoGame logo drawn rotated 90° and centered on the game window** | + +### Scale + +The `scale` parameter specifies the amount of scaling to apply to the sprite when it is rendered. The default value is `1.0f`, which can be read as "rendering the sprite at 1x the size". Increasing this will scale up the size of the sprite and decreasing it will scale down the sprite. Let's see an example of this by setting the scale of the logo sprite to `1.5f`: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + 1.5f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth +``` + +| ![Figure 5-7: The MonoGame logo drawn scaled at 1.5x the size](./images/logo-scaled-1.5x.png) | +| :---: | +| **Figure 5-7: The MonoGame logo drawn scaled at 1.5x the size** | + +Note that the sprite scaled up from the center. This is because we still have the `origin` parameter set as the center of the sprite. If we instead adjusted the code so the `origin` parameter was back in the upper-left corner like so: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White, // color + 0.0f, // rotation + Vector2.Zero, // origin + 1.5f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth +``` + +Then the scaling is applied from the origin in the upper-left corner producing the following result: + +| ![Figure 5-8: The MonoGame logo drawn scaled at 1.5x the size with the origin set in the upper-left corner](./images/logo-scaled-1.5x-zero-origin.png) | +| :---: | +| **Figure 5-8: The MonoGame logo drawn scaled at 1.5x the size with the origin set in the upper-left corner** | + +Scaling can also be applied to the x- and y-axes independently by providing it with a [**Vector2**](xref:Microsoft.Xna.Framework.Vector2) value instead of a float value. For instance, let's scale the x-axis of the sprite by 1.5x and reduce the scale of the y-axis to 0.5x: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + new Vector2(1.5f, 0.5f), // scale + SpriteEffects.None, // effects + 0.0f); +``` + +Which will produce the following result: + +| ![Figure 5-9: The MonoGame logo drawn scaled at 1.5x the size on the x-axis and 0.5x on the y-axis](./images/logo-scaled-1.5x-0.5x.png) | +| :---: | +| **Figure 5-9: The MonoGame logo drawn scaled at 1.5x the size on the x-axis and 0.5x on the y-axis** | + +### SpriteEffects + +The `effects` parameter is used to flip the sprite when rendered on either the horizontal or vertical axis, or both. This value for this parameter will be one of the [**SpriteEffects**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects) enum values. + +| SpriteEffect | Description | +| --- | --- | +| [**SpriteEffects.None**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects.None) | No effect is applied and the sprite is rendered normally. | +| [**SpriteEffects.FlipHorizontally**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects.FlipHorizontally) | The sprite is rendered flipped along the horizontal axis. | +| [**SpriteEffects.FlipVertically**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects.FlipVertically) | The sprite is rendered flipped along the vertical axis. | + +Let's see this by applying the [**SpriteEffects.FlipHorizontally**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects.FlipHorizontally) value to the sprite: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.FlipHorizontally, // effects + 0.0f); // layerDepth +``` + +Which will produce the following result: + +| ![Figure 5-10: The MonoGame logo flipped horizontally](./images/logo-flipped-horizontally.png) | +| :---: | +| **Figure 5-10: The MonoGame logo flipped horizontally** | + +The [**SpriteEffects**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects) enum value also uses the [`[Flag]`](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-flagsattribute) attribute, which means we can combine both horizontal and vertical flipping together. To do this, we use the [bitwise OR operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators#logical-or-operator-) `|`. Update the `effect` parameter value to the following: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.FlipHorizontally | // effects + SpriteEffects.FlipVertically, + 0.0f); // layerDepth +``` + +Now the sprite is flipped both horizontally and vertically + +| ![Figure 5-11: The MonoGame logo flipped horizontally and vertically](./images/logo-flipped-horizontally-and-vertically.png) | +| :---: | +| **Figure 5-11: The MonoGame logo flipped horizontally and vertically** | + +### Color and Opacity + +The `color` parameter applies a color mask to the sprite when it's rendered. Note that this is not setting the actual color of the image, just a mask that is applied, like a tint. The default value is [**Color.White**](xref:Microsoft.Xna.Framework.Color.White). So if we're setting it to [**Color.White**](xref:Microsoft.Xna.Framework.Color.White), why does this not affect the tinting of the sprite drawn? + +When the `color` parameter is applied, each color channel (Red, Green, Blue) of the sprite is multiplied by the corresponding channel in the `color` parameter, where each channel is represented as a value between `0.0f` and `1.0f`. For [**Color.White**](xref:Microsoft.Xna.Framework.Color.White), all color channels are set to `1.0f` (255 in byte form), so the multiplication looks like this: + +```sh +Final Red = Sprite Red * 1.0f +Final Green = Sprite Green * 1.0f +Final Blue = Sprite Blue * 1.0f; +``` + +Since multiplying by `1.0f` doesn't change the value, [**Color.White**](xref:Microsoft.Xna.Framework.Color.White) essentially preserves the original colors of the sprite. + +Let's change the `color` parameter to use [**Color.Green**](xref:Microsoft.Xna.Framework.Color.Green): + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.Green, // color + 0.0f, // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); +``` + +This produces the following result: + +| ![Figure 5-12: The MonoGame logo with a green color tint applied](./images/logo-green-tint.png) | +| :---: | +| **Figure 5-12: The MonoGame logo with a green color tint applied** | + +> [!NOTE] +> The icon and the word "GAME" in the logo look black after using a [**Color.Green**](xref:Microsoft.Xna.Framework.Color.Green) because the Red, Blue Green components of that color are (`0.0f`, `0.5f`, `0.0f`). The Orange color used in the logo is [**Color.MonoGameOrange**](xref:Microsoft.Xna.Framework.Color.MonoGameOrange), which has the component values of (`0.9f`, `0.23f`, `0.0f`). When multiplying the component values, the result is (`0.0f`, `0.125f`, `0.0f`) which would be Red 0, Green 31, Blue 0 in byte values. So it's not quite fully black, but it is very close. +> +> This is why it's important to understand how the `color` parameter values are applied to the sprite when it is rendered. + +To adjust the opacity of a sprite, we can multiply the `color` parameter value by a value between `0.0f` (fully transparent) and `1.0f` (fully opaque). For instance, if we wanted to render the logo with 50% transparency we can multiply the `color` parameter by `0.5f` like this: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + null, // sourceRectangle + Color.White * 0.5f, // color + 0.0f, // rotation + new Vector2( // origin + _logo.Width, + _logo.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); +``` + +Which will produce the following result: + +| ![Figure 5-13: The MonoGame logo with half transparency](./images/logo-half-transparency.png) | +| :---: | +| **Figure 5-13: The MonoGame logo with half transparency** | + +### Source Rectangle + +The `sourceRectangle` parameter specifies a specific boundary within the texture that should be rendered. So far, we've just set this parameter to `null`, which specifies that the full texture should be rendered. If we only wanted to render a portion of the texture as the sprite, we can set this parameter value. + +For instance, take the logo image we've been using. We can break it down into two distinct regions; the MonoGame icon and the MonoGame wordmark. + +| ![Figure 5-14: The MonoGame logo broken down into the icon and wordmark regions](./images/logo-texture-regions.png) | +| :---: | +| **Figure 5-14: The MonoGame logo broken down into the icon and wordmark regions** | + +We can see from Figure 5-13 above that the actual icon starts at position (0, 0) and is 128px wide and 128px tall. Likewise, the wordmark starts at position (150, 34) and is 458px wide and 58px tall. Knowing the starting position and the width and height of the region gives us a defined rectangle that we can use as the `sourceRectangle`. + +Let's see this in action by drawing the icon and the wordmark separately from the same texture. First, after the call to the [**GraphicsDevice.Clear**](xref:Microsoft.Xna.Framework.Graphics.GraphicsDevice.Clear(Microsoft.Xna.Framework.Color)) method, add the following variables: + +```cs +Rectangle iconSourceRect = new Rectangle(0, 0, 128, 128); +Rectangle wordmarkSourceRect = new Rectangle(150, 34, 458, 58); +``` + +Next, replace the current `_spriteBatch.Draw` method call with the following: + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + iconSourceRect, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + iconSourceRect.Width, + iconSourceRect.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth + +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + wordmarkSourceRect, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + wordmarkSourceRect.Width, + wordmarkSourceRect.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.None, // effects + 0.0f); // layerDepth +``` + +The following changes were made: + +- Two new [**Rectangle**](xref:Microsoft.Xna.Framework.Rectangle) values called `iconSourceRect` and `wordmarkSourceRect` that represent the boundaries of the MonoGame icon and wordmark regions within the logo texture were added. +- The *sourceRectangle* parameter of the `_spriteBatch.Draw` was updated to use the new `iconSourceRect` value. **Notice that we are still telling it to draw the `_logo` for the *texture*, we've just supplied it with a source rectangle this time.** +- The *origin* parameter was updated to use the width and height of the `iconSourceRect`. Since the overall dimensions of what we'll be rendering has changed due to supplying a source rectangle, the origin needs to be adjusted to those dimensions as well. +- Finally, a second `_spriteBatch.Draw` call is made, this time using the `wordmarkSourceRect` as the source rectangle so that the wordmark is drawn. + +If you run the game now, you should see the following: + +| ![Figure 5-16: The MonoGame icon and wordmark, from the logo texture, centered in the game window](./images/icon-wordmark-centered.png) | +| :---: | +| **Figure 5-16: The MonoGame icon and wordmark, from the logo texture, centered in the game window** | + +> [!NOTE] +> Making use of the `sourceRectangle` parameter to draw different sprites from the same texture is optimization technique that we'll explore further in the next chapter. + +### Layer Depth + +The final parameter to discuss is the `layerDepth` parameter. Notice that in Figure 5-14 above, the wordmark is rendered on top of the icon. This is because of the order the draw calls were made; first the icon was rendered, then the wordmark was rendered. + +The [**SpriteBatch.Begin**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Begin(Microsoft.Xna.Framework.Graphics.SpriteSortMode,Microsoft.Xna.Framework.Graphics.BlendState,Microsoft.Xna.Framework.Graphics.SamplerState,Microsoft.Xna.Framework.Graphics.DepthStencilState,Microsoft.Xna.Framework.Graphics.RasterizerState,Microsoft.Xna.Framework.Graphics.Effect,System.Nullable{Microsoft.Xna.Framework.Matrix})) method contains several optional parameters, one of which is the `sortMode` parameter. By default, this value is [**SpriteSortMode.Deferred**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.Deferred), which means what is drawn is done so in the order of the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.DrawString(Microsoft.Xna.Framework.Graphics.SpriteFont,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,System.Single,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) calls. Each subsequent call will be drawn visually on top of the previous call. + +When [**SpriteSortMode.Deferred**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.Deferred) is used, then the `layerDepth` parameter in the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.DrawString(Microsoft.Xna.Framework.Graphics.SpriteFont,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,System.Single,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) call is essentially ignored. For instance, in the first `_spriteBatch.Draw` method call, update the `layerDepth` parameter to `1.0f`. + +```cs +_spriteBatch.Draw( + _logo, // texture + new Vector2( // position + Window.ClientBounds.Width, + Window.ClientBounds.Height) * 0.5f, + iconSourceRect, // sourceRectangle + Color.White, // color + 0.0f, // rotation + new Vector2( // origin + iconSourceRect.Width, + iconSourceRect.Height) * 0.5f, + 1.0f, // scale + SpriteEffects.None, // effects + 1.0f); // layerDepth +``` + +Doing this should tell it to render on a layer above the wordmark since the icon is at `1.0f` and the wordmark is at `0.0f` for the `layerDepth`. However, if you run the game now, you'll see that no change actually happens; the wordmark is still drawn on top of the icon. + +To make use of the `layerDepth` parameter, you need to set the `sortMode` to either [**SpriteSortMode.BackToFront**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.BackToFront) or [**SpriteSortMode.FrontToBack**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.FrontToBack). + +| Sort Mode | Description | +|------------------------------|---------------------------------------------------------------------------------------| +| [**SpriteSortMode.BackToFront**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.BackToFront) | Sprites are sorted by depth in back-to-front order prior to drawing. | +| [**SpriteSortMode.FrontToBack**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.FrontToBack) | Sprites are sorted by depth in front-to-back order prior to drawing. | + +Let's see this in action. We've already set the `layerDepth` parameter of the icon to `1.0f`. Find the `_spriteBatch.Begin()` method call and update it to the following: + +```cs +_spriteBatch.Begin(sortMode: SpriteSortMode.FrontToBack); +``` + +Now we're telling it to use the [**SpriteSortMode.FrontToBack**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.FrontToBack) sort mode, which will sort the draw calls so that those with a higher `layerDepth` will be drawn on top of those with a lower one. Even though we didn't change the order of the `_spriteBatch.Draw` calls, if you run the game now, you will see the following: + +| ![Figure 5-17: The MonoGame icon drawn on top of the wordmark](./images/icon-on-top-of-wordmark.png) | +| :---: | +| **Figure 5-17: The MonoGame icon drawn on top of the wordmark** | + +There are also two additional [**SpriteSortMode**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode) values that can be used. These, however, are situational and can have draw backs when using them, so understanding what they are for is important. + +The first is [**SpriteSortMode.Texture**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.Texture). This works similar to [**SpriteSortMode.Deferred**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.Deferred) in that draw calls happen in the order they are made. However, before the draw calls are made, they are sorted by texture. This can be helpful when using multiple textures to reduce texture swapping, however it can have unintended results with layering if you're not careful. + +The second is [**SpriteSortMode.Immediate**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.Immediate). When using this sort mode, when a draw call is made, it is immediately flushed to the GPU and rendered to the screen, ignoring the layer depth, instead of batched and drawn when [**SpriteBatch.End**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.End) is called. Using this can cause performance issues and should only be used when necessary. We'll discuss an example of using this in a later chapter when we discuss shaders, since with [**SpriteSortMode.Immediate**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode.Immediate) you can adjust shader parameters for each individual draw call. + +## Conclusion + +Let's review what you accomplished in this chapter: + +- You learned about the different parameters of the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color)) method and how they affect sprite rendering. +- You learned how the `rotation` parameter works and how to convert between degrees and radians using [**MathHelper.ToRadians**](xref:Microsoft.Xna.Framework.MathHelper.ToRadians(System.Single)). +- You learned how the `origin` parameter affects sprite positioning, rotation, and scaling. +- You learned how to use the `scale` parameter to resize sprites uniformly or along individual axes. +- You explored the [**SpriteEffects**](xref:Microsoft.Xna.Framework.Graphics.SpriteEffects) enum to flip sprites horizontally and vertically. +- You learned how the `color` parameter can be used to tint sprites and adjust their opacity. +- You used the `sourceRectangle` parameter to draw specific regions from a texture. +- You explored sprite layering using the `layerDepth` parameter and different [**SpriteSortMode**](xref:Microsoft.Xna.Framework.Graphics.SpriteSortMode) options. + +In the next chapter, we'll take what we've learned about working with textures and learn techniques to optimize rendering to reduce texture swapping. + +## Test Your Knowledge + +1. What is the purpose of the `origin` parameter in SpriteBatch.Draw, and how does it affect position, rotation and scaling? + +
+ Question 1 Answer + + > The `origin` parameter determines the reference point for the sprite's position, rotation, and scaling. When set to [**Vector2.Zero**](xref:Microsoft.Xna.Framework.Vector2.Zero), the sprite rotates and scales from its upper-left corner. When set to the center of the sprite, the sprite rotates and scales from its center. The origin point also affects where the sprite is positioned relative to the `position` parameter. +

+ +2. How can you adjust a sprite's opacity using [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.DrawString(Microsoft.Xna.Framework.Graphics.SpriteFont,System.Text.StringBuilder,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,System.Single,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single))? + +
+ Question 2 Answer + + > A sprite's opacity can be adjusted by multiplying the `color` parameter by a value between `0.0f` (fully transparent) and `1.0f` (fully opaque). For example, `Color.White * 0.5f` will render the sprite at 50% opacity. +

+ +3. How can you flip a sprite horizontally and vertically at the same time using SpriteEffects? + +
+ Question 3 Answer + + > To flip a sprite both horizontally and vertically, you can combine the SpriteEffects values using the bitwise OR operator (`|`): + > + > ```cs + > SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically + > ``` + +

+ +4. When using the `sourceRectangle` parameter, what information do you need to specify, and what is its purpose? + +
+ Question 4 Answer + + > The `sourceRectangle` parameter requires a [**Rectangle**](xref:Microsoft.Xna.Framework.Rectangle) value where the x- and y-coordinates specify the upper-left corner of the region within the texture and the width and height, in pixels, of the region. + > + > Its purpose is to specify a specific region within a texture to draw, allowing multiple sprites to be drawn from different parts of the same texture. +

diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/atlas.png b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/atlas.png new file mode 100644 index 00000000..9e9dce5a Binary files /dev/null and b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/atlas.png differ diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/logo-texture-regions.png b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/logo-texture-regions.png new file mode 100644 index 00000000..09eb566c Binary files /dev/null and b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/logo-texture-regions.png differ diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/mgcb-editor-copy.png b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/mgcb-editor-copy.png new file mode 100644 index 00000000..0d36bc94 Binary files /dev/null and b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/mgcb-editor-copy.png differ diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/pong-atlas.png b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/pong-atlas.png new file mode 100644 index 00000000..7ca80388 Binary files /dev/null and b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/pong-atlas.png differ diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/slime-and-bat-rendered.png b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/slime-and-bat-rendered.png new file mode 100644 index 00000000..51d92b90 Binary files /dev/null and b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/slime-and-bat-rendered.png differ diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/slime-rendered.png b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/slime-rendered.png new file mode 100644 index 00000000..17452c6c Binary files /dev/null and b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/images/slime-rendered.png differ diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/index.md b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/index.md new file mode 100644 index 00000000..97b5498b --- /dev/null +++ b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/index.md @@ -0,0 +1,701 @@ +--- +title: "Chapter 06: Optimizing Texture Rendering" +description: Explore optimization techniques when rendering textures using a texture atlas. +--- + +In [Chapter 05](../05_working_with_textures/index.md), you learned how to load and render textures using [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch). While rendering individual textures works well for simple games, it can lead to performance issues as your game grows more complex. In this chapter, we will explore how to optimize texture rendering by reducing texture swaps and creating reusable components for better organization. + +In this chapter, you will: + +- Learn about texture swapping and its impact on performance. +- Explore texture atlases as a solution for optimizing texture rendering. +- Explore class libraries and the benefits of using them. +- Create reusable classes to optimize and simplify texture management and rendering. + +By the end of this chapter, you'll understand how to organize your game's textures for optimal performance and have a flexible texture atlas management system for your future game projects. + +## Texture Swapping + +Every time the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,System.Nullable{Microsoft.Xna.Framework.Rectangle},Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) method is executed with a different *texture* parameter than the previous [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,System.Nullable{Microsoft.Xna.Framework.Rectangle},Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Vector2,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) method call, a *texture swap* occurs, unbinding the current texture on the GPU and binding the new texture. + +> [!NOTE] +> A texture swap occurs when the GPU needs to switch between different textures during rendering. While each individual swap may seem trivial, the cumulative effect in a complex game can significantly impact performance. + +For example, let's explore the following simplified draw calls for an example Pong game: + +```cs +// Using the paddle texture to render the left player paddle. +// The paddle texture is bound to the GPU. +_spriteBatch.Draw(paddleTexture, leftPaddlePosition, Color.White); + +// Using the ball texture to render the ball +// A texture swap occurs, unbinding the paddle texture to bind the ball texture. +_spriteBatch.Draw(ballTexture, ballPosition, Color.White); + +// Reusing the paddle texture to draw the right player paddle. +// A texture swap occurs again, unbinding the ball texture to bind the paddle texture. +_spriteBatch.Draw(paddleTexture, rightPaddlePosition, Color.White); +``` + +In the above example: + +1. The paddle texture is bound to the GPU so the left player paddle can be drawn. +2. The paddle texture is unbound from the GPU and the ball texture is bound so that the ball can be drawn (Texture Swap #1). +3. The ball texture is unbound from the GPU and the paddle texture is bound again so the right player paddle can be drawn (Texture Swap #2). + +These texture swaps, while negligible in this example, can become a performance issue in a full game where you might be drawing hundreds or thousands of sprites per frame. + +### Attempting to Optimize Draw Order + +One approach to get around this could be to optimize the order of the draw calls to minimize texture swaps For example, if we reorder the draw calls from the previous example so that both paddles are drawn first and then the ball, the number of texture swaps is reduced from two to one: + +```cs +// Render the left and right paddles first. +// This reduces the number of texture swaps needed from two to one. +_spriteBatch.Draw(paddleTexture, _leftPaddlePosition, Color.White); +_spriteBatch.Draw(paddleTexture, _rightPaddlePosition, Color.White); +_spriteBatch.Draw(ballTexture, _ballPosition, Color.White); +``` + +However this is not a scalable solution. In a real game with dozens of different textures and complex draw orders for layered sprites, UI elements, particles, etc., managing draw order by texture becomes impractical and will conflict with desired visual layering. + +## What is a Texture Atlas + +A texture atlas (also known as a sprite sheet) is a large image file that contains multiple smaller images packed together. Instead of loading separate textures for each sprite, you load the single texture file with all the images combined like a scrapbook where all your photos are arranged on the same page. + +> [!NOTE] +> Using a texture atlas not only eliminates texture swaps but also reduces memory usage and simplifies asset management since you're loading and tracking a single texture instead of many individual ones. + +In the Pong example, imagine taking the paddle and ball image and combining them into a single image file like in Figure 6-1 below: + +| ![Figure 6-1: Pong Texture Atlas Example](./images/pong-atlas.png) | +| :---: | +| **Figure 6-1: Pong Texture Atlas Example** | + +Now when we draw these images, we would be using the same texture and just specify the source rectangles for the paddle or ball when needed, completely eliminating texture swaps. + +```cs +private Texture2D _textureAtlas; +private Rectangle _paddleSourceRect; +private Rectangle _ballSourceRect; + +protected override void LoadContent() +{ + _textureAtlas = Content.Load("pong-atlas"); + _paddleSourceRect = new Rectangle(0, 0, 32, 32); + _ballSourceRect = new Rectangle(32, 0, 32, 32); +} + +protected override void Draw(GameTime gameTime) +{ + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(); + + // All draw calls use the same texture, so there is no texture swapping! + _spriteBatch.Draw(_textureAtlas, _leftPaddlePosition, _paddleSourceRect, Color.White); + _spriteBatch.Draw(_textureAtlas, _rightPaddlePosition, _paddleSourceRect, Color.White); + _spriteBatch.Draw(_textureAtlas, _ballPosition, _ballSourceRect, Color.White); + + _spriteBatch.End(); +} +``` + +While using the single texture with source rectangles solves the potential performance issues, managing multiple source rectangles in variables can become complex as your game grows. In the Pong example above, we're already tracking the source rectangles for both the paddle and ball sprites. Imagine scaling this up to a game with dozens of different images, each potentially needing their own position, rotation, scale, and other rendering properties. + +To better organize this complexity, we can apply object-oriented design principles to create classes that encapsulates the information needed. Before creating these classes though, let's create a *class library* to put them in. + +## Creating A Class Library + +Instead of adding the classes we will create directly to the game *MonoGameSnake* game project, we can create a *class library* and add it to that. Creating a class library offers several advantages, including: + +1. **Reusability**: The classes can be easily reused in future game projects by simply adding a reference to the library. +2. **Organization**: Keeps game-specific code separate from reusable library code. +3. **Maintainability**: Changes to the library can benefit all games that use it. +4. **Testing**: The library code can be tested independently of any specific game. + +A class library is a project type that compiles into a [Dynamic Link Library](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-libraries) (DLL) instead of an executable. It contains reusable code that can be referenced by other projects, making it perfect for sharing common functionality across multiple games. MonoGame offers the *MonoGame Game Library* project template that can be used to create a class library. + +### Adding the Class Library + +MonoGame offers the *MonoGame Game Library* project template to add a new class library project that is configured with the correct monoGame framework references. Using this template saves time and ensures compatibility with MonoGame projects. + +To use the template to add the class library, perform the following + +#### [Visual Studio Code](#tab/vscode) + +To add the class library using the MonoGame Game Library project template in Visual Studio Code, perform the following: + +1. In the *Solution Explorer* panel, right-click the *MonoGameSnake* solution. +2. Chose *New Project* from the context menu. +3. Enter "Monogame Game Library" and select it as the template to use. +4. Name the project "MonoGameLibrary". +5. When prompted for a location, use the default option, which will put the new project in a folder next to your game project. +6. Select "Create Project". + +#### [Visual Studio 2022](#tab/vs2022) + +To add the class library using the MonoGame Game Library project template in Visual Studio 2022, perform the following: + +1. Right-click the *MonoGameSnake* solution in the Solution Explorer panel. +2. Choose Add > New Project from the context menu. +3. Enter "MonoGame Game Library" in the search box, select that template, then click Next. +4. Name the project "MonoGameLibrary". +5. The location by default will put the new project in a folder next to your game project; you do not need to adjust this. +6. Click "Create". + +#### [dotnet CLI](#tab/dotnetcli) + +To add the class library using the MonoGame Game Library project template with the dotnet CLI, perform the following: + +1. Open a new Command Prompt or Terminal window in the same directory as the *MonoGameSnake.sln* solution file. +2. Enter the command `dotnet new mglib -n MonoGameLibrary` to create the project, placing it in a folder next to your game project. +3. Enter the command `dotnet sln MonoGameSnake.sln add ./MonoGameLibrary/MonoGameLibrary.csproj` to add the newly created class library project to the *MonoGameSnake.sln* solution file. + +--- + +### Adding a Reference To The Class Library + +Now that the game library project has been created, a reference to it needs to be added in our game project. Without adding a reference, our game project will be unaware of anything we add to the class library. To do this: + +#### [Visual Studio Code](#tab/vscode) + +To add the game library project as a reference to the game project in Visual Studio Code: + +1. In the Solution Explorer panel, right-click the *MonoGameSnake* project. +2. Choose "Add Project Reference" from the context menu. +3. Choose *MonoGameLibrary" from the available options. + +> [!TIP] +> The Solution Explorer panel in VSCode is provided by the C# Dev Kit extension that was installed in [Chapter 02](../02_getting_started/index.md#install-the-c-dev-kit-extension). If you do not see this panel, you can open it by +> +> 1. Opening the *Command Palette* (View > Command Palette). +> 2. Enter "Explorer: Focus on Solution Explorer View" and select the command. + +#### [Visual Studio 2022](#tab/vs2022) + +To add the game library project as a reference to the game project in Visual Studio 2022: + +1. In the Solution Explorer panel, right-click the *MonoGameSnake* project. +2. Select Add > Project Reference from the context menu. +3. Check the box for the *MonoGameLibrary* project. +4. Click Ok. + +#### [dotnet CLI](#tab/dotnetcli) + +To add the game library project as a reference to the game project with the dotnet CLI: + +1. Open a new Command Prompt or Terminal window in the same directory as the *MonoGameSnake.csproj* C# project file. +2. Enter the command `dotnet add ./MonoGameSnake.csproj reference ../MonoGameLibrary/MonoGameLibrary.csproj`. This will add the *MonoGameLibrary* reference to the *MonoGameSnake* game project. + +--- + +### Clean Up + +When using the *MonoGame Game Library* project template, the generated project contains file similar to a standard MonoGame game project, including a *dotnet-tools.json* manifest file, a *Content.mgcb* file, and a *Game1.cs* file. For the purposes of this tutorial, we will not need these. To clean these up, locate the following in the *MonoGameLibrary* project directory and delete them: + +1. The *.config/* directory. +2. The *Content/* directory +3. The *Game1.cs* file. + +> [!TIP] +> These files are needed in more advanced scenarios such as creating a central code base for game logic that is referenced by other projects of which each target different platforms such as desktop, mobile, and console. Creating a project structure of this type is out of scope for this tutorial. +> +> If you would like more information on this, Simon Jackson has written the article [Going cross-platform with MonoGame](https://darkgenesis.zenithmoon.com/going-cross-platform-with-monogame.html) which covers this in more detail. + +## The TextureRegion Class + +In [Chapter 05](../05_working_with_textures/index.md#source-rectangle), we learned about using the `sourceRectangle` parameter to reuse the same texture when rendering sprites but specifying different regions within the texture to render. Let's first build on this and create a class called `TextureRegion`. + +We're going to add this class to the class library we just created. Perform the following: + +1. Add new directory in the *MonoGameLibrary* project named `Graphics` +2. Create a new file named *TextureRegion.cs* inside the *Graphics* directory you just created. + +Add the following code for the foundation of the `TextureRegion` class to the *TextureRegion.cs* file: + +```cs +using System.Diagnostics; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class TextureRegion +{ + +} +``` + +> [!NOTE] +> The *TextureRegion.cs* class file is placed in the *MonoGame/Graphics* directory and the class uses the `MonoGame.Graphics` [namespace](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/namespaces#namespaces-overview) to keep graphics-related classes organized together. As we add more functionality to the library, we will continue to use directories and namespaces to maintain a clean structure. + +### TextureRegion Members + +The `TextureRegion` class will utilize four properties to define and manage a region within a texture. Add the following properties: + +```cs +/// +/// Gets or Sets the source texture this texture region is part of. +/// +public Texture2D Texture { get; set; } + +/// +/// Gets or Sets the source rectangle boundary of this texture region within the source texture. +/// +public Rectangle SourceRectangle { get; set; } + +/// +/// Gets the width, in pixels, of this texture region. +/// +public int Width => SourceRectangle.Width; + +/// +/// Gets the height, in pixels, of this texture region. +/// +public int Height => SourceRectangle.Height; +``` + +The `Texture` and `SourceRectangle` properties work together to define where the region is located: `Texture` specifies which texture contains the region, while `SourceRectangle` defines its exact location and size within that texture. The `Width` and `Height` properties provide convenient access to the region's dimensions without having to access the SourceRectangle property directly. + +### TextureRegion Constructor + +The `TextureRegion` class will provide two ways to create a new texture region. Add the following constructors: + +```cs +/// +/// Creates a new texture region. +/// +public TextureRegion() { } + +/// +/// Creates a new texture region using the specified source texture. +/// +/// The texture to use as the source texture for this texture region. +/// The x-coordinate position of the upper-left corner of this texture region relative to the upper-left corner of the source texture. +/// +/// The width, in pixels, of this texture region. +/// The height, in pixels, of this texture region. +public TextureRegion(Texture2D texture, int x, int y, int width, int height) +{ + Texture = texture; + SourceRectangle = new Rectangle(x, y, width, height); +} +``` + +The default constructor creates an empty texture region that can be configured later, while the parameterized constructor allows you to define the region's source texture and boundary in a single step. This second constructor provides a convenient way to create texture regions when you know the exact location and dimensions within the source texture upfront. + +### TextureRegion Methods + +Finally, the `TextureRegion` class will provide three overloaded Draw methods to render the texture region. Add the following methods: + +```cs +/// +/// Submit this texture region for drawing in the current batch. +/// +/// The spritebatch instance used for batching draw calls. +/// The xy-coordinate location to draw this texture region on the screen. +/// The color mask to apply when drawing this texture region on screen. +public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color) +{ + Draw(spriteBatch, position, color, 0.0f, Vector2.Zero, Vector2.One, SpriteEffects.None, 0.0f); +} + +/// +/// Submit this texture region for drawing in the current batch. +/// +/// The spritebatch instance used for batching draw calls. +/// The xy-coordinate location to draw this texture region on the screen. +/// The color mask to apply when drawing this texture region on screen. +/// The amount of rotation, in radians, to apply when drawing this texture region on screen. +/// The center of rotation, scaling, and position when drawing this texture region on screen. +/// The scale factor to apply when drawing this texture region on screen. +/// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen. +/// The depth of the layer to use when drawing this texture region on screen. +public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, float scale, SpriteEffects effects, float layerDepth) +{ + Draw( + spriteBatch, + position, + color, + rotation, + origin, + new Vector2(scale, scale), + effects, + layerDepth + ); +} + +/// +/// Submit this texture region for drawing in the current batch. +/// +/// The spritebatch instance used for batching draw calls. +/// The xy-coordinate location to draw this texture region on the screen. +/// The color mask to apply when drawing this texture region on screen. +/// The amount of rotation, in radians, to apply when drawing this texture region on screen. +/// The center of rotation, scaling, and position when drawing this texture region on screen. +/// The amount of scaling to apply to the x- and y-axes when drawing this texture region on screen. +/// Specifies if this texture region should be flipped horizontally, vertically, or both when drawing on screen. +/// The depth of the layer to use when drawing this texture region on screen. +public void Draw(SpriteBatch spriteBatch, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) +{ + spriteBatch.Draw( + Texture, + position, + SourceRectangle, + color, + rotation, + origin, + scale, + effects, + layerDepth + ); +} +``` + +These methods provide flexible options for rendering the texture region, similar to what the [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,System.Nullable{Microsoft.Xna.Framework.Rectangle},Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,System.Single,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) method does: + +- The simplest overload requires only position and color. +- A second overload exposes all rendering parameters while allowing for a single float value to be applied to both axes for scaling. +- The third overload is the most flexible, offering all rendering parameters and independent x- and y-axis scaling. + +### Using the TextureRegion Class + +Let's put our new `TextureRegion` class to use by creating a simple game scene. So far, we've been practicing using textures with the MonoGame logo. Now we will use a new texture atlas that contains various sprites we'll need for our game. + +Download the texture atlas by right-clicking the following image and saving it as *atlas.png*: + +| ![Figure 6-2: The texture atlas for our game](./images/atlas.png) | +| :---: | +| **Figure 6-2: The texture atlas for our game** | + +Add this texture atlas to your content project using the MGCB Editor: + +1. Open the *Content.mgcb* file in the MGCB Editor +2. In the editor, right-click the *images* directory and choose *Add > Existing item...*. +3. Navigate to and choose the *atlas.png* file you downloaded to add it. +4. Save the changes and close the MGCB Editor. + +> [!TIP] +> If you need a refresher on adding content using the MGCB Editor, you can revisit the [Adding Assets in the MGCB Editor](../04_content_pipeline/index.md#adding-assets-in-the-mgcb-editor) section of Chapter 04. + +Replace the contents of *Game1.cs* with the following code: + +[!code-csharp[](./src/Game1-texture-region-usage.cs?highlight=12,32-36,51-56)] + +Let's examine the key changes in the code: + +1. We added a `TextureRegion` member `_slime` to store our sprite from the atlas. +2. In the [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method, we created a texture region by + - Loading the atlas texture using the content manager + - Created a `TextureRegion` that defines the slime's location in the atlas at position 0,160 with the size 40x40. +3. Update the [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) method to render the slime texture region using the `TextureRegion.Draw` method. + +Running the game now shows the slime sprite in the upper-left corner of the game window: + +| ![Figure 6-3: The slime texture region being rendered in the upper-left corner of the game window](./images/slime-rendered.png) | +| :---: | +| **Figure 6-3: The slime texture region being rendered in the upper-left corner of the game window** | + +## The TextureAtlas Class + +In the [What is a Texture Atlas](#what-is-a-texture-atlas) section above, a texture atlas was described as a scrap book that holds all of the individual sprites for the game. These individual sprites can now be represented by the `TextureRegion` class we just created. Now, we'll create the `TextureAtlas` class to represent the collection of the regions that make up all of our sprites. + +Just like the `TextureRegion` class, we're going to add this to the class library. In the *Graphics* directory within the *MonoGameLibrary* project, add a new file named *TextureAtlas.cs*. Add the following code for the foundation fo the `TextureAtlas` class to the *TextureAtlas.cs* file: + +```cs +using System.Collections.Generic; +using System.IO; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class TextureAtlas +{ + +} +``` + +### TextureAtlas Members + +The `TextureAtlas` class needs two key members to manage texture regions. Add the following: + +```cs +private Dictionary _regions; + +/// +/// Gets or Sets the source texture represented by this texture atlas. +/// +public Texture2D Texture { get; set; } +``` + +The private `_regions` dictionary stores named texture regions, allowing us to retrieve specific regions by name, while the `Texture` property holds the source texture that contains all the regions. Together, these members enable the atlas to manage multiple texture regions from a single source texture. + +### TextureAtlas Constructors + +The `TextureAtlas` class will provide two ways to create a new atlas. Add the following constructors: + +```cs +/// +/// Creates a new texture atlas. +/// +public TextureAtlas() +{ + _regions = new Dictionary(); +} + +/// +/// Creates a new texture atlas instance using the given texture. +/// +/// The source texture represented by the texture atlas. +public TextureAtlas(Texture2D texture) +{ + Texture = texture; + _regions = new Dictionary(); +} +``` + +The default constructor creates an empty atlas that can be configured later, while the parameterized constructor allows you to specify the source texture immediately. Both constructors initialize the `_regions` dictionary so that it's ready to be used either way. + +### TextureAtlas Methods + +Finally, The `TextureAtlas` class will provide methods for managing texture regions and creating atlases from configuration files. Add the following methods: + +```cs +/// +/// Creates a new region and adds it to this texture atlas. +/// +/// The name to give the texture region. +/// The top-left x-coordinate position of the region boundary relative to the top-left corner of the source texture boundary. +/// The top-left y-coordinate position of the region boundary relative to the top-left corner of the source texture boundary. +/// The width, in pixels, of the region. +/// The height, in pixels, of the region. +public void AddRegion(string name, int x, int y, int width, int height) +{ + TextureRegion region = new TextureRegion(Texture, x, y, width, height); + _regions.Add(name, region); +} + +/// +/// Gets the region from this texture atlas with the specified name. +/// +/// The name of the region to retrieve. +/// The TextureRegion with the specified name. +public TextureRegion GetRegion(string name) +{ + return _regions[name]; +} + +/// +/// Removes the region from this texture atlas with the specified name. +/// +/// The name of the region to remove. +/// +public bool RemoveRegion(string name) +{ + return _regions.Remove(name); +} + +/// +/// Removes all regions from this texture atlas. +/// +public void Clear() +{ + _regions.Clear(); +} + +/// +/// Creates a new texture atlas based a texture atlas xml configuration file. +/// +/// The content manager used to load the texture for the atlas. +/// The path to the xml file, relative to the content root directory. +/// The texture atlas created by this method. +public static TextureAtlas FromFile(ContentManager content, string fileName) +{ + TextureAtlas atlas = new TextureAtlas(); + + string filePath = Path.Combine(content.RootDirectory, fileName); + + using (Stream stream = TitleContainer.OpenStream(filePath)) + { + using (XmlReader reader = XmlReader.Create(stream)) + { + XDocument doc = XDocument.Load(reader); + XElement root = doc.Root; + + // The element contains the content path for the Texture2D to load. + // So we'll retrieve that value then use the content manager to load the texture. + string texturePath = root.Element("Texture").Value; + atlas.Texture = content.Load(texturePath); + + // The element contains individual elements, each one describing + // a different texture region within the atlas. + // + // Example: + // + // + // + // + // + // So we retrieve all of the elements then loop through each one + // and generate a new TextureRegion instance from it and add it to this atlas. + var regions = root.Element("Regions")?.Elements("Region"); + + if (regions != null) + { + foreach (var region in regions) + { + string name = region.Attribute("name")?.Value; + int x = int.Parse(region.Attribute("x")?.Value ?? "0"); + int y = int.Parse(region.Attribute("y")?.Value ?? "0"); + int width = int.Parse(region.Attribute("width")?.Value ?? "0"); + int height = int.Parse(region.Attribute("height")?.Value ?? "0"); + + if (!string.IsNullOrEmpty(name)) + { + atlas.AddRegion(name, x, y, width, height); + } + } + } + + return atlas; + } + } +} +``` + +These methods serve different purposes in managing the texture atlas: + +1. Region Management + - `AddRegion`: Creates a new `TextureRegion` at the specified location in the atlas. + - `GetRegion`: Retrieves a previously added region by its name. + - `RemoveRegion`: Removes a specific region by its name. + - `Clear`: Removes all regions from the atlas. +2. Atlas Creation + - `FromFile`: creates a new `TextureAtlas` from an XML configuration file. This method will load the source texture then create and add the regions based on the XML configuration. We'll look more into using the XML configuration in a moment. + +### Using the TextureAtlas Class + +Let's put our new `TextureAtlas` class to use by exploring two approaches; creating an atlas manually and using XML configuration. + +First, let's create the texture atlas in code. Replace the contents of *Game1.cs* with the following: + +[!code-csharp[](./src/Game1-texture-atlas-usage.cs?highlight=13,36-45,65-66)] + +The key changes in this implementation are: + +1. Added a `TextureREgion` member `_bat` alongside our existing `_slime`. +2. In [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent): + - Created a `TextureAtlas` with the atlas texture. + - Added regions for both the slime and the bat. + - Retrieved the regions using their names. +3. Updated [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)) to render both sprites, using the slime's `Width` property to position the bat. + +Running the game now shows both sprites in the upper-left corner: + +| ![Figure 6-4: The slime and bat texture regions being rendered in the upper-left corner of the game window](./images/slime-and-bat-rendered.png) | +| :---: | +| **Figure 6-4: The slime and bat texture regions being rendered in the upper-left corner of the game window** | + +While manual creation works for a few sprites, managing many regions becomes cumbersome. Let's now explore the `TextureAtlas.FromFile` method to load our atlas configuration from XML instead. Perform the following: + +1. Create a new file named *atlas-definition.xml* in the *Content/images* directory. +2. Add the following content to that file: + + ```xml + + + images/atlas + + + + + + ``` + +3. Open the *Content.mgcb* file in the MGCB Editor +4. In the editor, right-click the *images* directory and choose *Add . Existing item...*. +5. Navigate to and choose the *atlas-definition.xml* file you just created to add it. +6. In the properties panel at the bottom for the *atlas-definition.xml* file, change the *Build Action* property from *Build* to *Copy*. + + | ![Figure 6-5: The atlas-definition.xml file added to the content project with the Build Action property set to Copy](./images/mgcb-editor-copy.png) | + | :---: | + | **Figure 6-5: The atlas-definition.xml file added to the content project with the Build Action property set to Copy** | + +7. Save the changes and close the MGCB Editor + + > [!TIP] + > Using the content pipeline to copy files ensures they're placed in the correct location alongside other game content. While there are other methods (like editing the .csproj), this approach keeps asset management centralized + +8. Replace the contents of *Game1.cs* with the following code: + +[!code-csharp[](./src/Game1-texture-atlas-xml-usage.cs?highlight=33-38)] + +The key improvements here is in [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent), where we now: + +- Create an atlas from the XML configuration file. +- Let the `TextureAtlas.FromFile` method handle texture loading and region creation. + +This configuration based approached is advantageous because we can now add new and modify existing regions within the atlas without having to change code and/or recompile. This also keeps the sprite definitions separate from the game logic. + +Running the game now will show the same results as Figure 6-5 above, with the slime and bat texture regions rendered in the upper-left corner of the game window. + +## Conclusion + +Let's review what you accomplished in this chapter: + +- Learned about texture swapping and its impact on performance +- Explored texture atlases as a solution for optimizing texture rendering +- Learned what a class library is and the benefits of using one. +- Created a new MonoGame Game Library project. +- Added the library project as a reference to the game project. +- Created reusable `TextureRegion` and `TextureAtlas` classes to optimize and simplify texture management. +- Learned how to include assets in the content pipeline that should only be copied and not processed. + +In the next chapter, we'll build on the concepts of the `TextureAtlas` and explore creating the `Sprite` and `AnimatedSprite` classes to further simplify managing and rendering sprites. + +## Test Your Knowledge + +1. What is a texture swap and why can it impact performance? + +
+ Question 1 Answer + + > A texture swap occurs when the GPU needs to unbind one texture and bind another between draw calls. While individual swaps may seem trivial, they can significantly impact performance in games with many sprites as each swap is an expensive GPU operation. +

+ +2. Name a benefit of using a texture atlas. + +
+ Question 2 Answer + + > Any of the following are benefits of using a texture atlas: + > - Eliminates texture swaps by using a single texture + > - Reduces memory usage + > - Simplifies asset management + > - Improves rendering performance +

+ +3. Name an advantage of using a class library for game development. + +
+ Question 3 Answer + + > Any of the following are advantages of using a class library: + > - Reusability: The classes can be easily reused in future game projects by simply adding a reference to the library. + > - Organization: It keeps game-specific code separate from reusable library code. + > - Maintainability: Changes to the library can benefit all games that use it. + > - Testing: The library code can be tested independently of any specific game. +

+ +4. Why should we use the MonoGame Game Library template instead of a standard class library template? + +
+ Question 4 Answer + + > The MonoGame Game Library template automatically sets up the correct MonoGame framework references and configuration, saving time and ensuring compatibility. +

\ No newline at end of file diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-atlas-usage.cs b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-atlas-usage.cs new file mode 100644 index 00000000..0ce12e70 --- /dev/null +++ b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-atlas-usage.cs @@ -0,0 +1,72 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Graphics; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + private TextureRegion _slime; + private TextureRegion _bat; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + + // Load the atlas texture using the content manager + Texture2D atlasTexture = Content.Load("images/atlas"); + + // Create a TextureAtlas instance from the atlas + TextureAtlas atlas = new TextureAtlas(atlasTexture); + + // Create and add the slime and bad regions + atlas.AddRegion("slime", 0, 160, 40, 40); + atlas.AddRegion("bat", 80, 160, 40, 40); + + // Retrieve the slime and bat regions + _slime = atlas.GetRegion("slime"); + _bat = atlas.GetRegion("bat"); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(); + + // Draw the slime texture region. + _slime.Draw(_spriteBatch, Vector2.One, Color.White); + + // Draw the bat texture region 10px to the right of the slime. + _bat.Draw(_spriteBatch, new Vector2(_slime.Width + 10, 0), Color.White); + + _spriteBatch.End(); + + base.Draw(gameTime); + } +} diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-atlas-xml-usage.cs b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-atlas-xml-usage.cs new file mode 100644 index 00000000..cf75d815 --- /dev/null +++ b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-atlas-xml-usage.cs @@ -0,0 +1,65 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Graphics; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + private TextureRegion _slime; + private TextureRegion _bat; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + + // Create the texture atlas from the XML configuration file + TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml"); + + // Retrieve the slime and bat regions + _slime = atlas.GetRegion("slime"); + _bat = atlas.GetRegion("bat"); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(); + + // Draw the slime texture region. + _slime.Draw(_spriteBatch, Vector2.One, Color.White); + + // Draw the bat texture region 10px to the right of the slime. + _bat.Draw(_spriteBatch, new Vector2(_slime.Width + 10, 0), Color.White); + + _spriteBatch.End(); + + base.Draw(gameTime); + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-region-usage.cs b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-region-usage.cs new file mode 100644 index 00000000..ef5e8640 --- /dev/null +++ b/articles/tutorials/building_2d_games/06_optimizing_texture_rendering/src/Game1-texture-region-usage.cs @@ -0,0 +1,60 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Graphics; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + private TextureRegion _slime; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + + // Load the atlas texture using the content manager + Texture2D atlasTexture = Content.Load("images/atlas"); + + // create a texture region from the atlas for the slime sprite + _slime = new TextureRegion(atlasTexture, 0, 160, 40, 40); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(); + + // Draw the slime texture region + _slime.Draw(_spriteBatch, Vector2.One, Color.White); + + _spriteBatch.End(); + + base.Draw(gameTime); + } +} diff --git a/articles/tutorials/building_2d_games/07_the_sprite_class/images/slime-and-bat-rendered.png b/articles/tutorials/building_2d_games/07_the_sprite_class/images/slime-and-bat-rendered.png new file mode 100644 index 00000000..7a6fb643 Binary files /dev/null and b/articles/tutorials/building_2d_games/07_the_sprite_class/images/slime-and-bat-rendered.png differ diff --git a/articles/tutorials/building_2d_games/07_the_sprite_class/index.md b/articles/tutorials/building_2d_games/07_the_sprite_class/index.md new file mode 100644 index 00000000..e143269f --- /dev/null +++ b/articles/tutorials/building_2d_games/07_the_sprite_class/index.md @@ -0,0 +1,251 @@ +--- +title: "Chapter 07: The Sprite Class" +description: "Explore creating a reusable Sprite class to efficiently sprites and their rendering properties, including position, rotation, scale, and more." +--- + +In [Chapter 06](../06_optimizing_texture_rendering/index.md), you learned how to use texture atlases to optimize rendering performance. While this solved the issue of texture swapping, managing individual sprites and their properties becomes increasingly complex as your game grows. Even in our simple example with just a slime and a bat, we would eventually need to track various properties for each sprite: + +- Color mask for tinting. +- Origin for rotation and scale. +- Scale for size adjustments. +- Rotation for orientation. +- Sprite effects to flip horizontally and/or vertically. +- Layer depth for draw order layering. + +Imagine scaling this up to dozens of sprites, each with multiple instances on screen. Tracking all these properties through individual variables quickly becomes unmanageable. In this chapter, we'll solve this by creating a class that encapsulates sprite information and handles rendering. + +## The Sprite Class + +A sprite in our game represents a visual object created from a texture region along with its rendering properties. While multiple sprites might use the same texture region (like multiple enemies of the same type), each sprite can have unique properties that control how it appears on screen; its position, rotation, scale, and other visual characteristics. + +By creating a `Sprite` class, we can encapsulate both the texture region and its rendering parameters into a single, reusable component. This not only makes our code more organized but also makes it easier to manage multiple instances of the same type of sprite. + +In the *Graphics* directory within the *MonoGameLibrary* project, add a new file named *Sprite.cs*. Add the following code for the foundation of the `Sprite` class to the *Sprite.cs* file: + +```cs +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace MonoGameLibrary.Graphics; + +public class Sprite +{ + +} +``` + +### Properties + +The `Sprite` class will utilize properties that mirror the parameters used in [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,System.Nullable{Microsoft.Xna.Framework.Rectangle},Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,System.Single,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) so the rendering parameter for each sprite is self contained. Add the following properties: + +```cs +/// +/// Gets or Sets the source texture region represented by this sprite. +/// +public TextureRegion Region { get; set; } + +/// +/// Gets or Sets the color mask to apply when rendering this sprite. +/// +/// +/// Default value is Color.White +/// +public Color Color { get; set; } = Color.White; + +/// +/// Gets or Sets the amount of rotation, in radians, to apply when rendering this sprite. +/// +/// +/// Default value is 0.0f +/// +public float Rotation { get; set; } = 0.0f; + +/// +/// Gets or Sets the scale factor to apply to the x- and y-axes when rendering this sprite. +/// +/// +/// Default value is Vector2.One +/// +public Vector2 Scale { get; set; } = Vector2.One; + +/// +/// Gets or Sets the xy-coordinate origin point, relative to the top-left corner, of this sprite. +/// +/// +/// Default value is Vector2.Zero +/// +public Vector2 Origin { get; set; } = Vector2.Zero; + +/// +/// Gets or Sets the sprite effects to apply when rendering this sprite. +/// +/// +/// Default value is SpriteEffects.None +/// +public SpriteEffects Effects { get; set; } = SpriteEffects.None; + +/// +/// Gets or Sets the layer depth to apply when rendering this sprite. +/// +/// +/// Default value is 0.0f +/// +public float LayerDepth { get; set; } = 0.0f; + +/// +/// Gets the width, in pixels, of this sprite. +/// +/// +/// Width is calculated by multiplying the width of the source texture region by the x-axis scale factor. +/// +public float Width => Region.Width * Scale.X; + +/// +/// Gets the height, in pixels, of this sprite. +/// +/// +/// Height is calculated by multiplying the height of the source texture region by the y-axis scale factor. +/// +public float Height => Region.Height * Scale.Y; +``` + +The `TextureRegion` property works to provide the texture and source rectangle when rendering the sprite. Other properties directly correspond to [**SpriteBatch.Draw**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch.Draw(Microsoft.Xna.Framework.Graphics.Texture2D,Microsoft.Xna.Framework.Vector2,System.Nullable{Microsoft.Xna.Framework.Rectangle},Microsoft.Xna.Framework.Color,System.Single,Microsoft.Xna.Framework.Vector2,System.Single,Microsoft.Xna.Framework.Graphics.SpriteEffects,System.Single)) parameters with the same default values, making it easy to understand how each property affects the sprite's appearance. + +> [!TIP] +> The calculated `Width` and `Height` properties make it easier to position sprites relative to each other without manually applying scale factors. + +### Constructors + +The `Sprite` class will provide two ways to create a new sprite. Add the following constructors: + +```cs +/// +/// Creates a new sprite. +/// +public Sprite() { } + +/// +/// Creates a new sprite using the specified source texture region. +/// +/// The texture region to use as the source texture region for this sprite. +public Sprite(TextureRegion region) +{ + Region = region; +} +``` + +The default constructor creates an empty sprite that can be configured later, while the parameterized constructor allows you to specify the source texture region for the sprite. + +### Methods + +Finally, the `Sprite` class provides the following two methods: + +```cs +/// +/// Sets the origin of this sprite to the center +/// +public void CenterOrigin() +{ + Origin = new Vector2(Region.Width, Region.Height) * 0.5f; +} + +/// +/// Submit this sprite for drawing to the current batch. +/// +/// The SpriteBatch instance used for batching draw calls. +/// The xy-coordinate position to render this sprite at. +public void Draw(SpriteBatch spriteBatch, Vector2 position) +{ + Region.Draw(spriteBatch, position, Color, Rotation, Origin, Scale, Effects, LayerDepth); +} +``` + +- `CenterOrigin`: Sets the origin point of the sprite to its center. + + > [!NOTE] + > The origin needs to be set based on the width and height of the source texture region itself, regardless of the scale the sprite is rendered at. + +- `Draw`: Uses the `TextureRegion` property to submit the sprite for rendering using the properties of the sprite itself. + +## Create Sprites With The TextureAtlas Class + +While the `GetRegion` method of the `TextureAtlas` class we created in [Chapter 06](../06_optimizing_texture_rendering/index.md#the-textureatlas-class) works well for retrieving regions, creating sprites requires multiple steps: + +1. Get the region by name. +2. Store it in a variable. +3. Create a new sprite with that region. + +We can simplify this process by adding a sprite creation method to the `TextureAtlas` class. Open *TextureAtlas.cs* and add the following method: + +```cs +/// +/// Creates a new sprite using the region from this texture atlas with the specified name. +/// +/// The name of the region to create the sprite with. +/// A new Sprite using the texture region with the specified name. +public Sprite CreateSprite(string regionName) +{ + TextureRegion region = GetRegion(regionName); + return new Sprite(region); +} +``` + +## Using the Sprite Class + +Let's adjust our game now to use the `Sprite` class instead of just the texture regions. Replace the contents of *Game1.cs* with the following: + +[!code-csharp[](./src/Game1-sprite-usage.cs?highlight=12-13,36-38,55-59)] + +The key changes in this implementation are: + +- The `_slime` and `_bat` members were changed from `TextureRegion` to `Sprite`. +- In [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) the `_slime` and `_bat` sprites are now created using the new `TextureAtlas.CreateSprite` method. +- In [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), the draw calls were updated to use the `Sprite.Draw` method. + +Running the game now will produce the same result as in the previous chapter. + +| ![Figure 7-1: The slime and bat sprites being rendered in the upper-left corner of the game window](./images/slime-and-bat-rendered.png) | +| :---: | +| **Figure 7-1: The slime and bat sprites being rendered in the upper-left corner of the game window** | + +Try adjusting the various properties available for the slime and the bat sprites to see how they affect the rendering. + +## Conclusion + +In this chapter, we created a reusable `Sprite` class that encapsulates the properties for each sprite that we would render. The `TextureAtlas` class was updated to simplify sprite creation based on the `Sprite` class we created. + +In the next chapter, we'll build upon the `Sprite` class to create an `AnimatedSprite` class that will allow us to bring our sprites to life through animation. + +## Test Your Knowledge + +1. What is the benefit of using a Sprite class instead of managing texture regions directly? + +
+ Question 1 Answer + + > The `Sprite` class encapsulates all rendering properties (position, rotation, scale, etc.) into a single, reusable component. This makes it easier to manage multiple instances of the same type of sprite without having to track properties through individual variables. +

+ +2. Why do the `Width` and `Height` properties of a Sprite take the Scale property into account? + +
+ Question 2 Answer + + > The `Width` and `Height` properties account for scaling to make it easier to position sprites relative to each other without having to manually calculate the scaled dimensions. This is particularly useful when sprites are rendered at different scales. +

+ +3. When using the `CenterOrigin` method, why is the origin calculated using the region's dimensions rather than the sprite's scaled dimensions? + +
+ Question 3 Answer + + > The origin needs to be set based on the texture region's actual dimensions because it represents the point around which scaling and rotation are applied. Using the scaled dimensions would result in incorrect positioning since the origin would change based on the current scale factor. +

+ +4. What advantage does the `TextureAtlas.CreateSprite` method provide over using `GetRegion`? + +
+ Question 4 Answer + + > The `CreateSprite` method simplifies sprite creation by combining multiple steps (getting the region, storing it, creating a sprite) into a single method call. This reduces code repetition and makes sprite creation more straightforward. +

\ No newline at end of file diff --git a/articles/tutorials/building_2d_games/07_the_sprite_class/src/Game1-sprite-usage.cs b/articles/tutorials/building_2d_games/07_the_sprite_class/src/Game1-sprite-usage.cs new file mode 100644 index 00000000..82bad4d2 --- /dev/null +++ b/articles/tutorials/building_2d_games/07_the_sprite_class/src/Game1-sprite-usage.cs @@ -0,0 +1,65 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Graphics; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + private Sprite _slime; + private Sprite _bat; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + + // Create the texture atlas from the XML configuration file + TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml"); + + // Create the slime and bat sprites + _slime = atlas.CreateSprite("slime"); + _bat = atlas.CreateSprite("bat"); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(); + + // Draw the slime sprite + _slime.Draw(_spriteBatch, Vector2.One); + + // Draw the bat sprite 10px to the right of the slime. + _bat.Draw(_spriteBatch, new Vector2(_slime.Width + 10, 0)); + + _spriteBatch.End(); + + base.Draw(gameTime); + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/08_the_animatedsprite_class/images/bat-animation-example.gif b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/images/bat-animation-example.gif new file mode 100644 index 00000000..c23d07a3 Binary files /dev/null and b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/images/bat-animation-example.gif differ diff --git a/articles/tutorials/building_2d_games/08_the_animatedsprite_class/images/slime-bat-animated.gif b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/images/slime-bat-animated.gif new file mode 100644 index 00000000..c6948abf Binary files /dev/null and b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/images/slime-bat-animated.gif differ diff --git a/articles/tutorials/building_2d_games/08_the_animatedsprite_class/index.md b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/index.md new file mode 100644 index 00000000..27e4ebea --- /dev/null +++ b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/index.md @@ -0,0 +1,538 @@ +--- +title: "Chapter 08: The AnimatedSprite Class" +description: "Create an AnimatedSprite class that builds upon our Sprite class to support frame-based animations." +--- + +While packing images into a texture atlas and managing them through our `Sprite` class improves performance and organization, games need animation to bring their visuals to life. 2D animation in games works much like a flip book; a sequence of individual images (*frames*) displayed in rapid succession creates the illusion of movement. Each frame represents a specific point in the sprite's animation, and when these frames are cycled through quickly, our eyes perceive fluid motion. + +> [!NOTE] +> The term "frame" in animation refers to a single image in an animation sequence. This is different from a game frame, which represents one complete render cycle of your game. + +In MonoGame, we can create these animations by cycling through different regions of our texture atlas, with each region representing a single frame of the animation. For example, Figure 8-1 below shows three frames that make up a bat's wing-flapping animation: + +| ![Figure 8-1: Animation example of a bat flapping its wings](./images/bat-animation-example.gif) | +| :---: | +| **Figure 8-1: Animation example of a bat flapping its wings** | + +By drawing each frame sequentially over time, we create the illusion that the bat is flapping its wings. The speed at which we switch between frames determines how smooth or rapid the animation appears. + +In this chapter, we'll build off of the `Sprite` class we created in [Chapter 07](../07_the_sprite_class/) to create an `AnimatedSprite` class we can use to bring animations to life. + +## The Animation Class + +Before we can create animated sprites, we need a way to manage animation data. Let's create an `Animation` class to encapsulate this information. In the *Graphics* directory within the *MonoGameLibrary* project, add a new file named *Animation.cs* with this initial structure: + +```cs +using System; +using System.Collections.Generic; + +namespace MonoGameLibrary.Graphics; + +public class Animation +{ + +} +``` + +### Animation Properties + +An animation requires two key pieces of information: the sequence of frames to display and the timing between them. Let's add these properties to the `Animation` class: + +```cs +/// +/// The texture regions that make up the frames of this animation. The order of the regions within the collection +/// are the order that the frames should be displayed in. +/// +public List Frames {get; set;} + +/// +/// The amount of time to delay between each frame before moving to the next frame for this animation. +/// +public TimeSpan Delay {get; set;} +``` + +The `Frames` property stores the collection of texture regions that make up the animation sequence. The order of regions in this collection is important; they'll be displayed in the same sequence they're added, creating the animation's movement. For example, in our bat animation, the frames would be ordered to show the wings moving up, then fully extended, then down. + +The `Delay` property defines how long each frame should be displayed before moving to the next one. This timing control allows us to adjust the speed of our animations; a shorter delay creates faster animations, while a longer delay creates slower ones. + +> [!NOTE] +> Using `TimeSpan` for the delay allows us to specify precise timing intervals, making it easier to synchronize animations with game time. In other scenarios, you could opt to use `float` values instead. + +### Animation Constructors + +The `Animation` class will provide two ways to create an animation. Add the following constructors: + +```cs +/// +/// Creates a new animation. +/// +public Animation() +{ + Frames = new List(); + Delay = TimeSpan.FromMilliseconds(100); +} + +/// +/// Creates a new animation with the specified frames and delay. +/// +/// An ordered collection of the frames for this animation. +/// The amount of time to delay between each frame of this animation. +public Animation(List frames, TimeSpan delay) +{ + Frames = frames; + Delay = delay; +} +``` + +The default constructor creates an animation with an empty collection of frames and a default delay of 100 milliseconds between each frame. The parameterized constructor allows you to specify the frames of animation and the delay for the animation. + +> [!TIP] +> The default 100 milliseconds delay provides a good starting point for most animations, roughly equivalent to 10 animation frame changes per second. + +## Creating Animations With The TextureAtlas Class + +The `TextureAtlas` class we created in [Chapter 06](../06_optimizing_texture_rendering/index.md#the-textureatlas-class) can do more than just manage texture regions and create sprites; it can also store and manage animation data to create animated sprites with. The *atlas.png* image we are currently using contains the frames of animation for both a slime and a bat, as well as sprites for other things. Let's first update our *atlas-definition.xml* file to include all regions in the atlas, as well as add new `` elements to define the animations. Open the *atlas-definition.xml* file and replace the contents with the following: + +```xml + + + images/atlas + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +The key changes here are: + +- Regions have been added for all regions within the atlas. +- The slime and bat regions have been renamed to reflect the frame number of the animation. +- A new `` element has been added that defines `` elements. + +> [!NOTE] +> In the bat animation, we reuse frame "bat-1" in the sequence (bat-1, bat-2, bat-1, bat-3). This creates a smoother wing-flapping animation by returning to the neutral position between up and down wing positions. + +Now that we have a fully configured XML configuration for the atlas, we need to update the `TextureAtlas` class to manage animation data. Open the *TextureAtlas.cs* file and make the following changes: + +1. Add storage for animations + + ```cs + private Dictionary _animations; + ``` + +2. Update the constructors so that the animations dictionary is initialized: + + ```cs + /// + /// Creates a new texture atlas. + /// + public TextureAtlas() + { + _regions = new Dictionary(); + _animations = new Dictionary(); + } + + /// + /// Creates a new texture atlas instance using the given texture. + /// + /// The source texture represented by the texture atlas. + public TextureAtlas(Texture2D texture) + { + Texture = texture; + _regions = new Dictionary(); + _animations = new Dictionary(); + } + ``` + +3. Add methods to manage animations, similar to those that we use to manage regions: + + ```cs + /// + /// Adds the given animation to this texture atlas with the specified name. + /// + /// The name of the animation to add. + /// The animation to add. + public void AddAnimation(string animationName, Animation animation) + { + _animations.Add(animationName, animation); + } + + /// + /// Gets the animation from this texture atlas with the specified name. + /// + /// The name of the animation to retrieve. + /// The animation with the specified name. + public Animation GetAnimation(string animationName) + { + return _animations[animationName]; + } + + /// + /// Removes the animation with the specified name from this texture atlas. + /// + /// The name of the animation to remove. + /// true if the animation is removed successfully; otherwise, false. + public bool RemoveAnimation(string animationName) + { + return _animations.Remove(animationName); + } + ``` + +4. Update the `FromFile` method to parse the new `` animation definitions from the XML configuration file + + ```cs + /// + /// Creates a new texture atlas based a texture atlas xml configuration file. + /// + /// The content manager used to load the texture for the atlas. + /// The path to the xml file, relative to the content root directory.. + /// The texture atlas created by this method. + public static TextureAtlas FromFile(ContentManager content, string fileName) + { + TextureAtlas atlas = new TextureAtlas(); + + string filePath = Path.Combine(content.RootDirectory, fileName); + + using (Stream stream = TitleContainer.OpenStream(filePath)) + { + using (XmlReader reader = XmlReader.Create(stream)) + { + XDocument doc = XDocument.Load(reader); + XElement root = doc.Root; + + // The element contains the content path for the Texture2D to load. + // So we'll retrieve that value then use the content manager to load the texture. + string texturePath = root.Element("Texture").Value; + atlas.Texture = content.Load(texturePath); + + // The element contains individual elements, each one describing + // a different texture region within the atlas. + // + // Example: + // + // + // + // + // + // So we retrieve all of the elements then loop through each one + // and generate a new TextureRegion instance from it and add it to this atlas. + var regions = root.Element("Regions")?.Elements("Region"); + + if (regions != null) + { + foreach (var region in regions) + { + string name = region.Attribute("name")?.Value; + int x = int.Parse(region.Attribute("x")?.Value ?? "0"); + int y = int.Parse(region.Attribute("y")?.Value ?? "0"); + int width = int.Parse(region.Attribute("width")?.Value ?? "0"); + int height = int.Parse(region.Attribute("height")?.Value ?? "0"); + + if (!string.IsNullOrEmpty(name)) + { + atlas.AddRegion(name, x, y, width, height); + } + } + } + + // The element contains individual elements, each one describing + // a different animation within the atlas. + // + // Example: + // + // + // + // + // + // + // + // So we retrieve all of the elements then loop through each one + // and generate a new Animation instance from it and add it to this atlas. + var animationElements = root.Element("Animations").Elements("Animation"); + + if(animationElements != null) + { + foreach(var animationElement in animationElements) + { + string name = animationElement.Attribute("name")?.Value; + float delayInMilliseconds = float.Parse(animationElement.Attribute("delay")?.Value ?? "0"); + TimeSpan delay = TimeSpan.FromMilliseconds(delayInMilliseconds); + + List frames = new List(); + + var frameElements = animationElement.Elements("Frame"); + + if(frameElements != null) + { + foreach(var frameElement in frameElements) + { + string regionName = frameElement.Attribute("region").Value; + TextureRegion region = atlas.GetRegion(regionName); + frames.Add(region); + } + } + + Animation animation = new Animation(frames, delay); + atlas.AddAnimation(name, animation); + } + } + + return atlas; + } + } + } + ``` + +The updated `FromFile` method now handles both region and animation definitions from the XML configuration. For animations, it: + +- Reads the `` section from the XML. +- For each animation: + - Gets the name and frame delay. + - Collects the referenced texture regions. + - Creates and stores a new `Animation` instance. + +## The AnimatedSprite Class + +With our `Animation` class handling animation data, and the `TextureAtlas` updated to store the animation data, we can now create a class that represents an animated sprites. Since an animated sprite is essentially a sprite that changes its texture region over time, we can build upon our existing `Sprite` class through inheritance. + +> [!NOTE] +> By inheriting from `Sprite`, our `AnimatedSprite` class automatically gets all the rendering properties (position, rotation, scale, etc.) while adding animation-specific functionality. + +The key to this design is the `Sprite.Region` property. Our `Sprite` class already knows how to render whatever region is currently set, so our `AnimatedSprite` class just needs to update this region property to the correct animation frame at the right time. + +Let's create the initial structure for our `AnimatedSprite` class. In the *Graphics* directory within the *MonoGameLibrary* project, add a new file named *AnimatedSprite.cs*: + +```cs +using System; +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Graphics; + +public class AnimatedSprite : Sprite +{ + +} +``` + +### AnimatedSprite Members + +An animated sprite needs to track both its current animation state and timing information. Let's add the following members to the `AnimatedSprite` class: + +```cs +private int _currentFrame; +private TimeSpan _elapsed; +private Animation _animation; + +/// +/// Gets or Sets the animation for this animated sprite. +/// +public Animation Animation +{ + get => _animation; + set + { + _animation = value; + Region = _animation.Frames[0]; + } +} +``` + +The class uses three private fields to manage its animation state: + +- `_currentFrame`: Tracks which frame of the animation is currently being displayed. +- `_elapsed`: Keeps track of how much time has passed since the last frame change. +- `_animation`: Stores the current animation being played. + +The `Animation` property provides access to the current animation while ensuring the sprite always starts with the first frame when a new animation is set. When you assign a new animation, the property's setter automatically updates the sprite's region to display the first frame of that animation. + +> [!NOTE] +> Starting with the first frame when setting a new animation ensures consistent behavior when switching between different animations. + +### AnimatedSprite Constructors + +The `AnimatedSprite` class will provide two ways to create an animated sprite. Add the following constructors: + +```cs +/// +/// Creates a new animated sprite. +/// +public AnimatedSprite() { } + +/// +/// Creates a new animated sprite with the specified frames and delay. +/// +/// The animation for this animated sprite. +public AnimatedSprite(Animation animation) +{ + Animation = animation; +} +``` + +The default constructor creates an empty animated sprite that can be configured later. The parameterized constructor creates an animated sprite with a specified animation, which automatically sets the sprite's initial region to the first frame of that animation through the `Animation` property. + +> [!NOTE] +> Both constructors inherit from the base `Sprite` class, so an `AnimatedSprite` will have all the same rendering properties (position, rotation, scale, etc.) as a regular sprite. + +### AnimatedSprite Methods + +The `AnimatedSprite` class needs a way to update its animation state over time. This is handled by a single `Update` method: + +```cs +/// +/// Updates this animated sprite. +/// +/// A snapshot of the game timing values provided by the framework. +public void Update(GameTime gameTime) +{ + _elapsed += gameTime.ElapsedGameTime; + + if (_elapsed >= _animation.Delay) + { + _elapsed -= _animation.Delay; + _currentFrame++; + + if (_currentFrame >= _animation.Frames.Count) + { + _currentFrame = 0; + } + + Region = _animation.Frames[_currentFrame]; + } +} +``` + +The `Update` method manages the animation timing and frame progression: + +1. Accumulates the time passed since the last update in `_elapsed`. +2. When enough time has passed (defined by the animation's delay): + - Resets the elapsed time counter + - Advances to the next frame + - Loops back to the first frame if we've reached the end + - Updates the sprite's region to display the current frame + +> [!NOTE] +> Unlike the `Sprite` class which only needs a `Draw` method, the `AnimatedSprite` requires this additional `Update` method to handle frame changes over time. This follows MonoGame's update/draw pattern we first saw in [Chapter 03](../03_the_game1_file/index.md) + +The `Draw` method inherited from the base `Sprite` class remains unchanged, as it will automatically use whatever frame is currently set as the sprite's region. + +## Creating AnimatedSprites With The TextureAtlas Class + +Similar to the update we did to the `TextureAtlas` class in [Chapter 07](../07_the_sprite_class/index.md#create-sprites-with-the-textureatlas-class), creating an `AnimatedSprite` from the atlas would require + +1. Get the animation by name. +2. Store it in a variable. +3. Create a new animated sprite with that animation. + +We can simplify this process by adding an animated spirte creation method to the `TextureAtlas` class. Open *TextureAtlas.cs* and add the following method: + +```cs +/// +/// Creates a new animated sprite using the animation from this texture atlas with the specified name. +/// +/// The name of the animation to use. +/// A new AnimatedSprite using the animation with the specified name. +public AnimatedSprite CreateAnimatedSprite(string animationName) +{ + Animation animation = GetAnimation(animationName); + return new AnimatedSprite(animation); +} +``` + +## Using the AnimatedSprite Class + +Let's adjust our game now to use the `AnimatedSprite` class to see our sprites come to life. Replaces the contents of *Game1.cs* with the following: + +[!code-csharp[](./src/Game1-animatedsprite-usage.cs?highlight=13-14,37-41,49-51)] + +Let's examine the key changes in this implementation: + +- The `_slime` and `_bat` members were changed from `Sprite` to `AnimatedSprite`. +- In [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) the `_slime` and `_bat` sprites are now created using the new `TextureAtlas.CreateAnimatedSprite` method. +- In [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), the animations are updated based on the game time using hte `AnimatedSprite.Update` method. + +Running the game now shows both sprites animating automatically: + +- The slime bounces between two frames +- The bat's wings flap in a continuous cycle + +| ![Figure 8-2: The slime and bat sprite animating](./images/slime-bat-animated.gif) | +| :---: | +| **Figure 8-2: The slime and bat sprite animating** | + +## Conclusion + +Let's review what you accomplished in this chapter: + +- Created an `Animation` class to manage frame sequences and timing. +- Extended the `TextureAtlas` class to support animation definitions. +- Built an `AnimatedSprite` class that inherits from `Sprite`. +- Applied inheritance to add animation capabilities while maintaining existing sprite functionality. +- Used XML configuration to define animations separately from code. + +Now that we can efficiently manage and render sprites and animations, in the next chapter we'll start taking a look at user input. + +## Test Your Knowledge + +1. Why did we create a separate `Animation` class instead of putting animation properties directly in `AnimatedSprite`? + +
+ Question 1 Answer + + > Separating animation data into its own class allows multiple `AnimatedSprite` instances to share the same animation definition. This is more efficient than each sprite having its own copy of the frame sequence and timing information. +

+ +2. What is the benefit of using `TimeSpan` for animation delays instead of float values? + +
+ Question 2 Answer + + > `TimeSpan` provides precise timing control and makes it easier to synchronize animations with game time. It also makes the delay values more explicit (milliseconds vs arbitrary numbers) and helps prevent timing errors. +

+ +3. Why does the `AnimatedSprite` class need an `Update` method while the base `Sprite` class doesn't? + +
+ Question 3 Answer + + > The `AnimatedSprite` needs to track elapsed time and change frames based on the animation's timing. This requires updating its state over time, while a regular sprite's appearance remains static until explicitly changed. +

+ +4. In the `TextureAtlas` XML configuration, why might you want to reuse a frame in an animation sequence, like we did with the bat animation? + +
+ Question 4 Answer + + > Reusing frames in an animation sequence can create smoother animations by providing transition states. In the bat animation, reusing the neutral position (bat-1) between wing movements creates a more natural flapping motion without requiring additional sprite frames. +

\ No newline at end of file diff --git a/articles/tutorials/building_2d_games/08_the_animatedsprite_class/src/Game1-animatedsprite-usage.cs b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/src/Game1-animatedsprite-usage.cs new file mode 100644 index 00000000..721c1901 --- /dev/null +++ b/articles/tutorials/building_2d_games/08_the_animatedsprite_class/src/Game1-animatedsprite-usage.cs @@ -0,0 +1,72 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using MonoGameLibrary.Graphics; + +namespace MonoGameSnake; + +public class Game1 : Game +{ + private GraphicsDeviceManager _graphics; + private SpriteBatch _spriteBatch; + private AnimatedSprite _slime; + private AnimatedSprite _bat; + + public Game1() + { + _graphics = new GraphicsDeviceManager(this); + Content.RootDirectory = "Content"; + IsMouseVisible = true; + } + + protected override void Initialize() + { + // TODO: Add your initialization logic here + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + + // Create the texture atlas from the XML configuration file + TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml"); + + // Create the slime animated sprite + _slime = atlas.CreateAnimatedSprite("slime-animation"); + + // Create the bat animated sprite + _bat = atlas.CreateAnimatedSprite("bat-animation"); + } + + protected override void Update(GameTime gameTime) + { + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape)) + Exit(); + + // Update the slime and bat animated sprites + _slime.Update(gameTime); + _bat.Update(gameTime); + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + GraphicsDevice.Clear(Color.CornflowerBlue); + + _spriteBatch.Begin(samplerState: SamplerState.PointClamp); + + // Draw the slime animated sprite + _slime.Draw(_spriteBatch, Vector2.One); + + // Draw the bat animated sprite 10px to the right of the slime. + _bat.Draw(_spriteBatch, new Vector2(_slime.Width + 10, 0)); + + _spriteBatch.End(); + + base.Draw(gameTime); + } +} \ No newline at end of file diff --git a/articles/tutorials/building_2d_games/09_handling_input/images/ps-controller-back.svg b/articles/tutorials/building_2d_games/09_handling_input/images/ps-controller-back.svg new file mode 100644 index 00000000..4a9f39ba --- /dev/null +++ b/articles/tutorials/building_2d_games/09_handling_input/images/ps-controller-back.svg @@ -0,0 +1,206 @@ + + + +Right ShoulderLeft ShoulderLeft TriggerRight Trigger diff --git a/articles/tutorials/building_2d_games/09_handling_input/images/ps-controller-front.svg b/articles/tutorials/building_2d_games/09_handling_input/images/ps-controller-front.svg new file mode 100644 index 00000000..cb6b268f --- /dev/null +++ b/articles/tutorials/building_2d_games/09_handling_input/images/ps-controller-front.svg @@ -0,0 +1,3 @@ + + +xLeft ThumbstickRight ShoulderLeft ShoulderXBAYStartBackDPadRight Thumbstick diff --git a/articles/tutorials/building_2d_games/09_handling_input/images/xbox-controller-back.svg b/articles/tutorials/building_2d_games/09_handling_input/images/xbox-controller-back.svg new file mode 100644 index 00000000..d00541b2 --- /dev/null +++ b/articles/tutorials/building_2d_games/09_handling_input/images/xbox-controller-back.svg @@ -0,0 +1,129 @@ + + + +Left ShoulderRight ShoulderLeft TriggerRight Trigger diff --git a/articles/tutorials/building_2d_games/09_handling_input/images/xbox-controller-front.svg b/articles/tutorials/building_2d_games/09_handling_input/images/xbox-controller-front.svg new file mode 100644 index 00000000..338ce85f --- /dev/null +++ b/articles/tutorials/building_2d_games/09_handling_input/images/xbox-controller-front.svg @@ -0,0 +1,2 @@ + +Left ThumbstickRight ThumbstickDPadBackStartYABXLeft ShoulderRight ShoulderABXY diff --git a/articles/tutorials/building_2d_games/09_handling_input/index.md b/articles/tutorials/building_2d_games/09_handling_input/index.md new file mode 100644 index 00000000..393832d2 --- /dev/null +++ b/articles/tutorials/building_2d_games/09_handling_input/index.md @@ -0,0 +1,671 @@ +--- +title: "Chapter 09: 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.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: + +```cs +KeyboardState keyboardState = Keyboard.GetState(); + +if(keyboardState.IsKeyDown(Keys.Space)) +{ + // The space key is down, so do something. +} +``` + +> [!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. + +### Implementing Keyboard Input + +Let's implement keyboard controls to move our slime sprite around the screen. Open the *Game1.cs* file and perform the following: + +1. First, add the following fields to track the position of the slime and movement speed: + + ```cs + private Vector2 _slimePosition; + private const float MOVEMENT_SPEED = 5.0f; + ``` + +2. Next, add the following method which checks if the up, down, left, or right arrow keys are pressed, and if any of them are, adjusts the slime's position: + + ```cs + private void HandleKeyboardInput() + { + KeyboardState keyboardState = Keyboard.GetState(); + + if(keyboardState.IsKeyDown(Keys.Up)) + { + _slimePosition.Y -= MOVEMENT_SPEED; + } + + if(keyboardState.IsKeyDown(Keys.Down)) + { + _slimePosition.Y += MOVEMENT_SPEED; + } + + if(keyboardState.IsKeyDown(Keys.Left)) + { + _slimePosition.X -= MOVEMENT_SPEED; + } + + if(keyboardState.IsKeyDown(Keys.Right)) + { + _slimePosition.X += MOVEMENT_SPEED; + } + } + ``` + + > [!IMPORTANT] + > Why are we subtracting from the Y position when moving up instead of adding? Recall from [Chapter 05](../05_working_with_textures/index.md#drawing-a-texture) that MonoGame uses a coordinate system where the Y value **increases** moving down. So in order to move **up** the screen, we need to reduce the Y value. + +3. Next, in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), call the new `HandleKeyboardInput` method just before the call to `base.Update`: + + ```cs + HandleKeyboardInput(); + ``` + +4. Finally, in [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), update the position of the slime when it is rendered by using the `_slimePosition` value: + + ```cs + _slime.Draw(_spriteBatch, _slimePosition); + ``` + +Running the game now, you can move the slime sprite around using the arrow keys on the keyboard. Try it out! + +## 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 + +```cs +MouseState mouseState = Mouse.GetState(); + +if(mouseState.LeftButton == ButtonState.Pressed) +{ + // The left button is down, so do something. +} +``` + +### Implementing Mouse Input + +Let's implement mouse controls to move the bat sprite around the screen to the point that the cursor is clicked at. Open *Game1.cs* and perform the following: + +1. First, add the following field to track the position of the bat: + + ```cs + private Vector2 _batPosition; + ``` + +2. Next, in [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) set the initial position of the bat to where it is currently drawn, 10px to the right of the slime. Add the following **after** the call to `base.Initialize()` + + ```cs + _batPosition = new Vector2(_slime.Width + 10, 0); + ``` + + > [!IMPORTANT] + > Notice that we set the value of the bat position **after** the call to `base.Initialize`. Recall from Chapter 03 in the [Content Loading](../03_the_game1_file/index.md#content-loading), that the [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method is called during the `base.Initialize()` call. Since we are creating the slime sprite inside of [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) we need to ensure it's been created before we can use the `Width` property of the slime to set the position of the bat in [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize). + > + > We could have just as easily set the bat's position inside the [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) method after creating the slime, but I wanted to demonstrate the importance of the call order relationship between [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) and [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) + +3. Next, add the following method which checks if the left mouse button is pressed, and if so, adjusts the bat's position to the position of the mouse cursor: + + ```cs + private void HandleMouseInput() + { + MouseState mouseState = Mouse.GetState(); + + if(mouseState.LeftButton == ButtonState.Pressed) + { + _batPosition = mouseState.Position.ToVector2(); + } + } + ``` + +4. Next, in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), call the new `HandleMouseInput` method after the `HandleKeyboardInput` call: + + ```cs + HandleMouseInput(); + ``` + +5. Finally, in [**Draw**](xref:Microsoft.Xna.Framework.Game.Draw(Microsoft.Xna.Framework.GameTime)), update the position of the bat when it is rendered by using the `_batPosition` value: + + ```cs + _bat.Draw(_spriteBatch, _batPosition); + ``` + +Running the game now, you can move the bat sprite around by clicking the left mouse button on the game screen and it will move to that position. Try it out! + +> [!NOTE] +> When the bat moves to the position of the mouse cursor, notice that it does so relative to the upper-left corner of the bat sprite and not the center of the sprite. This is because the `Origin` of the bat sprite is [**Vector2.Zero**](xref:Microsoft.Xna.Framework.Vector2.Zero) (upper-left) corner by default. + +## 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: + +```cs +GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); + +if(gamePadState.Buttons.A == ButtonState.Pressed) +{ + // Button A is pressed, do something. +} +``` + +#### 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: + +```cs +GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); + +if(gamePadState.DPad.Down == ButtonState.Pressed) +{ + // DPad down is pressed, do something. +} +``` + +#### 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 + +```cs +GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); + +Vector2 leftStick = gamePadState.Thumbsticks.Left; +leftStick.Y *= -1.0f; + +sprite.Position += leftStick; +``` + +> [!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 05](../05_working_with_textures/index.md#drawing-a-texture) and in the [Implementing Keyboard Input](#implementing-keyboard-input) section above. +> +> 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: + +```cs +float acceleration = GamePad.GetState(PlayerIndex.One).Triggers.Right; +``` + +### 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: + +```cs +GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); + +if(gamePadState.IsButtonDown(Buttons.A)) +{ + // The A button is pressed, do something. +} +``` + +### Implementing GamePad Input + +Let's implement gamepad controls as an alternative method of moving the slime sprite around. Open the *Game1.cs* file and perform the following: + +1. Add the following method which takes the value of the left thumbstick and uses it to adjust the sprite's position: + + ```cs + private void HandleGamepadInput() + { + GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); + + _slimePosition.X += gamePadState.ThumbSticks.Left.X * MOVEMENT_SPEED; + _slimePosition.Y -= gamePadState.ThumbSticks.Left.Y * MOVEMENT_SPEED; + } + ``` + + > [!TIP] + > Since the value of the thumbstick is a range between `1.0f` and `1.0f`, we can multiply those values by the `MOVEMENT_SPEED`. This will make the slime move slower or faster depending on how far in the direction the thumbstick is pushed. + +2. Next, in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), call the new `HandleGamepadInput` method after the `HandleMouseInput` call: + + ```cs + HandleGamepadInput(); + ``` + +Running the game now, you can move the slime sprite around using the left thumbstick on your gamepad. Try it out! Notice that the more you push the thumbstick in a particular direction, the faster the slime moves up to the movement speed cap. + +### GamePad Vibration + +Another thing we can do with a gamepad is tell it to vibrate. To do this, the [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad) class has a [**SetVibration**](xref:Microsoft.Xna.Framework.Input.GamePad.SetVibration(Microsoft.Xna.Framework.PlayerIndex,System.Single,System.Single)) method that requires the player index, and the speed of the left and right vibration motors. The speed can be any value from `0.0f` (no vibration) to `1.0f` (full vibration). + +Let's adjust the current code so that when the A button is pressed on the gamepad, it gives a slight speed boost to the slime as it moves. When moving with a speed boost, we can apply vibration to the gamepad as feedback to the player. Update the `HandleGamePadInput` method to the following: + +```cs +private void HandleGamepadInput() +{ + GamePadState gamePadState = GamePad.GetState(PlayerIndex.One); + + if (gamePadState.Buttons.A == ButtonState.Pressed) + { + _slimePosition.X += gamePadState.ThumbSticks.Left.X * 1.5f * MOVEMENT_SPEED; + _slimePosition.Y -= gamePadState.ThumbSticks.Left.Y * 1.5f * MOVEMENT_SPEED; + GamePad.SetVibration(PlayerIndex.One, 1.0f, 1.0f); + } + else + { + _slimePosition.X += gamePadState.ThumbSticks.Left.X * MOVEMENT_SPEED; + _slimePosition.Y -= gamePadState.ThumbSticks.Left.Y * MOVEMENT_SPEED; + GamePad.SetVibration(PlayerIndex.One, 0.0f, 0.0f); + } +} +``` + +Running the game now, when you press the A button, the slime sprite will move slightly faster and you can feel the vibration. Try it out! + +## 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: + +```cs +TouchCollection touchCollection = TouchPanel.GetState(); + +foreach(TouchLocation touchLocation in touchCollection) +{ + if(touchLocation.State == TouchLocationState.Pressed || touchLocation.State == TouchLocationState.Moved) + { + // The the location at touchLocation.Position is currently being pressed, + // so we can act on that information. + } +} +``` + +> [!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: +> +> ```cs +> protected override void Initialize() +> { +> base.Initialize(); +> +> // Enable gestures we want to handle +> TouchPanel.EnabledGestures = +> GestureType.Tap | +> GestureType.HorizontalDrag | +> GestureType.VerticalDrag; +> } + +The following is an example of using a gesture to detect horizontal and vertical drags: + +```cs +while(TouchPanel.IsGestureAvailable) +{ + GestureSample gesture = TouchPanel.ReadGesture(); + + if(gesture.GestureType == GestureType.HorizontalDrag) + { + // A horizontal drag from left-to-right or right-to-left occurred. + // You can use the Delta property to determine how much movement + // occurred during the swipe. + float xDragAmount = gesture.Delta.X; + + // Now do something with that information. + } + + if(gesture.GestureType == GestureType.VerticalDrag) + { + // A vertical drag from top-to-bottom or bottom-to-top occurred. + // You can use the Delta property to determine how much movement + // occurred during the swipe. + float yDragAmount = gesture.Delta.Y; + + // Now do something with that information. + } +} +``` + +> [!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 TouchPanel Input (Optional) + +> [!NOTE] +> This section is optional. This tutorial does not go into detail on creating mobile projects where a touch screen would be available for input. However, the following code is implemented as a reference. + +Let's implement touch controls to move the bat sprite around the screen to the point that the screen is touched, similar to what we did for mouse controls in the [Implementing Mouse Input](#implementing-mouse-input) section above. Open *Game1.cs* and perform the following: + +1. Add the following method which checks for a touch location and moves the bat sprite to that location if a touch occurs: + + ```cs + private void HandleTouchInput() + { + TouchCollection touchCollection = TouchPanel.GetState(); + + if (touchCollection.Count > 0) + { + TouchLocation touchLocation = touchCollection[0]; + _batPosition = touchLocation.Position; + } + } + ``` + +2. Next, in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), call the new `HandleTouchInput` method after the `HandleGamePadInput` call: + + ```cs + HandleTouchInput() + ``` + +If you have your development environment setup for mobile development, running the game now, you can touch the screen to move the bat to the point that was touched. + +## 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 1 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 2 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 3 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 4 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 5 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 6 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 7 Answer + + > Touch input can handle multiple simultaneous touch points through the [**TouchCollection**](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 8 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_input_management/index.md b/articles/tutorials/building_2d_games/10_input_management/index.md new file mode 100644 index 00000000..3927b21f --- /dev/null +++ b/articles/tutorials/building_2d_games/10_input_management/index.md @@ -0,0 +1,1004 @@ +--- +title: "Chapter 10: Input Management" +description: "Learn how to create an input management system to handle keyboard, mouse, and gamepad input, including state tracking between frames and creating a reusable framework for handling player input." +--- + +In [Chapter 09](../09_handling_input/index.md), you learned how to handle input from various devices like keyboard, mouse, and gamepad. While checking if an input is currently down works well for continuous actions like movement, many game actions should only happen once when an input is first pressed; think firing a weapon or jumping. To handle these scenarios, we need to compare the current input state with the previous frame's state to detect when an input changes from up to down. + +In this chapter you will: + +- Learn the difference between an input being down versus being pressed +- Track input states between frames +- Create a reusable input management system +- Simplify handling input across multiple devices + +Let's start by understanding the concept of input state changes and how we can detect them. + +## Understanding Input States + +When handling input in games, there are two key scenarios we need to consider: + +- An input is being held down (like holding a movement key). +- An input was just pressed for one frame (like pressing a jump button). + +Let's look at the difference using keyboard input as an example. With our current implementation, we can check if a key is down using [**KeyboardState.IsKeyDown**](xref:Microsoft.Xna.Framework.Input.KeyboardState.IsKeyDown(Microsoft.Xna.Framework.Input.Keys)): + +```cs +KeyboardState keyboardState = Keyboard.GetState(); + +if (keyboardState.IsKeyDown(Keys.Space)) +{ + // This runs EVERY frame the space key is held down +} +``` + +However, many game actions shouldn't repeat while a key is held. For instance, if the Space key makes your character jump, you probably don't want them to jump repeatedly just because the player is holding the key down. Instead, you want the jump to happen only on the first frame when Space is pressed. + +To detect this "just pressed" state, we need to compare two states: + +1. Is the key down in the current frame? +2. Was the key up in the previous frame? + +If both conditions are true, we know the key was just pressed. If we were to modify the above code to track the previous keyboard state it would look something like this: + +```cs +private KeyboardState _previousKeyboardState; + +protected override void Update(GameTime gameTime) +{ + KeyboardState keyboardState = Keyboard.GetState(); + + if (keyboardState.IsKeyDown(Keys.Space) && _previousKeyboardState.IsKeyUp(Keys.Space)) + { + // This will only run on the first frame Space is pressed and will not + // happen again until it has been released and then pressed again. + } + + _previousKeyboardState = keyboardState; + base.Update(gameTime); +} +``` + +This same concept applies to mouse buttons and gamepad input as well. Any time you need to detect a "just pressed" or "just released" state, you'll need to compare the current input state with the previous frame's state. + +So far, we've only been working with our game within the *Game1.cs* file. This has been fine for the examples given. Overtime, as the game grows, we're going to have a more complex system setup with different scenes, and each scene will need a way to track the state of input over time. We could do this by creating a lot of variables in each scene to track this information, or we can use object-oriented design concepts to create a reusable `InputManager` class to simplify this for us. + +Before we create the `InputManager` class, let's first create classes for the keyboard, mouse, and gamepad that encapsulates the information about those inputs which will then be exposed through the `InputManager`. + +To get started, create a new directory called *Input* in the *MonoGameLibrary* project. We'll put all of our input related classes here. + +## The KeyboardInfo Class + +Let's start our input management system by creating a class to handle keyboard input. The `KeyboardInfo` class will encapsulate all keyboard-related functionality, making it easier to: + +- Track current and previous keyboard states +- Detect when keys are pressed or released +- Check if keys are being held down + +In the *Input* directory of the *MonoGameLibrary* project, add a new file named *KeyboardInfo.cs* with this initial structure: + +```cs +using Microsoft.Xna.Framework.Input; + +namespace MonoGameLibrary.Input; + +public class KeyboardInfo +{ + +} +``` + +### KeyboardInfo Properties + +To detect changes in keyboard input between frames, we need to track both the previous and current keyboard states. Add these properties to the `KeyboardInfo` class: + +```cs +/// +/// Gets the state of keyboard input during the previous update cycle. +/// +public KeyboardState PreviousState { get; private set; } + +/// +/// Gets the state of keyboard input during the current input cycle. +/// +public KeyboardState CurrentState { get; private set; } +``` + +> [!NOTE] +> These properties use a public getter but private setter pattern. This allows other parts of the game to read the keyboard states if needed, while ensuring only the `KeyboardInfo` class can update them. + +### KeyboardInfo Constructor + +The `KeyboardInfo` class needs a constructor to initialize the keyboard states. Add this constructor: + +```cs +/// +/// Creates a new KeyboardInfo +/// +public KeyboardInfo() +{ + PreviousState = new KeyboardState(); + CurrentState = Keyboard.GetState(); +} +``` + +The constructor: + +- Creates an empty state for `PreviousState` since there is no previous input yet +- Gets the current keyboard state as our starting point for `CurrentState` + +This initialization ensures we have valid states to compare against in the first frame of our game, preventing any potential null reference issues when checking for input changes. + +### KeyboardInfo Methods + +The `KeyboardInfo` class needs methods both for updating states and checking key states. Let's start with our update method: + +```cs +/// +/// Updates the state information about keyboard input. +/// +public void Update() +{ + PreviousState = CurrentState; + CurrentState = Keyboard.GetState(); +} +``` + +> [!NOTE] +> Each time `Update` is called, the current state becomes the previous state, and we get a fresh current state. This creates our frame-to-frame comparison chain. + +Next, we'll add methods to check various key states: + +```cs +/// +/// Returns a value that indicates if the specified key is currently down. +/// +/// The key to check. +/// true if the specified key is currently down; otherwise, false. +public bool IsKeyDown(Keys key) +{ + return CurrentState.IsKeyDown(key); +} + +/// +/// Returns a value that indicates whether the specified key is currently up. +/// +/// The key to check. +/// true if the specified key is currently up; otherwise, false. +public bool IsKeyUp(Keys key) +{ + return CurrentState.IsKeyUp(key); +} + +/// +/// Returns a value that indicates if the specified key was just pressed on the current frame. +/// +/// The key to check. +/// true if the specified key was just pressed on the current frame; otherwise, false. +public bool WasKeyJustPressed(Keys key) +{ + return CurrentState.IsKeyDown(key) && PreviousState.IsKeyUp(key); +} + +/// +/// Returns a value that indicates if the specified key was just released on the current frame. +/// +/// The key to check. +/// true if the specified key was just released on the current frame; otherwise, false. +public bool WasKeyJustReleased(Keys key) +{ + return CurrentState.IsKeyUp(key) && PreviousState.IsKeyDown(key); +} +``` + +These methods serve two distinct purposes. For checking continuous states: + +- `IsKeyDown`: Returns true as long as a key is being held down. +- `IsKeyUp`: Returns true as long as a key is not being pressed. + +And for detecting state changes: + +- `WasKeyJustPressed`: Returns true only on the frame when a key changes from up-to-down. +- `WasKeyJustReleased`: Returns true only on the frame when a key changes from down-to-up. + +> [!TIP] +> Use continuous state checks (`IsKeyDown`/`IsKeyUp`) for actions that should repeat while a key is held, like movement. Use single-frame checks (`WasKeyJustPressed`/`WasKeyJustReleased`) for actions that should happen once per key press, like jumping or shooting. + +That's it for the `KeyboardInfo` class, let's move on to mouse input next. + +## MouseButton Enum + +Recall from the [Mouse Input](../09_handling_input/index.md#mouse-input) section of the previous chapter that the [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) struct provides button states through properties rather than methods like `IsButtonDown`/`IsButtonUp`. To keep our input management API consistent across devices, we'll create a `MouseButton` enum that lets us reference mouse buttons in a similar way to how we use [**Keys**](xref:Microsoft.Xna.Framework.Input.Keys) for keyboard input and [**Buttons**](xref:Microsoft.Xna.Framework.Input.Buttons) for gamepad input. + +In the *Input* directory of the *MonoGameLibrary* project, add a new file named *MouseButton.cs* with the following code: + +```cs +namespace MonoGameLibrary.Input; + +public enum MouseButton +{ + Left, + Middle, + Right, + XButton1, + XButton2 +} +``` + +> [!NOTE] +> Each enum value corresponds directly to a button property in MouseState: +> +> - `Left`: Maps to [**MouseState.LeftButton**](xref:Microsoft.Xna.Framework.Input.MouseState.LeftButton). +> - `Middle`: Maps to [**MouseState.MiddleButton**](xref:Microsoft.Xna.Framework.Input.MouseState.MiddleButton). +> - `Right`: Maps to [**MouseState.RightButton**](xref:Microsoft.Xna.Framework.Input.MouseState.RightButton). +> - `XButton1`: Maps to [**MouseState.XButton1**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton1). +> - `XButton2`: Maps to [**MouseState.XButton2**](xref:Microsoft.Xna.Framework.Input.MouseState.XButton2). + +## The MouseInfo Class + +To manage mouse input effectively, we need to track both current and previous states, as well as provide easy access to mouse position, scroll wheel values, and button states. The `MouseInfo` class will encapsulate all of this functionality, making it easier to: + +- Track current and previous mouse states. +- Track the mouse position. +- Check the change in mouse position between frames and if it was moved. +- Track scroll wheel changes. +- Detect when mouse buttons are pressed or released +- Check if mouse buttons are being held down + +Let's create this class in the *Input* directory of the *MonoGameLibrary* project. Add a new file named *MouseInfo.cs* with the following initial structure: + +```cs +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGameLibrary.Input; + +public class MouseInfo +{ + +} +``` + +### MouseInfo Properties + +The `MouseInfo` class needs properties to track both mouse states and provide easy access to common mouse information. Let's add these properties. + +First, we need properties for tracking mouse states: + +```cs +/// +/// The state of mouse input during the previous update cycle. +/// +public MouseState PreviousState { get; private set; } + +/// +/// The state of mouse input during the current update cycle. +/// +public MouseState CurrentState { get; private set; } +``` + +Next, we'll add properties for handling cursor position: + +```cs +/// +/// Gets or Sets the current position of the mouse cursor in screen space. +/// +public Point Position +{ + get => CurrentState.Position; + set => SetPosition(value.X, value.Y); +} + +/// +/// Gets or Sets the current x-coordinate position of the mouse cursor in screen space. +/// +public int X +{ + get => CurrentState.X; + set => SetPosition(value, CurrentState.Y); +} + +/// +/// Gets or Sets the current y-coordinate position of the mouse cursor in screen space. +/// +public int Y +{ + get => CurrentState.Y; + set => SetPosition(CurrentState.X, value); +} + +/// +/// Gets a value that indicates if the mouse cursor moved between the previous and current frames. +/// +public bool WasMoved => CurrentState.X != PreviousState.X || CurrentState.Y != PreviousState.Y; +``` + +> [!NOTE] +> The position properties use a `SetPosition` method that we'll implement later. This method will handle the actual cursor positioning on screen. + +These properties provide different ways to work with the cursor position: + +- `Position`: Gets/sets the cursor position as a [**Point**](xref:Microsoft.Xna.Framework.Point). +- `X`: Gets/sets just the horizontal position. +- `Y`: Gets/sets just the vertical position. + +Next, we'll add properties for determining if the mouse cursor moved between game frames and if so how much: + +```cs +/// +/// Gets the difference in the mouse cursor position between the previous and current frame. +/// +public Point PositionDelta => CurrentState.Position - PreviousState.Position; + +/// +/// Gets the difference in the mouse cursor x-position between the previous and current frame. +/// +public int XDelta => CurrentState.X - PreviousState.X; + +/// +/// Gets the difference in the mouse cursor y-position between the previous and current frame. +/// +public int YDelta => CurrentState.Y - PreviousState.Y; + +/// +/// Gets a value that indicates if the mouse cursor moved between the previous and current frames. +/// +public bool WasMoved => PositionDelta != Point.Zero; +``` + +The properties provide different ways of detecting mouse movement between frames: + +- `PositionDelta`: Gets how much the cursor moved between frames as a [**Point**](xref:Microsoft.Xna.Framework.Point). +- `XDelta`: Gets how much the cursor moved horizontally between frames. +- `YDelta`: Gets how much the cursor moved vertically between frames. +- `WasMoved`: Indicates if the cursor moved between frames. + +Finally, we'll add properties for handling the scroll wheel: + +```cs +/// +/// Gets the cumulative value of the mouse scroll wheel since the start of the game. +/// +public int ScrollWheel => CurrentState.ScrollWheelValue; + +/// +/// Gets the value of the scroll wheel between the previous and current frame. +/// +public int ScrollWheelDelta => CurrentState.ScrollWheelValue - PreviousState.ScrollWheelValue; +``` + +The scroll wheel properties serve different purposes: + +- `ScrollWheel`: Gets the total accumulated scroll value since game start. +- `ScrollWheelDelta`: Gets the change in scroll value just in this frame. + +> [!TIP] +> Use `ScrollWheelDelta` when you need to respond to how much the user just scrolled, rather than tracking the total scroll amount. + +### MouseInfo Constructor + +The `KeyboardInfo` class needs a constructor to initialize the mouse states. Add this constructor: + +```cs +/// +/// Creates a new MouseInfo. +/// +public MouseInfo() +{ + PreviousState = new MouseState(); + CurrentState = Mouse.GetState(); +} +``` + +The constructor: + +- Creates an empty state for `PreviousState` since there is no previous input yet. +- Gets the current mouse state as our starting point for `CurrentState`. + +This initialization ensures we have valid states to compare against in the first frame of our game, preventing any potential null reference issues when checking for input changes. + +### MouseInfo Methods + +The `MouseInfo` class needs methods for updating states, checking button states, and setting the cursor position. Let's start with our update method: + +```cs +/// +/// Updates the state information about mouse input. +/// +public void Update() +{ + PreviousState = CurrentState; + CurrentState = Mouse.GetState(); +} +``` + +Next, we'll add methods to check various button states: + +```cs +/// +/// Returns a value that indicates whether the specified mouse button is currently down. +/// +/// The mouse button to check. +/// true if the specified mouse button is currently down; otherwise, false. +public bool IsButtonDown(MouseButton button) +{ + switch (button) + { + case MouseButton.Left: + return CurrentState.LeftButton == ButtonState.Pressed; + case MouseButton.Middle: + return CurrentState.MiddleButton == ButtonState.Pressed; + case MouseButton.Right: + return CurrentState.RightButton == ButtonState.Pressed; + case MouseButton.XButton1: + return CurrentState.XButton1 == ButtonState.Pressed; + case MouseButton.XButton2: + return CurrentState.XButton2 == ButtonState.Pressed; + default: + return false; + } +} + +/// +/// Returns a value that indicates whether the specified mouse button is current up. +/// +/// The mouse button to check. +/// true if the specified mouse button is currently up; otherwise, false. +public bool IsButtonUp(MouseButton button) +{ + switch (button) + { + case MouseButton.Left: + return CurrentState.LeftButton == ButtonState.Released; + case MouseButton.Middle: + return CurrentState.MiddleButton == ButtonState.Released; + case MouseButton.Right: + return CurrentState.RightButton == ButtonState.Released; + case MouseButton.XButton1: + return CurrentState.XButton1 == ButtonState.Released; + case MouseButton.XButton2: + return CurrentState.XButton2 == ButtonState.Released; + default: + return false; + } +} + +/// +/// Returns a value that indicates whether the specified mouse button was just pressed on the current frame. +/// +/// The mouse button to check. +/// true if the specified mouse button was just pressed on the current frame; otherwise, false. +public bool WasButtonJustPressed(MouseButton button) +{ + switch (button) + { + case MouseButton.Left: + return CurrentState.LeftButton == ButtonState.Pressed && PreviousState.LeftButton == ButtonState.Released; + case MouseButton.Middle: + return CurrentState.MiddleButton == ButtonState.Pressed && PreviousState.MiddleButton == ButtonState.Released; + case MouseButton.Right: + return CurrentState.RightButton == ButtonState.Pressed && PreviousState.RightButton == ButtonState.Released; + case MouseButton.XButton1: + return CurrentState.XButton1 == ButtonState.Pressed && PreviousState.XButton1 == ButtonState.Released; + case MouseButton.XButton2: + return CurrentState.XButton2 == ButtonState.Pressed && PreviousState.XButton2 == ButtonState.Released; + default: + return false; + } +} + +/// +/// Returns a value that indicates whether the specified mouse button was just released on the current frame. +/// +/// The mouse button to check. +/// true if the specified mouse button was just released on the current frame; otherwise, false.F +public bool WasButtonJustReleased(MouseButton button) +{ + switch (button) + { + case MouseButton.Left: + return CurrentState.LeftButton == ButtonState.Released && PreviousState.LeftButton == ButtonState.Pressed; + case MouseButton.Middle: + return CurrentState.MiddleButton == ButtonState.Released && PreviousState.MiddleButton == ButtonState.Pressed; + case MouseButton.Right: + return CurrentState.RightButton == ButtonState.Released && PreviousState.RightButton == ButtonState.Pressed; + case MouseButton.XButton1: + return CurrentState.XButton1 == ButtonState.Released && PreviousState.XButton1 == ButtonState.Pressed; + case MouseButton.XButton2: + return CurrentState.XButton2 == ButtonState.Released && PreviousState.XButton2 == ButtonState.Pressed; + default: + return false; + } +} +``` + +These methods serve two distinct purposes. For checking continuous states: + + +- `IsKeyDown`: Returns true as long as a key is being held down. +- `IsKeyUp`: Returns true as long as a key is not being pressed. + +And for detecting state changes: + +- `WasKeyJustPressed`: Returns true only on the frame when a key changes from up-to-down. +- `WasKeyJustReleased`: Returns true only on the frame when a key changes from down-to-up. + +> [!NOTE] +> Each method uses a switch statement to check the appropriate button property from the [**MouseState**](xref:Microsoft.Xna.Framework.Input.MouseState) based on which `MouseButton` enum value is provided. This provides a consistent API while handling the different button properties internally. + +Finally, we need a method to handle setting the cursor position: + +```cs +/// +/// Sets the current position of the mouse cursor in screen space and updates the CurrentState with the new position. +/// +/// The x-coordinate location of the mouse cursor in screen space. +/// The y-coordinate location of the mouse cursor in screen space. +public void SetPosition(int x, int y) +{ + Mouse.SetPosition(x, y); + CurrentState = new MouseState( + x, + y, + CurrentState.ScrollWheelValue, + CurrentState.LeftButton, + CurrentState.MiddleButton, + CurrentState.RightButton, + CurrentState.XButton1, + CurrentState.XButton2 + ); +} +``` + +> [!TIP] +> Notice that after setting the position, we immediately update the `CurrentState`. This ensures our state tracking remains accurate even when manually moving the cursor. + +That's it for the `MouseInfo` class, next we'll move onto gamepad input. + +## The GamePadInfo Class + +To manage gamepad input effectively, we need to track both current and previous states, is the gamepad still connected, as well as provide easy access to the thumbstick values, trigger values, and button states. The `GamePadInfo` class will encapsulate all of this functionality, making it easier to: + +- Track current and previous gamepad states. +- Check if the gamepad is still connected. +- Track the position of the left and right thumbsticks. +- Check the values of the left and right triggers. +- Detect when gamepad buttons are pressed or released. +- Check if gamepad buttons are being held down. +- Start and Stop vibration of a gamepad. + +Let's create this class in the *Input* directory of the *MonoGameLibrary* project. Add a new file named *GamePadInfo.cs* with the following initial structure: + +```cs +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Input; + +namespace MonoGameLibrary.Input; + +public class GamePadInfo +{ + +} +``` + +### GamePadInfo Properties + +We use vibration in gamepads to provide haptic feedback to the player. The [**GamePad**](xref:Microsoft.Xna.Framework.Input.GamePad) class provides the [**SetVibration**](xref:Microsoft.Xna.Framework.Input.GamePad.SetVibration(Microsoft.Xna.Framework.PlayerIndex,System.Single,System.Single)) method to tell the gamepad to vibrate, but it does not provide a timing mechanism for it if we wanted to only vibrate for a certain period of time. Add the following private field to the `GamePadInfo` class: + +```cs +private TimeSpan _vibrationTimeRemaining = TimeSpan.Zero; +``` + +Recall from the [previous chapter](../09_handling_input/index.md#gamepad-input) that a [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) value needs to be supplied when calling [**Gamepad.GetState**](xref:Microsoft.Xna.Framework.Input.GamePad.GetState(Microsoft.Xna.Framework.PlayerIndex)). Doing this returns the state of the gamepad connected at that player index. So we'll need a property to track the player index this gamepad info is for. + +```cs +/// +/// Gets the index of the player this gamepad is for. +/// +public PlayerIndex PlayerIndex { get; } +``` + +To detect changes in the gamepad input between frames, we need to track both the previous and current gamepad states. Add these properties to the `GamePadInfo` class: + +```cs +/// +/// Gets the state of input for this gamepad during the previous update cycle. +/// +public GamePadState PreviousState { get; private set; } + +/// +/// Gets the state of input for this gamepad during the current update cycle. +/// +public GamePadState CurrentState { get; private set; } +``` + +There are times that a gamepad can disconnect for various reasons; being unplugged, bluetooth disconnection, or battery dying are just some examples. To track if the gamepad is connected, add the following property: + +```cs +/// +/// Gets a value that indicates if this gamepad is currently connected. +/// +public bool IsConnected => CurrentState.IsConnected; +``` + +The values of the thumbsticks and triggers can be accessed through the `CurrentState`. However, instead of having to navigate through multiple property chains to get this information, add the following properties to get direct access to the values: + +```cs +/// +/// Gets the value of the left thumbstick of this gamepad. +/// +public Vector2 LeftThumbStick => CurrentState.ThumbSticks.Left; + +/// +/// Gets the value of the right thumbstick of this gamepad. +/// +public Vector2 RightThumbStick => CurrentState.ThumbSticks.Right; + +/// +/// Gets the value of the left trigger of this gamepad. +/// +public float LeftTrigger => CurrentState.Triggers.Left; + +/// +/// Gets the value of the right trigger of this gamepad. +/// +public float RightTrigger => CurrentState.Triggers.Right; +``` + +### GamePadInfo Constructor + +The `GamePadInfo` class needs a constructor to initialize the gamepad states. Add this constructor + +```cs +/// +/// Creates a new GamePadInfo for the gamepad connected at the specified player index. +/// +/// The index of the player for this gamepad. +public GamePadInfo(PlayerIndex playerIndex) +{ + PlayerIndex = playerIndex; + PreviousState = new GamePadState(); + CurrentState = GamePad.GetState(playerIndex); +} +``` + +This constructor + +- Requires a [**PlayerIndex**](xref:Microsoft.Xna.Framework.PlayerIndex) value which is stored and will be used to get the states for the correct gamepad +- Creates an empty state for `PreviousState` since there is no previous state yet. +- Gets the current gamepad state as our starting `CurrentState`. + +This initialization ensures we have valid states to compare against in the first frame of our game, preventing any potential null reference issues when checking for input changes. + +### GamePadInfo Methods + +The `GamePadInfo` class needs methods for updating states, checking button states, and controlling vibration. Let's start with our update method: + +```cs +/// +/// Updates the state information for this gamepad input. +/// +/// +public void Update(GameTime gameTime) +{ + PreviousState = CurrentState; + CurrentState = GamePad.GetState(PlayerIndex); + + if (_vibrationTimeRemaining > TimeSpan.Zero) + { + _vibrationTimeRemaining -= gameTime.ElapsedGameTime; + + if (_vibrationTimeRemaining <= TimeSpan.Zero) + { + StopVibration(); + } + } +} +``` + +> [!NOTE] +> Unlike keyboard and mouse input, the gamepad update method takes a [**GameTime**](xref:Microsoft.Xna.Framework.GameTime) parameter. This allows us to track and manage timed vibration effects. + +Next, we'll add methods to check various button states: + +```cs +/// +/// Returns a value that indicates whether the specified gamepad button is current down. +/// +/// The gamepad button to check. +/// true if the specified gamepad button is currently down; otherwise, false. +public bool IsButtonDown(Buttons button) +{ + return CurrentState.IsButtonDown(button); +} + +/// +/// Returns a value that indicates whether the specified gamepad button is currently up. +/// +/// The gamepad button to check. +/// true if the specified gamepad button is currently up; otherwise, false. +public bool IsButtonUp(Buttons button) +{ + return CurrentState.IsButtonUp(button); +} + +/// +/// Returns a value that indicates whether the specified gamepad button was just pressed on the current frame. +/// +/// +/// true if the specified gamepad button was just pressed on the current frame; otherwise, false. +public bool WasButtonJustPressed(Buttons button) +{ + return CurrentState.IsButtonDown(button) && PreviousState.IsButtonUp(button); +} + +/// +/// Returns a value that indicates whether the specified gamepad button was just released on the current frame. +/// +/// +/// true if the specified gamepad button was just released on the current frame; otherwise, false. +public bool WasButtonJustReleased(Buttons button) +{ + return CurrentState.IsButtonUp(button) && PreviousState.IsButtonDown(button); +} +``` + +These methods serve two distinct purposes. For checking continuous states: + +- `IsButtonDown`: Returns true as long as a button is being held down. +- `IsButtonUp`: Returns true as long as a button is not being pressed. + +And for detecting state changes: + +- `WasButtonJustPressed`: Returns true only on the frame when a button changes from up-to-down. +- `WasButtonJustReleased`: Returns true only on the frame when a button changes from down-to-up. + +Finally, we'll add methods for controlling gamepad vibration: + +```cs +/// +/// Sets the vibration for all motors of this gamepad. +/// +/// The strength of the vibration from 0.0f (none) to 1.0f (full). +/// The amount of time the vibration should occur. +public void SetVibration(float strength, TimeSpan time) +{ + _vibrationTimeRemaining = time; + GamePad.SetVibration(PlayerIndex, strength, strength); +} + +/// +/// Stops the vibration of all motors for this gamepad. +/// +public void StopVibration() +{ + GamePad.SetVibration(PlayerIndex, 0.0f, 0.0f); +} +``` + +The vibration methods provide control over the gamepad's haptic feedback: + +- `SetVibration`: Starts vibration at the specified strength for a set duration. +- `StopVibration`: Immediately stops all vibration. + +> [!TIP] +> When setting vibration, you can specify both the strength (`0.0f` to `1.0f`) and duration. The vibration will automatically stop after the specified time has elapsed, so you don't need to manage stopping it manually. + +That's it for the `GamePadInfo` class. Next, let's create the actual input manager. + +## The InputManager Class + +Now that we have classes to handle keyboard, mouse, and gamepad input individually, we can create a centralized manager class to coordinate all input handling. The `InputManager` class will be static, providing easy access to all input states from anywhere in our game. + +In the *Input* directory of the *MonoGameLibrary* project, add a new file named *InputManager.cs* with this initial structure: + +```cs +using Microsoft.Xna.Framework; + +namespace MonoGameLibrary.Input; + +public static class InputManager +{ + +} +``` + +### InputManager Properties + +The InputManager class needs properties to access each type of input device. Add these properties: + +```cs +/// +/// Gets the state information of keyboard input. +/// +public static KeyboardInfo Keyboard { get; private set; } + +/// +/// Gets the state information of mouse input. +/// +public static MouseInfo Mouse { get; private set; } + +/// +/// Gets the state information of a gamepad. +/// +public static GamePadInfo[] GamePads { get; private set; } +``` + +> [!NOTE] +> The `GamePads` property is an array because MonoGame supports up to four gamepads simultaneously. Each gamepad is associated with a PlayerIndex (0-3). + +### InputManager Methods + +First, we need a method to initialize our input devices: + +```cs +/// +/// Initializes this input manager. +/// +public static void Initialize() +{ + Keyboard = new KeyboardInfo(); + Mouse = new MouseInfo(); + + GamePads = new GamePadInfo[4]; + for (int i = 0; i < 4; i++) + { + GamePads[i] = new GamePadInfo((PlayerIndex)i); + } +} +``` + +Next, we'll add a method to update all input states: + +```cs +/// +/// Updates the state information for the keyboard, mouse, and gamepad inputs. +/// +/// A snapshot of the timing values for the current frame. +public static void Update(GameTime gameTime) +{ + Keyboard.Update(); + Mouse.Update(); + + for (int i = 0; i < 4; i++) + { + GamePads[i].Update(gameTime); + } +} +``` + +> [!TIP] +> By centralizing input updates in the `InputManager`, we ensure all input states are updated consistently each frame. You only need to call `InputManager.Update` once in your game's [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) method. + +### Implementing the InputManager Class + +Now that we have our input management system complete, let's update our game to use it. Instead of tracking input states directly, we'll use the `InputManager` to handle all our input detection. Open *Game1.cs* and make the following changes: + +Let's update the input code in our game now to instead use the `InputManager` class to manage tracking input states which inputs are active. Open the *Game1.cs* file and perform the following: + +1. First we need to set up the `InputManager`. In [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize), add this initialization code just before `base.Initialize()`: + + ```cs + InputManager.Initialize(); + ``` + +2. Next, in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), we need to ensure input states are updated each frame. Add the following as the first line of code inside the [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) method: + + ```cs + InputManager.Update(gameTime); + ``` + +3. Next, remove the `if` statement in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) that checks for the gamepad back button or the keyboard escape key being pressed and then exits the game. + +4. Finally, update the game controls to use the `InputManager`. Replace the `HandleKeyboardInput`, `HandleMouseInput` and `HandleGamePadInput` methods with the following: + + ```cs + private void HandleKeyboardInput() + { + if (InputManager.Keyboard.IsKeyDown(Keys.Escape)) + { + Exit(); + } + if (InputManager.Keyboard.IsKeyDown(Keys.Up)) + { + _slimePosition.Y -= MOVEMENT_SPEED; + } + if (InputManager.Keyboard.IsKeyDown(Keys.Down)) + { + _slimePosition.Y += MOVEMENT_SPEED; + } + if (InputManager.Keyboard.IsKeyDown(Keys.Left)) + { + _slimePosition.X -= MOVEMENT_SPEED; + } + if (InputManager.Keyboard.IsKeyDown(Keys.Right)) + { + _slimePosition.X += MOVEMENT_SPEED; + } + } + + private void HandleMouseInput() + { + if (InputManager.Mouse.WasButtonJustPressed(MouseButton.Left)) + { + _batPosition = InputManager.Mouse.Position.ToVector2(); + } + } + + private void HandleGamepadInput() + { + GamePadInfo gamePadOne = InputManager.GamePads[(int)PlayerIndex.One]; + + if(gamePadOne.IsButtonDown(Buttons.Back)) + { + Exit(); + } + + if (gamePadOne.IsButtonDown(Buttons.A)) + { + _slimePosition.X += gamePadOne.LeftThumbStick.X * 1.5f * MOVEMENT_SPEED; + _slimePosition.Y -= gamePadOne.LeftThumbStick.Y * 1.5f * MOVEMENT_SPEED; + gamePadOne.SetVibration(1.0f, TimeSpan.FromSeconds(0.5f)); + } + else + { + _slimePosition.X += gamePadOne.LeftThumbStick.X * MOVEMENT_SPEED; + _slimePosition.Y -= gamePadOne.LeftThumbStick.Y * MOVEMENT_SPEED; + } + } + ``` + +The key improvements in this implementation are: + +1. Centralized Input Management: + - All input is now accessed through the `InputManager`. + - Input states are automatically tracked between frames. + - No need to manually store previous states. + +2. Improved Input Detection: + - Mouse movement now only triggers on initial click using `WasButtonJustPressed`. + - Gamepad vibration is handled through `SetVibration` with automatic duration. + - Thumbstick values are easily accessed through `LeftThumbStick` property. + +> [!NOTE] +> Using `WasButtonJustPressed` instead of `IsButtonDown` for the mouse control means the bat only moves when you first click, not continuously while holding the button. This gives you more precise control over movement. + +Running the game now, you will be able to control it the same as before, only now we're using our new `InputManager` class instead. + +## Conclusion + +In this chapter, you learned how to: + +- Detect the difference between continuous and single-frame input states. +- Create classes to manage different input devices. +- Build a centralized `InputManager` to coordinate all input handling that is: + - Reusable across different game projects + - Easy to maintain and extend + - Consistent across different input devices + +## Test Your Knowledge + +1. What's the difference between checking if an input is "down" versus checking if it was "just pressed"? + +
+ Question 1 Answer + + > "Down" checks if an input is currently being held, returning true every frame while held. "Just pressed" only returns true on the first frame when the input changes from up to down, requiring comparison between current and previous states. +

+ +2. Why do we track both current and previous input states? + +
+ Question 2 Answer + + > Tracking both states allows us to detect when input changes occur by comparing the current frame's state with the previous frame's state. This is essential for implementing "just pressed" and "just released" checks. +

+ +3. What advantage does the `InputManager` provide over handling input directly? + +
+ Question 3 Answer + + > The `InputManager` centralizes all input handling, automatically tracks states between frames, and provides a consistent API across different input devices. This makes the code more organized, reusable, and easier to maintain. +

diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/bounce.wav b/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/bounce.wav new file mode 100644 index 00000000..baa7a47b Binary files /dev/null and b/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/bounce.wav differ diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/collect.wav b/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/collect.wav new file mode 100644 index 00000000..506220de Binary files /dev/null and b/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/collect.wav differ diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/theme.mp3 b/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/theme.mp3 new file mode 100644 index 00000000..d85a1067 Binary files /dev/null and b/articles/tutorials/building_2d_games/12_soundeffects_and_music/files/theme.mp3 differ diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/song-properties.png b/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/song-properties.png new file mode 100644 index 00000000..87d53631 Binary files /dev/null and b/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/song-properties.png differ diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/sound-effect-properties.png b/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/sound-effect-properties.png new file mode 100644 index 00000000..6fe24eed Binary files /dev/null and b/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/sound-effect-properties.png differ diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/xact-editor.png b/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/xact-editor.png new file mode 100644 index 00000000..667d6771 Binary files /dev/null and b/articles/tutorials/building_2d_games/12_soundeffects_and_music/images/xact-editor.png differ diff --git a/articles/tutorials/building_2d_games/12_soundeffects_and_music/index.md b/articles/tutorials/building_2d_games/12_soundeffects_and_music/index.md new file mode 100644 index 00000000..6a9920bc --- /dev/null +++ b/articles/tutorials/building_2d_games/12_soundeffects_and_music/index.md @@ -0,0 +1,729 @@ +--- +title: "Chapter 12: SoundEffects and Music" +description: "Learn how to load and play sound effects and background music in MonoGame including managing audio volume, looping, and handling multiple sound effects at once." +--- + +In [Chapter 11](../11_collision_detection/index.md), we implemented collision detection to enable interactions between game objects; the slime can now "eat" the bat, which respawns in a random location, while the bat bounces off screen edges. While these mechanics work visually, our game lacks an important element of player feedback: audio. + +Audio plays a crucial role in game development by providing immediate feedback for player actions and creating atmosphere. Sound effects alert players when events occur (like collisions or collecting items), while background music helps establish mood and atmosphere. + +In this chapter, you will: + +- Learn how MonoGame handles different types of audio content. +- Learn how to load and play sound effects and music using the content pipeline. +- Implement sound effects for collision events. +- Add background music to enhance atmosphere. +- Control audio playback including volume and looping. + +Let's start by understanding how MonoGame approaches audio content. + +## Understanding Audio in MonoGame + +Recall from [Chapter 01](../01_what_is_monogame/index.md) that MonoGame is an implementation of the XNA API. When Microsoft originally released XNA, there were two methods for implementing audio in your game: the *Microsoft Cross-Platform Audio Creation Tool* (XACT) and the simplified sound API. XACT is a mini audio engineering studio where you can easily edit the audio for your game like editing volume, pitch, looping, applying effects, and other properties without having to do it in code. At that time, XACT for XNA games was akin to what FMOD Studio is today for game audio. + +| ![Figure 12-1: Microsoft Cross-Platform Audio Creation Tool](./images/xact-editor.png) | +|:--------------------------------------------------------------------------------------:| +| **Figure 12-1: Microsoft Cross-Platform Audio Creation Tool** | + +While XACT projects are still fully supported in MonoGame, it remains a Windows-only tool that hasn't been updated since Microsoft discontinued the original XNA, nor has its source code been made open source. Though it's possible to install XACT on modern Windows, the process can be complex. For these reasons, this tutorial will focus on the simplified sound API, which provides all the core functionality needed for most games while remaining cross-platform compatible. + +The simplified sound API approaches audio management through two distinct paths, each optimized for different use cases in games. When adding audio to your game, you need to consider how different types of sounds should be handled. A short sound effect, like the bounce of a ball, needs to play immediately and might need to play multiple times simultaneously. In contrast, background music needs to play continuously but doesn't require the same immediate response. + +MonoGame addresses these different needs through two main classes: + +### Sound Effects + +The [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) class handles short audio clips like: + +- Collision sounds. +- Player action feedback (jumping, shooting, etc.). +- UI interactions (button clicks, menu navigation). +- Environmental effects (footsteps, ambient sounds). + +The key characteristics of sound effects are: + +- Loaded entirely into memory for quick access +- Can play multiple instances simultaneously +- Lower latency playback (ideal for immediate feedback) +- Individual volume control per instance + +When you play a [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect), it returns a [**SoundEffectInstance**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance) that can be used to control that specific playback: + +```cs +// Load the sound effect +SoundEffect soundEffect = Content.Load("sound effect"); + +// Create an instance we can control +SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance(); +soundEffectInstance.Volume = 0.5f; +soundEffectInstance.Play(); + +// Or play it directly if we don't need to control it +soundEffectInstance.Play(); +``` + +### Music + +The [**Song**](xref:Microsoft.Xna.Framework.Audio.Song) class handles longer audio pieces like background music. The key characteristics of songs are: + +- Streamed from storage rather than loaded into memory. +- Only one song can be played at a time. +- Higher latency, but lower memory usage. + +Songs are played through the [**MediaPlayer**](xref:Microsoft.Xna.Framework.Media.MediaPlayer) class: + +```cs +// Load the song +Song backgroundMusic = Content.Load("theme"); + +// Play the song, optionally looping +MediaPlayer.IsRepeating = true; +MediaPlayer.Play(backgroundMusic); +``` + +> [!NOTE] +> While [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) instances can be played simultaneously, trying to play a new [**Song**](xref:Microsoft.Xna.Framework.Audio.Song) while another is playing will automatically stop the current song. This is why [**Song**](xref:Microsoft.Xna.Framework.Audio.Song) is ideal for background music where you typically only want one track playing at a time. + +Throughout this chapter, we'll use both classes to add audio feedback to our game; sound effects for the bat bouncing and being eaten by the slime, and background music to create atmosphere. + +## Loading Audio Content + +Just like textures, audio content in MonoGame can be loaded through the content pipeline, optimizing the format for your target platform. + +### Supported Audio Formats + +MonoGame supports several audio file formats for both sound effects and music: + +- `.wav`: Uncompressed audio, ideal for short sound effects +- `.mp3`: Compressed audio, better for music and longer sounds +- `.ogg`: Open source compressed format, supported on all platforms +- `.wma`: Windows Media Audio format (not recommended for cross-platform games) + +> [!TIP] +> For sound effects, `.wav` files provide the best loading and playback performance since they don't need to be decompressed. For music, `.mp3` or `.ogg` files are better choices as they reduce file size while maintaining good quality. + +### Adding Audio Files + +Before we can add audio to our game, we need some sound files to work with. Download the following audio files: + +- [bounce.wav](./files/bounce.wav) - For when the bat bounces off screen edges +- [collect.wav](./files/collect.wav) - For when the slime eats the bat +- [theme.mp3](./files/theme.mp3) - Background music + +> [!NOTE] +> +> - *bounce.wav* is "Retro Impact Punch 07" by Davit Masia (https://kronbits.itch.io/retrosfx). +> - *collect.wav* is "Retro Jump Classic 08" by Davit Masia (https://kronbits.itch.io/retrosfx). +> - *theme.mp3* is "8bit Dungeon Level" by Kevin MacLeod (incompetech.com), Licensed under Creative Commons: By Attribution 4.0 License http://creativecommons.org/licenses/by/4.0/ + +Add these files to your content project using the MGCB Editor: + +1. Open the *Content.mgcb* file in the MGCB Editor. +2. Create a new directory called `audio` (right-click *Content* > *Add* > *New Folder*). +3. Right-click the new *audio* directory and choose *Add* > *Existing Item...*. +4. Navigate to and select the audio files you downloaded. +5. For each file that's added, check its properties in the Properties panel: + - For `.wav` files, ensure *Build Action* is set to `Build` and *Content Processor* is set to `Sound Effect`. + - For `.mp3` files, ensure *Build Action* is set to `Build` and *Content Processor* is set to `Song`. + + | ![Figure 12-2: MGCB Editor properties panel showing Sound Effect content processor settings for .wav files** | **Figure 12-3: MGCB Editor properties panel showing Song content processor settings for .mp3 files](./images/sound-effect-properties.png) | ![Figure 12-3: MGCB Editor properties panel showing Song content processor settings for .mp3 files](./images/song-properties.png) | + | :---: | :---: | + | **Figure 12-2: MGCB Editor properties panel showing Sound Effect content processor settings for .wav files** | **Figure 12-3: MGCB Editor properties panel showing Song content processor settings for .mp3 files** | + +### Loading Sound Effects + +To load a sound effect, we use [**ContentManager.Load**](xref:Microsoft.Xna.Framework.Content.ContentManager.Load``1(System.String)) with the [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) type. Open then *Game1.cs* and perform the following: + +1. First, add the following fields to store the sound effects that are loaded: + + ```cs + private SoundEffect _bounceSound; + private SoundEffect _collectSound; + ``` + +2. In [LoadContent](xref:Microsoft.Xna.Framework.Game.LoadContent) add the following to load the sound effects: + + ```cs + _bounceSound = Content.Load("audio/bounce"); + _collectSound = Content.Load("audio/collect"); + ``` + +### Loading Music + +Loading music is similar, only we specify the [**Song**](xref:Microsoft.Xna.Framework.Audio.Song) type instead. Perform the following: + +1. First, add the following field to track the song: + + ```cs + private Song _backgroundMusic; + ``` + +2. In [LoadContent](xref:Microsoft.Xna.Framework.Game.LoadContent) add the following to load the song: + + ```cs + _backgroundMusic = Content.Load("audio/theme"); + ``` + +## Playing Audio + +Now that we have our audio content loaded, we can implement sound playback in our game. Let's start by understanding how to play sound effects and music, then we'll add them to our game's collision events. + +### Playing Sound Effects + +The [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) class provides two ways to play sounds: + +1. Direct playback using [**SoundEffect.Play**](xref:Microsoft.Xna.Framework.Audio.SoundEffect.Play): + + ```cs + // Play the sound effect with default settings + _bounceSound.Play(); + ``` + +2. Creating an instance using [**SoundEffect.CreateInstance**](xref:Microsoft.Xna.Framework.Audio.SoundEffect.CreateInstance): + + ```cs + // Create an instance we can control + SoundEffectInstance bounceInstance = _bounceSound.CreateInstance(); + bounceInstance.Volume = 0.5f; + bounceInstance.Play(); + ``` + +The first method is simpler but provides no control over the playback. The second method returns a [**SoundEffectInstance**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance) that can be controlled through several properties: + +| Property | Type | Description | +|---------------------------------------------------------------------------------|-----------------------------------------------------------------|----------------------------------------------------------------------------| +| [**IsLooped**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance.IsLooped) | `bool` | Whether the sound should loop when it reaches the end. | +| [**Pan**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance.Pan) | `float` | Stereo panning between -1.0f (full left) and 1.0f (full right). | +| [**Pitch**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance.Pitch) | `float` | Pitch adjustment between -1.0f (down one octave) and 1.0f (up one octave). | +| [**State**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance.State) | [**SoundState**](xref:Microsoft.Xna.Framework.Audio.SoundState) | Current playback state (Playing, Paused, or Stopped). | +| [**Volume**](xref:Microsoft.Xna.Framework.Audio.SoundEffectInstance.Volume) | `float` | Volume level between 0.0f (silent) and 1.0f (full volume). | + +### Playing Music + +Unlike sound effects, music is played through the [**MediaPlayer**](xref:Microsoft.Xna.Framework.Media.MediaPlayer) class. This static class manages playback of [**Song**](xref:Microsoft.Xna.Framework.Media.Song) instances and provides global control over music playback: + +```cs +// Set whether the song should repeat when finished +MediaPlayer.IsRepeating = true; + +// Adjust the volume (0.0f to 1.0f) +MediaPlayer.Volume = 0.5f; + +// Start playing the background music +MediaPlayer.Play(_backgroundMusic); +``` + +> [!NOTE] +> Only one song can play at a time. Starting a new song will automatically stop any currently playing song. + +### Adding Audio to Our Game + +Let's update our game to play audio at appropriate times. We'll play: + +- Background music when the game first loads. +- The bounce sound when the bat hits screen edges. +- The collect sound when the slime eats the bat. + +Open *Game1.cs* and make the following changes: + +1. First, let's add background music playback. In [Initialize](xref:Microsoft.Xna.Framework.Game.Initialize), after the call to `base.Initialize()`, add: + + ```cs + // Ensure the song is looping + MediaPlayer.IsRepeating = true; + + // PLay the song + MediaPlayer.Play(_backgroundMusic); + ``` + +2. Next, update the `UpdateBatMovement` method to play the bounce sound when the bat hits screen edges. Add the following as part of the collision response: + + ```cs + // Play bounce sound + _bounceSound.Play(); + ``` + +3. Finally, in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)), add the collect sound during the collision response when the slime eats the bat: + + ```cs + // Play collect sound + _collectSound.Play(); + ``` + +Running the game now, you'll hear: + +- Background music playing continuously. +- A bounce sound whenever the bat hits screen edges. +- A collect sound whenever the slime eats the bat. + +## Audio Management + +While playing sounds and music is straightforward, a complete game needs to handle various audio states and cleanup. An audio manager that will: + +- Track and manage sound effects and songs +- Handle volume control +- Manage audio state (pause/resume, mute/unmute) +- Clean up resources properly + +To get started, create a new directory called *Audio* in the *MonoGameLibrary* project. + +### The AudioManager Class + +To effectively manage audio in our games, we'll create an `AudioManager` class that handles loading, playing, and controlling both sound effects and music. This manager will be implemented as a [**GameComponent**](xref:Microsoft.Xna.Framework.GameComponent), allowing it to receive automatic updates and cleanup. + +In the *Audio* directory of the *MonoGameLibrary* project, add a new file named *AudioManager.cs* with this initial structure: + +```cs +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Media; + +namespace MonoGameLibrary.Audio; + +public class AudioManager : GameComponent +{ + +} +``` + +> [!TIP] +> The `AudioManager` class inherits from [**GameComponent**](xref:Microsoft.Xna.Framework.GameComponent), which allows it to be added to a game's component collection and automatically receive updates. + +#### AudioManager Members + +The `AudioManager` class needs to track various audio resources and states. Add these private fields: + +```cs +private readonly Dictionary _soundEffects; +private readonly Dictionary _songs; +private readonly List _activeSoundEffectInstances; +private float _previousMusicVolume; +private float _previousSoundEffectVolume; +``` + +These fields serve different purposes: + +- `_soundEffects`: Stores loaded sound effects by their asset name. +- `_songs`: Stores loaded songs by their asset name. +- `_activeSoundEffectInstances`: Tracks currently playing sound effects. +- `_previousMusicVolume` and `_previousSoundEffectVolume`: Store volume levels for mute/unmute functionality. + +### AudioManager Properties + +The AudioManager provides a property to track its mute state. Add the following property: + +```cs +/// +/// Gets a value that indicates if audio is muted. +/// +public bool IsMuted { get; private set; } +``` + +### AudioManager Constructor + +The constructor initializes our collections and sets up the component. Add the following constructor: + +```cs +/// +/// Creates a new AudioManager instance. +/// +/// The game this audio manager will belong too.. +public AudioManager(Game game) + : base(game) +{ + _soundEffects = new Dictionary(); + _songs = new Dictionary(); + _activeSoundEffectInstances = new List(); +} +``` + +### AudioManager Methods + +The `AudioManager` class provides several categories of methods to handle different aspects of audio management. Each group of methods serves a specific purpose in managing game audio, from basic playback to more complex state management. + +#### Game Component Methods + +Add the following methods to the manager which override key methods from [**GameComponent**](xref:Microsoft.Xna.Framework.GameComponent): + +```cs +/// +/// Initializes this Audio manager. +/// +public override void Initialize() +{ + _previousMusicVolume = MediaPlayer.Volume = 1.0f; + _previousSoundEffectVolume = SoundEffect.MasterVolume = 1.0f; + base.Initialize(); +} + +/// +/// Updates this Audio manager +/// +/// A snapshot of the current timing values for the game. +public override void Update(GameTime gameTime) +{ + int index = 0; + + while (index < _activeSoundEffectInstances.Count) + { + SoundEffectInstance instance = _activeSoundEffectInstances[index]; + + if (instance.State == SoundState.Stopped && !instance.IsDisposed) + { + instance.Dispose(); + } + + _activeSoundEffectInstances.RemoveAt(index); + } + + base.Update(gameTime); +} + +/// +/// Disposes this Audio manager and cleans up resources. +/// +/// Indicates whether managed resources should be disposed. +protected override void Dispose(bool disposing) +{ + if (disposing) + { + foreach (SoundEffect soundEffect in _soundEffects.Values) + { + soundEffect.Dispose(); + } + + foreach (Song song in _songs.Values) + { + song.Dispose(); + } + + _soundEffects.Clear(); + _songs.Clear(); + _activeSoundEffectInstances.Clear(); + } + + base.Dispose(disposing); +} +``` + +- `Initialize`: Sets up initial audio states by setting the default volume levels for both music and sound effects to full volume (1.0f). These values are also stored as the previous volumes for use when unmuting. +- `Update`: Handles cleanup of completed sound effects. Each frame, it checks for any sound effect instances that have finished playing (reached the Stopped state) and disposes of them to free up resources. +- `Dispose`: Ensures proper cleanup of audio resources when the game closes. All sound effects and songs are disposed of, and the collections tracking them are cleared. + +#### Content Management Methods + +Add the following methods to handle handle loading audio content into the manager: + +```cs +/// +/// Adds the sound effect with the specified asset name to this audio manager. +/// +/// The asset name of the sound effect to add. +public void AddSoundEffect(string assetName) +{ + SoundEffect soundEffect = Game.Content.Load(assetName); + _soundEffects.Add(assetName, soundEffect); +} + +/// +/// Adds the song with the specified asset name to this audio manager. +/// +/// The asset name of the song to add. +public void AddSong(string assetName) +{ + Song song = Game.Content.Load(assetName); + _songs.Add(assetName, song); +} +``` + +- `AddSoundEffect`: Loads a sound effect from the content pipeline using the provided asset name and stores it in the `_soundEffects` dictionary for later use. +- `AddSong`: Similar to `AddSoundEffect`, this loads a song from the content pipeline and stores it in the `_songs` dictionary. + +#### Playback Methods + +Add the following methods to control audio playback: + +```cs +/// +/// Plays the sound effect with the specified asset name. +/// +/// The asset name of the sound effect to play. +/// The volume, ranging from 0.0 (silence) to 1.0 (full volume). +/// The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave). +/// The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker). +/// Whether the the sound effect should loop after playback. +/// The sound effect instance created by playing the sound effect. +public SoundEffectInstance PlaySoundEffect(string assetName, float volume = 1.0f, float pitch = 0.0f, float pan = 0.0f, bool isLooped = false) +{ + SoundEffect soundEffect = _soundEffects[assetName]; + + SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance(); + soundEffectInstance.Volume = volume; + soundEffectInstance.Pitch = pitch; + soundEffectInstance.Pan = pan; + soundEffectInstance.IsLooped = isLooped; + + soundEffectInstance.Play(); + + return soundEffectInstance; +} + +/// +/// Plays the song with the specified asset name. +/// +/// The asset name of the song to play. +public void PlaySong(string assetName) +{ + Song song = _songs[assetName]; + MediaPlayer.Play(song); +} +``` + +- `PlaySoundEffect`: Creates and plays an instance of a sound effect with customizable properties like volume, pitch, panning, and looping. Returns the instance for further control if needed. +- `PlaySong`: Starts playing a song through the MediaPlayer. Since only one song can play at a time, this will automatically stop any currently playing song. + +#### State Control Methods + +Add the following methods to manage the overall state of audio playback: + +```cs +/// +/// Pauses all audio. +/// +public void PauseAudio() +{ + // Pause any active songs playing + MediaPlayer.Pause(); + + // Pause any active sound effects + foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances) + { + soundEffectInstance.Pause(); + } +} + +/// +/// Resumes play of all previous paused audio. +/// +public void ResumeAudio() +{ + // Resume paused music + MediaPlayer.Resume(); + + // Resume any active sound effects + foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances) + { + soundEffectInstance.Resume(); + } +} + +/// +/// Mutes all audio. +/// +public void MuteAudio() +{ + // Store the volume so they can be restored during ResumeAudio + _previousMusicVolume = MediaPlayer.Volume; + _previousSoundEffectVolume = SoundEffect.MasterVolume; + + // Set all volumes to 0 + MediaPlayer.Volume = 0.0f; + SoundEffect.MasterVolume = 0.0f; + + IsMuted = true; +} + +/// +/// Unmutes all audio to the volume level prior to muting. +/// +public void UnmuteAudio() +{ + // Restore the previous volume values + MediaPlayer.Volume = _previousMusicVolume; + SoundEffect.MasterVolume = _previousSoundEffectVolume; + + IsMuted = false; +} + +/// +/// Toggles the current audio mute state. +/// +public void ToggleMute() +{ + if (IsMuted) + { + UnmuteAudio(); + } + else + { + MuteAudio(); + } +} +``` + +- `PauseAudio`: Pauses all currently playing audio including both the active song and any playing sound effects. +- `ResumeAudio`: Resumes playback of previously paused audio, both for the song and sound effects. +- `MuteAudio`: Silences all audio by setting volumes to zero while storing previous volumes. +- `UnmuteAudio`: Restores audio to the volume levels that were active before muting. +- `ToggleMute`: Provides a convenient way to switch between muted and unmuted states. + +#### Volume Control Methods + +Finally, add the following methods to adjusting the volume of all audio: + +```cs +/// +/// Increases volume of all audio by the specified amount. +/// +/// The amount to increase the audio by. +public void IncreaseVolume(float amount) +{ + if (!IsMuted) + { + MediaPlayer.Volume = Math.Min(MediaPlayer.Volume + amount, 1.0f); + SoundEffect.MasterVolume = Math.Min(SoundEffect.MasterVolume + amount, 1.0f); + } +} + +/// +/// Decreases the volume of all audio by the specified amount. +/// +/// The amount to decrease the audio by. +public void DecreaseVolume(float amount) +{ + if (!IsMuted) + { + MediaPlayer.Volume = Math.Max(MediaPlayer.Volume - amount, 0.0f); + SoundEffect.MasterVolume = Math.Max(SoundEffect.MasterVolume - amount, 0.0f); + } +} +``` + +- `IncreaseVolume`: Raises the volume of both music and sound effects by the specified amount, ensuring it doesn't exceed the maximum (1.0f). +- `DecreaseVolume`: Lowers the volume of both music and sound effects by the specified amount, ensuring it doesn't go below zero. + +### Using the AudioManager + +Now that we have our `AudioManager` class to handle audio, let's update our game to use it instead of managing audio directly. We'll remove the individual audio fields and update our collision response code to use the manager. + +Open *Game1.cs* and make the following changes to use the new audio manager: + +1. First, remove the individual audio fields `_bounceSound`, `_collectSound`, and `_backgroundMusic` and add an `AudioManger` field: + + ```cs + private AudioManager _audioManager; + ``` + +2. In the constructor, create the manager and add it as a game component: + + ```cs + // Create and add the audio manager + _audioManager = new AudioManager(this); + Components.Add(_audioManager); + ``` + +3. Update [**Initialize**](xref:Microsoft.Xna.Framework.Game.Initialize) to play the background music through the audio manager instead of the [**MediaPlayer**](xref:Microsoft.Xna.Framework.Media.MediaPlayer): + + ```cs + // Start playing background music + _audioManager.PlaySong("audio/theme"); + ``` + +4. Update [**LoadContent**](xref:Microsoft.Xna.Framework.Game.LoadContent) to load audio through the manager: + + ```cs + // Load audio content + _audioManager.AddSoundEffect("audio/bounce"); + _audioManager.AddSoundEffect("audio/collect"); + _audioManager.AddSong("audio/theme"); + + // Start playing background music + _audioManager.PlaySong("audio/theme"); + ``` + +5. Update `HandleKeyboardInput` to use the manager for audio controls: + + ```cs + if (InputManager.Keyboard.WasKeyJustPressed(Keys.M)) + { + _audioManager.ToggleMute(); + } + + if (InputManager.Keyboard.WasKeyJustPressed(Keys.OemPlus)) + { + _audioManager.IncreaseVolume(0.1f); + } + + if (InputManager.Keyboard.WasKeyJustPressed(Keys.OemMinus)) + { + _audioManager.DecreaseVolume(0.1f); + } + ``` + +6. Update the collision response in `UpdateBatMovement` to play the bounce sound effect using the manager: + + ```cs + // Play bounce sound through the manager + _audioManager.PlaySoundEffect("audio/bounce"); + ``` + +7. Update the collision response in [**Update**](xref:Microsoft.Xna.Framework.Game.Update(Microsoft.Xna.Framework.GameTime)) to play the collect sound effect using the manager: + + ```cs + // Play collect sound through the manager + _audioManager.PlaySoundEffect("audio/collect"); + ``` + +> [!NOTE] +> Since we added the `AudioManager` as a game component, we don't need to worry about updating or disposing of audio resources - the component system handles this automatically. + +Running the game now, you'll have the same audio feedback as before, but with better management of audio resources and additional features like volume control and muting. Try using the following controls: + +- M key to toggle mute/unmute. +- Plus (+) key to increase volume. +- Minus (-) key to decrease volume. + +## Conclusion + +Let's review what you accomplished in this chapter: + +- Learned about MonoGame's audio system including sound effects and music. +- Added sound effects to our game for: + - Bat bouncing off screen edges. + - Slime collecting the bat. +- Implemented background music. +- Created a reusable `AudioManager` class that: + - Manages loading and playback of audio content. + - Handles volume control and muting. + - Automatically cleans up audio resources. + - Integrates with the game component system. + +In the next chapter, we'll explore scene management to handle different game screens like a title screen and a gameplay screen. + +## Test Your Knowledge + +1. What are the two main classes MonoGame provides for audio playback and how do they differ? + +
+ Question 1 Answer + + > MonoGame provides [**SoundEffect**](xref:Microsoft.Xna.Framework.Audio.SoundEffect) for short audio clips (loaded entirely into memory, multiple can play at once) and [**Song**](xref:Microsoft.Xna.Framework.Media.Song) for longer audio like music (streamed from storage, only one can play at a time). +

+ +2. What is the advantage of using the content pipeline for loading audio files? + +
+ Question 2 Answer + + > The content pipeline processes audio files into an optimized format for your target platform, manages asset copying to the output directory, and provides a consistent way to load content at runtime through the [**ContentManager**](xref:Microsoft.Xna.Framework.Content.ContentManager). +

+ +3. Why did we create the `AudioManager` as a game component? + +
+ Question 3 Answer + + > By inheriting from [**GameComponent**](xref:Microsoft.Xna.Framework.GameComponent), the `AudioManager` receives automatic updates and cleanup through the game's component system. +

\ No newline at end of file diff --git a/articles/tutorials/building_2d_games/index.md b/articles/tutorials/building_2d_games/index.md new file mode 100644 index 00000000..51aa7793 --- /dev/null +++ b/articles/tutorials/building_2d_games/index.md @@ -0,0 +1,87 @@ +--- +title: Building 2D Games with MonoGame +description: A beginner tutorial for building 2D games with MonoGame. +--- + +This tutorial covers game development concepts using the MonoGame framework as our tool. Throughout each chapter, we will explore game development with MonoGame, introducing new concepts to build upon previous ones as we go. we will create a Snake game, which will serve as the vehicle to apply the concepts learned throughout the tutorial. The goal of this tutorial is to give you a solid foundation in 2D game development with MonoGame and provide you with reusable modules that you can jump start future projects. + +## Who This Is For + +This documentation is meant to be an introduction to game development and MonoGame. Readers should have a foundational understanding of C# and be comfortable with concepts such as classes and objects. + +> [!NOTE] +> If you are just getting started with C# for the first time, I would recommend following the official [Learn C#](https://dotnet.microsoft.com/en-us/learn/csharp) tutorials provided by Microsoft. These are free tutorials that teach you programming concepts as well as the C# languages. Once you feel you have a good foundation with that, come back and continue here. + +## How This Documentation Is Organized + +This documentation will introduce game development concepts using the MonoGame framework while walking the reader through the development of a Snake clone. The documentation is organized such that each chapter should be read sequentially, with each introducing new concepts and building off of the previous chapters. + +> [!CAUTION] +> This is currently a work in progress and is not finished. + +| Chapter | Summary | Source Files | +|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------| +| [01: What Is MonoGame](01_what_is_monogame/index.md) | Learn about the history of MonoGame and explore the features it provides developers when creating games. | | +| [02: Getting Started](02_getting_started/index.md) | Setup your development environment for dotnet development and MonoGame using Visual Studio Code as your IDE. | | +| [03: The Game1 File](03_the_game1_file/index.md) | Explore the contents of the Game1 file generated when creating a new MonoGame project. | | +| [04: Content Pipeline](04_content_pipeline/index.md) | Learn the advantages of using the **Content Pipeline** to load assets and go through the processes of loading your first asset | | +| [05: Working with Textures](05_working_with_textures/index.md) | Learn how to load and render textures using the MonoGame content pipeline and [**SpriteBatch**](xref:Microsoft.Xna.Framework.Graphics.SpriteBatch). | | +| [06: Optimizing Texture Rendering](06_optimizing_texture_rendering/index.md) | Explore optimization techniques when rendering textures using a texture atlas. | | +| [07: The Sprite Class](07_the_sprite_class/index.md) | Explore creating a reusable Sprite class to efficiently sprites and their rendering properties, including position, rotation, scale, and more. | | +| [08: The AnimatedSprite Class](07_the_sprite_class/index.md) | Create an AnimatedSprite class that builds upon our Sprite class to support frame-based animations. | | +| [09: Handling Input](09_handling_input/index.md) | Learn how to handle keyboard, mouse, and gamepad input in MonoGame. | | +| [10: Input Management](10_input_management/index.md) | Learn how to create an input management system to handle keyboard, mouse, and gamepad input, including state tracking between frames and creating a reusable framework for handling player input. | | +| [11: Collision Detection](11_collision_detection/index.md) | Learn how to implement collision detection between game objects and handle collision responses like blocking, triggering events, and bouncing. | | +| [12: Sound Effects and Music](12_soundeffects_and_music/index.md) | Learn how to load and play sound effects and background music in MonoGame including managing audio volume, looping, and handling multiple sound effects at once. | | + +In additional to the chapter documentation, supplemental documentation is also provided to give a more in-depth look at different topics with MonoGame. These are provided through the Appendix documentation below: + +| Appendix | Summary | +|----------|---------| +| | | + +## Conventions Used in This Documentation + +The following conventions are used in this documentation + +### Italics + +*Italics* are used for emphasis, technical terms, and paths such as file paths including filenames and extensions. + +### Inline Code Blocks + +`Inline code` blocks are used for methods, functions, and variable names when they are discussed with the body a text paragraph. + +### Code Blocks + +```cs +// Example Code Block +public void Foo() { } +``` + +Code blocks are used to show code examples with syntax highlighting + +## MonoGame + +If you ever have questions about MonoGame or would like to talk with other developers to share ideas or just hang out with us, you can find us in the various MonoGame communities below + +* [Discord](https://discord.gg/monogame) +* [GitHub Discussions Forum](https://github.com/MonoGame/MonoGame/discussions) +* [Community Forums (deprecated)](https://community.monogame.net/) +* [Reddit](https://www.reddit.com/r/monogame/) +* [Facebook](https://www.facebook.com/monogamecommunity) + +## Note From Author + +> I have been using MonoGame for the past several years (since 2017). It was a time in my game development journey where I was looking for something that I had more control over. I didn't want to spend the time to write a full game engine, but I also wanted to have more control than what the current engines at the time (i.e. Unity) offered. At that time, there was a vast amount of resources available for getting started, but none of them felt like they fit a good beginner series. Even now, the resources available still seem this way. They either require the reader to have a great understanding of game development and programming, or they assume the reader has none and instead focuses on teaching programming more than teaching MonoGame. Even still, some relied too heavily on third party libraries, others were simply very bare bones asking the reader to just copy and paste code without explaining the *what* of it all. +> +> Since then, I have written various small self contained tutorials on different topics for MonoGame to try and give back to the community for those getting started. I also participate regularly in the community discussion channels, answering questions and offering technical advice, so I'm very familiar with the topics and pain points that users get hung up on when first starting out. +> +> With this documentation, I hope to take the lessons I've learned and provide a tutorial series that I wish was available when I first started.. To present using MonoGame in a straight forward way, introducing concepts and building off of them as we go along in a way that makes sense and is easy to follow. +> +> \- Christopher Whitley (Aristurtle) + +## Acknowledgements + +> [!NOTE] +> Acknowledgments will be added at a later time to recognize everyone that assisted with editing and reviewing this documentation. \ No newline at end of file diff --git a/articles/tutorials.md b/articles/tutorials/index.md similarity index 100% rename from articles/tutorials.md rename to articles/tutorials/index.md diff --git a/templates/monogame/public/main.css b/templates/monogame/public/main.css index 580919e0..01a331c4 100644 --- a/templates/monogame/public/main.css +++ b/templates/monogame/public/main.css @@ -48,6 +48,17 @@ body[data-disable-toc="true" i] .toc-offcanvas { visibility: hidden; } +th:has(a img[alt^="Figure"]), +td:has(a img[alt^="Figure"]) { + width: 50%; +} + +th video, +td video { + width: 100%; + height: 100% +} + /******************************************************************************* *** Section: Bootstrap Overrides @@ -230,3 +241,9 @@ p img { margin-top: 1.5em; margin-bottom: 1.5em; } + +/* Resolves issue where xref links in table columns will break and wrap text in +the middle of the word */ +td > .xref { + word-break: normal; +} \ No newline at end of file