Commit f5564fa9 authored by Nathan Bean's avatar Nathan Bean
Browse files

Added game architecture chapter

parent a3db9007
---
title: "Input State"
pre: "7. "
weight: 70
date: 2018-08-24T10:53:26-05:00
---
The [Game State Management Sample](https://github.com/tomizechsterson/game-state-management-monogame) provides a contrasting approach to the input manager. Instead of being tailored to a specific game, it seeks to provide generic access to all input information. It also handles multiplayer input, and can be used to manage when a player switches gamepads. A simplified form (which does not handle gestural input) is provided below.
In particular, the `IsButtonPressed(Buttons button, PlayerIndex? controllingPlayer, out PlayerIndex playerIndex)` can check for a key press on any connected keyboard, or identify what player's keyboard was the source of the input. And the `IsNewButtonPress(Buttons button, PlayerIndex? controllingPlayer, out PlayerIndex playerIndex)` is handled the same way, but detects _new_ button presses.
There are also equivalents for keyboard input.
```csharp
// Helper for reading input from keyboard, gamepad, and touch input. This class
// tracks both the current and previous state of the input devices, and implements
// query methods for high level input actions such as "move up through the menu"
// or "pause the game".
public class InputState
{
private const int MaxInputs = 4;
public readonly KeyboardState[] CurrentKeyboardStates;
public readonly GamePadState[] CurrentGamePadStates;
private readonly KeyboardState[] _lastKeyboardStates;
private readonly GamePadState[] _lastGamePadStates;
public readonly bool[] GamePadWasConnected;
public InputState()
{
CurrentKeyboardStates = new KeyboardState[MaxInputs];
CurrentGamePadStates = new GamePadState[MaxInputs];
_lastKeyboardStates = new KeyboardState[MaxInputs];
_lastGamePadStates = new GamePadState[MaxInputs];
GamePadWasConnected = new bool[MaxInputs];
}
// Reads the latest user input state.
public void Update()
{
for (int i = 0; i < MaxInputs; i++)
{
_lastKeyboardStates[i] = CurrentKeyboardStates[i];
_lastGamePadStates[i] = CurrentGamePadStates[i];
CurrentKeyboardStates[i] = Keyboard.GetState();
CurrentGamePadStates[i] = GamePad.GetState((PlayerIndex)i);
// Keep track of whether a gamepad has ever been
// connected, so we can detect if it is unplugged.
if (CurrentGamePadStates[i].IsConnected)
GamePadWasConnected[i] = true;
}
}
// Helper for checking if a key was pressed during this update. The
// controllingPlayer parameter specifies which player to read input for.
// If this is null, it will accept input from any player. When a keypress
// is detected, the output playerIndex reports which player pressed it.
public bool IsKeyPressed(Keys key, PlayerIndex? controllingPlayer, out PlayerIndex playerIndex)
{
if (controllingPlayer.HasValue)
{
// Read input from the specified player.
playerIndex = controllingPlayer.Value;
int i = (int)playerIndex;
return CurrentKeyboardStates[i].IsKeyDown(key);
}
// Accept input from any player.
return IsKeyPressed(key, PlayerIndex.One, out playerIndex) ||
IsKeyPressed(key, PlayerIndex.Two, out playerIndex) ||
IsKeyPressed(key, PlayerIndex.Three, out playerIndex) ||
IsKeyPressed(key, PlayerIndex.Four, out playerIndex);
}
// Helper for checking if a button was pressed during this update.
// The controllingPlayer parameter specifies which player to read input for.
// If this is null, it will accept input from any player. When a button press
// is detected, the output playerIndex reports which player pressed it.
public bool IsButtonPressed(Buttons button, PlayerIndex? controllingPlayer, out PlayerIndex playerIndex)
{
if (controllingPlayer.HasValue)
{
// Read input from the specified player.
playerIndex = controllingPlayer.Value;
int i = (int)playerIndex;
return CurrentGamePadStates[i].IsButtonDown(button);
}
// Accept input from any player.
return IsButtonPressed(button, PlayerIndex.One, out playerIndex) ||
IsButtonPressed(button, PlayerIndex.Two, out playerIndex) ||
IsButtonPressed(button, PlayerIndex.Three, out playerIndex) ||
IsButtonPressed(button, PlayerIndex.Four, out playerIndex);
}
// Helper for checking if a key was newly pressed during this update. The
// controllingPlayer parameter specifies which player to read input for.
// If this is null, it will accept input from any player. When a keypress
// is detected, the output playerIndex reports which player pressed it.
public bool IsNewKeyPress(Keys key, PlayerIndex? controllingPlayer, out PlayerIndex playerIndex)
{
if (controllingPlayer.HasValue)
{
// Read input from the specified player.
playerIndex = controllingPlayer.Value;
int i = (int)playerIndex;
return (CurrentKeyboardStates[i].IsKeyDown(key) &&
_lastKeyboardStates[i].IsKeyUp(key));
}
// Accept input from any player.
return IsNewKeyPress(key, PlayerIndex.One, out playerIndex) ||
IsNewKeyPress(key, PlayerIndex.Two, out playerIndex) ||
IsNewKeyPress(key, PlayerIndex.Three, out playerIndex) ||
IsNewKeyPress(key, PlayerIndex.Four, out playerIndex);
}
// Helper for checking if a button was newly pressed during this update.
// The controllingPlayer parameter specifies which player to read input for.
// If this is null, it will accept input from any player. When a button press
// is detected, the output playerIndex reports which player pressed it.
public bool IsNewButtonPress(Buttons button, PlayerIndex? controllingPlayer, out PlayerIndex playerIndex)
{
if (controllingPlayer.HasValue)
{
// Read input from the specified player.
playerIndex = controllingPlayer.Value;
int i = (int)playerIndex;
return CurrentGamePadStates[i].IsButtonDown(button) &&
_lastGamePadStates[i].IsButtonUp(button);
}
// Accept input from any player.
return IsNewButtonPress(button, PlayerIndex.One, out playerIndex) ||
IsNewButtonPress(button, PlayerIndex.Two, out playerIndex) ||
IsNewButtonPress(button, PlayerIndex.Three, out playerIndex) ||
IsNewButtonPress(button, PlayerIndex.Four, out playerIndex);
}
}
```
+++
title = "Rigid Body Dynamics"
title = "Physics"
date = 2018-08-24T10:53:05-05:00
weight = 6
chapter = true
......@@ -8,7 +8,7 @@ pre = "6. "
### Chapter 6
# Rigid Body Dynamics
# Physics
For every action...
......
---
title: "Introduction"
pre: "1. "
weight: 1
date: 2020-03-20T10:53:05-05:00
---
Now that you've moved through much of the foundations of building games, let's take a step back and talk about how to best _organize_ that task. After all, games are one of the most complex software systems you can build. In the words of Jason Gregory, a game is:
> A soft real-time interactive agent-based simulation
This means that not only do you need to process user input, update a simulated world, and then render that simulated world, you _also_ have to do this in realtime (i.e. within 1/60th of a second)! This is not a trivial challenge!
As with any software system, organization can go a long way to managing this complexity. Consider this diagram of a AAA game engine's software architecture:
![Software Engine Architecture]({{<static "images/7.1.1.png">}})
Note how the engine is broken into systems and organized into layers. Building an engine like this is outside the scope of this book (I encourage you to read Jason Gregory's _Game Engine Architecture_ if you'd like to delve into it), but the idea of loosely coupled systems is certainly something we can adapt. Moreover, it is already explicitly supported by the MonoGame framework. We'll explore how in this chapter.
\ No newline at end of file
---
title: "Game Services"
pre: "2. "
weight: 2
date: 2020-03-20T10:53:05-05:00
---
A common approach in software architecture for loose coupling of systems is the use of _services_. Services are implemented with 1) a _service provider_ - essentially a collection of services that can be searched for a service, and new services can be registered with, 2) interfaces that define specific services how to work with the service, and 3) classes that implement these interfaces. This is the [Service Locator Pattern](https://gameprogrammingpatterns.com/service-locator.html) as implemented in C#.
For a Service Provider, MonoGame provides the [`GameServiceContainer`](https://docs.monogame.net/api/Microsoft.Xna.Framework.GameServiceContainer.html) class, and the `Game` class has one as its `Service` property. The default game class adds at least two services: an `IGraphicsDeviceService` and an `IGraphicsDeviceManager`. If we need to retrieve the graphics device for some reason we could use the code:
```csharp
var gds = game.Services.GetService(typeof(IGraphicDeviceService));
GraphicsDevice gd = gds.GraphicsDevice;
```
We can add any service we want to with the `GameServicesContainer.AddService(Type type, object provider)`. In effect, the `GameServicesContainer` acts as a dictionary for finding initialized instances of services you would use across the game. For example, we might want to have a custom service for reporting achievements in the game. Because the implementation would be different for the Xbox than the Playstation, we could define an interface to represent our type:
```csharp
public class IAchievementService
{
public void RegisterAchievement(Achievement achievement);
}
```
Then we could author _two_ classes implementing this interface, one for the Xbox and one for the Playstation. We would initalize and register the appropriate one for the build of our program:
```csharp
game.Services.AddService(IAchievementService, new XBoxAchievementService());
```
This provides us with that desirable "loose coupling", where the only change we'd need to make between the two builds is what achievement service we initialize. A second common use for the `GameServicesContainer` is it can be passed to a constructor to provide multiple services as a single parameter, instead of having to pass each one separately. It can also be held onto to retrieve the service at a later point in the execution, as is the case with the `ContentManager` constructor.
Game services are a good replacement for systems that you might otherwise use the [Singleton Pattern](https://gameprogrammingpatterns.com/singleton.html) to implement.
\ No newline at end of file
---
title: "Game Components"
pre: "3. "
weight: 3
date: 2020-03-20T10:53:05-05:00
---
A second useful decoupling pattern in MonoGame is the use of _game components_. You've probably noticed that many of the classes you have written have a similar pattern: they each have a `LoadContent()`, `Update()`, and `Draw()` method, and these often take the same arguments. In your game class, you probably mostly invoke these methods for each class in turn. MonoGame provides the concept of game components to help manage this task. This involves: 1) a `GameComponentCollection` which game components are registered with, and components are implemented by extending the `GameComponent` or `DrawableGameComponent` base classes.
The `Game` implements a `GameCollection` in its `Components` property. It will iterate through all components it holds, invoking the `Update()` and `Draw()` methods within the game loop.
### GameComponent
The `GameComponent` base class implements `IGameComponent`, `IUpdatable`, and `IDisposable` interfaces. It has the following properties:
* `Enabled` - this boolean indicates if the component will be updated
* `Game` - the game this component belongs to
* `UpdateOrder` - an integer used to sort the order in which game component's `Update()` method is invoked
It also has the following virtual methods, which can be overridden:
* `Dispose(bool disposing)` - Disposes of the component
* `Initialize()` - Initializes the component; used to set/load any non-graphical content; invoked during the game's initialization step
* `Update(GameTime gameTime)` - Invoked every pass through the game loop
Finally, it also implements the following events:
* `EnabledChanged` - triggered when the `Enabled` property changes
* `UpdateOrderChanged` - triggered when the `UpdateOrder` property changes
### DrawableGameComponent
The `DrawableGameComponent` inherits from the `GameComponent` base class, and additionally implements the `IDrawable` interface. In addition to its inherited properties, it declares:
* `DrawOrder` - an integer that determines the order game components are drawn in
* `GraphicsDevice` - the graphics device used to draw the game component
* `Visible` - a boolean that determines if the game component should be drawn
It also has the additional virtual methods:
* `LoadContent()` - Loads graphical content, invoked by the game during its content loading step
* `Draw(GameTime gameTime)` - Draws the game component, invoked during the game loop
Finally, it implements the additional properties:
* `DrawOrderChanged` - triggered when the `DrawOrder` property changes
* `VisibleChanged` - triggered when the `Visible` property changes
{{% notice info %}}
The concept of Game Component espoused by MonoGame is _not_ the same one defined by the [Component Pattern](https://gameprogrammingpatterns.com/component.html), though it could potentially be leveraged to implement that pattern.
{{% /notice %}}
\ No newline at end of file
---
title: "Game Screens"
pre: "4. "
weight: 4
date: 2020-03-20T10:53:05-05:00
---
XNA offered a sample building with these ideas that further organized a game into _screens_ that has been [ported to MonoGame](https://github.com/tomizechsterson/game-state-management-monogame).This was heavily influenced by Windows Phone, and includes gestures and "tombstoning" support. A more simplified form is presented here. It organizes a game into "screens", each with its own logic and rendering, such as a menu, puzzle, cutscene, etc.
A scene manager game component manages a stack of these screens, and updates and renders the topmost. Thus, from a gameplay screen, if we trigger a cutscene it would be pushed onto the stack, play, and then pop itself from the stack. Similarly, pressing the "menu" button would push the menu screen onto the stack, leaving the player to interact with the menu instead of the game. Screens manage their transition on and off this stack - and can incorporate visual effects into the transition.
### ScreenState Enum
This enumeration represents the states a GameScreen can be in.
```csharp
/// <summary>
/// Enumerations of the possible screen states
/// </summary>
public enum ScreenState
{
TransitionOn,
Active,
TransitionOff,
Hidden
}
```
### GameScreen
The `GameScreen` class is an abstract base class that represents a single screen.
```csharp
/// <summary>
/// A screen is a single layer of game content that has
/// its own update and draw logic and can be combined
/// with other layers to create complex menus or game
/// experiences
/// </summary>
public abstract class GameScreen
{
/// <summary>
/// Indicates if this screen is a popup
/// </summary>
/// <remarks>
/// Normally when a new screen is brought over another,
/// the covered screen will transition off. However, this
/// bool indicates the covering screen is only a popup, and
/// the covered screen will remain partially visible
/// </remarks>
public bool IsPopup { get; protected set; }
/// <summary>
/// The amount of time taken for this screen to transition on
/// </summary>
protected TimeSpan TransitionOnTime {get; set;} = TimeSpan.Zero;
/// <summary>
/// The amount of time taken for this screen to transition off
/// </summary>
protected TimeSpan TransitionOffTime {get; set;} = TimeSpan.Zero;
/// <summary>
/// The screen's position in the transition
/// </summary>
/// <value>Ranges from 0 to 1 (fully on to fully off)</value>
protected float TransitionPosition { get; set; } = 1;
/// <summary>
/// The alpha value based on the current transition position
/// </summary>
public float TransitionAlpha => 1f - TransitionPosition;
/// <summary>
/// The current state of the screen
/// </summary>
public ScreenState ScreenState { get; set; } = ScreenState.TransitionOn;
/// <summary>
/// Indicates the screen is exiting for good (not simply obscured)
/// </summary>
/// <remarks>
/// There are two possible reasons why a screen might be transitioning
/// off. It could be temporarily going away to make room for another
/// screen that is on top of it, or it could be going away for good.
/// This property indicates whether the screen is exiting for real:
/// if set, the screen will automatically remove itself as soon as the
/// transition finishes.
/// </remarks>
public bool IsExiting { get; protected internal set; }
/// <summary>
/// Indicates if this screen is active
/// </summary>
public bool IsActive => !_otherScreenHasFocus && (
ScreenState == ScreenState.TransitionOn ||
ScreenState == ScreenState.Active);
private bool _otherScreenHasFocus;
/// <summary>
/// The ScreenManager in charge of this screen
/// </summary>
public ScreenManager ScreenManager { get; internal set; }
/// <summary>
/// Gets the index of the player who is currently controlling this screen,
/// or null if it is accepting input from any player.
/// </summary>
/// <remarks>
/// This is used to lock the game to a specific player profile. The main menu
/// responds to input from any connected gamepad, but whichever player makes
/// a selection from this menu is given control over all subsequent screens,
/// so other gamepads are inactive until the controlling player returns to the
/// main menu.
/// </remarks>
public PlayerIndex? ControllingPlayer { protected get; set; }
/// <summary>
/// Activates the screen. Called when the screen is added to the screen manager
/// or the game returns from being paused.
/// </summary>
public virtual void Activate() { }
/// <summary>
/// Deactivates the screen. Called when the screen is removed from the screen manager
/// or when the game is paused.
/// </summary>
public virtual void Deactivate() { }
/// <summary>
/// Unloads content for the screen. Called when the screen is removed from the screen manager
/// </summary>
public virtual void Unload() { }
/// <summary>
/// Updates the screen. Unlike HandleInput, this method is called regardless of whether the screen
/// is active, hidden, or in the middle of a transition.
/// </summary>
public virtual void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
{
_otherScreenHasFocus = otherScreenHasFocus;
if (IsExiting)
{
// If the screen is going away forever, it should transition off
ScreenState = ScreenState.TransitionOff;
if (!UpdateTransitionPosition(gameTime, TransitionOffTime, 1))
ScreenManager.RemoveScreen(this);
}
else if(coveredByOtherScreen)
{
// if the screen is covered by another, it should transition off
ScreenState = UpdateTransitionPosition(gameTime, TransitionOffTime, 1)
? ScreenState.TransitionOff
: ScreenState.Hidden;
}
else
{
// Otherwise the screen should transition on and become active.
ScreenState = UpdateTransitionPosition(gameTime, TransitionOnTime, -1)
? ScreenState.TransitionOn
: ScreenState.Active;
}
}
/// <summary>
/// Updates the TransitionPosition property based on the time
/// </summary>
/// <param name="gameTime">an object representing time in the game</param>
/// <param name="time">The amount of time the transition should take</param>
/// <param name="direction">The direction of the transition</param>
/// <returns>true if still transitioning, false if the transition is done</returns>
private bool UpdateTransitionPosition(GameTime gameTime, TimeSpan time, int direction)
{
// How much should we move by?
float transitionDelta = (time == TimeSpan.Zero)
? 1
: (float)(gameTime.ElapsedGameTime.TotalMilliseconds / time.TotalMilliseconds);
// Update the transition time
TransitionPosition += transitionDelta * direction;
// Did we reach the end of the transition?
if(direction < 0 && TransitionPosition <= 0 || direction > 0 && TransitionPosition >= 0)
{
TransitionPosition = MathHelper.Clamp(TransitionPosition, 0, 1);
return false;
}
// if not, we are still transitioning
return true;
}
/// <summary>
/// Handles input for this screen. Only called when the screen is active,
/// and not when another screen has taken focus.
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
/// <param name="input">An object representing input</param>
public virtual void HandleInput(GameTime gameTime, InputState input) { }
/// <summary>
/// Draws the GameScreen. Only called with the screen is active, and not
/// when another screen has taken the focus.
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
public virtual void Draw(GameTime gameTime) { }
/// <summary>
/// This method tells the screen to exit, allowing it time to transition off
/// </summary>
public void ExitScreen()
{
if (TransitionOffTime == TimeSpan.Zero)
ScreenManager.RemoveScreen(this); // If the screen has a zero transition time, remove it immediately
else
IsExiting = true; // Otherwise flag that it should transition off and then exit.
}
}
```
#### ScreenManager
The `ScreenManager` class manages the screens, updating and drawing only when appropriate.
```csharp
/// <summary>
/// The ScreenManager is a component which manages one or more GameScreen instance.
/// It maintains a stack of screens, calls their Update and Draw methods when
/// appropriate, and automatically routes input to the topmost screen.
/// </summary>
public class ScreenManager : DrawableGameComponent
{
private readonly List<GameScreen> _screens = new List<GameScreen>();
private readonly List<GameScreen> _tmpScreensList = new List<GameScreen>();
private readonly ContentManager _content;
private readonly InputState _input = new InputState();
private bool _isInitialized;
/// <summary>
/// A SpriteBatch shared by all GameScreens
/// </summary>
public SpriteBatch SpriteBatch { get; private set; }
/// <summary>
/// A SpriteFont shared by all GameScreens
/// </summary>
public SpriteFont MenuFont { get; private set; }
/// <summary>
/// Constructs a new ScreenManager
/// </summary>
/// <param name="game">The game this ScreenManager belongs to</param>
public ScreenManager(Game game) : base(game)
{
game.Components.Add(this);
_content = new ContentManager(game.Services, "Content");
}
/// <summary>
/// Initializes the ScreenManager
/// </summary>
public override void Initialize()
{
base.Initialize();
_isInitialized = true;
}
/// <summary>
/// Loads content for the ScreenManager and its screens
/// </summary>
protected override void LoadContent()
{
SpriteBatch = new SpriteBatch(GraphicsDevice);
MenuFont = _content.Load<SpriteFont>("MenuFont");
// Tell each of the screens to load thier content
foreach(var screen in _screens)
{
screen.Activate();
}
}
/// <summary>
/// Unloads content for the ScreenManager's screens
/// </summary>
protected override void UnloadContent()
{
foreach(var screen in _screens)
{
screen.Unload();
}
}
/// <summary>
/// Updates all screens managed by the ScreenManager
/// </summary>
/// <param name="gameTime">An object representing time in the game</param>
public override void Update(GameTime gameTime)
{
// Read in the keyboard and gamepad
_input.Update();
// Make a copy of the screen list, to avoid confusion if
// the process of updating a screen adds or removes others
_tmpScreensList.Clear();
_tmpScreensList.AddRange(_screens);
bool otherScreenHasFocus = !Game.IsActive;
bool coveredByOtherScreen = false;
while(_tmpScreensList.Count > 0)
{
// Pop the topmost screen
var screen = _tmpScreensList[_tmpScreensList.Count - 1];
_tmpScreensList.RemoveAt(_tmpScreensList.Count - 1);
screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);