StorageDeviceManager

From XNAWiki
Jump to: navigation, search


Introduction

There are a few things that make storage on the Xbox 360 difficult. Some of these are multiple devices, devices being removed mid-game, and users canceling the selector screen.

Here is one method of using a GameComponent to manage a StorageDevice, including prompting for device disconnects and user cancellation of the device selection screen.

Example

Using the StorageDeviceManager can be a very simple process. To get the very minimal support, you simply create the device and add it to the Components collection (while making sure to add the GamerServicesComponent):

(In your game's constructor)

StorageDeviceManager manager = new StorageDeviceManager(this);
manager.PromptForDevice();
Components.Add(manager)

The above code creates a player-agnostic storage device with no minimum required space. You can pull out memory cards and see that the component automatically prompts the user to see if they want to select a device or not and follows that up by showing the device selector if desired. In addition, the same logic is applied if the user cancels the storage device selector.

Also note that the PromptForDevice method does not have to be called before adding the manager to the game component collection and, in fact, can be called anywhere you want in your game. You could very well create and add the component in the constructor, but not call the PromptForDevice method until a certain point in your game.

The code below demonstrates a much more complex game scenario allowing the game to customize the component's logic of prompting for device disconnects and cancellation events.

public class Game1 : Game
{
   public Game1()
   {
      // usual constructor stuff
 
      StorageDeviceManager deviceManager = new StorageDeviceManager(this);
      Components.Add(deviceManager);
      deviceManager.DeviceSelectorCanceled += DeviceSelectorCanceled;
      deviceManager.DeviceDisconnected += DeviceDisconnected;
      deviceManager.PromptForDevice();
   }
 
   void DeviceDisconnected(object sender, StorageDeviceEventArgs e)
   {
      // force the user to choose a new storage device
      e.EventResponse = StorageDeviceSelectorEventResponse.Force;
   }
 
   void DeviceSelectorCanceled(object sender, StorageDeviceEventArgs e)
   {
      // force the user to choose a new storage device
      e.EventResponse = StorageDeviceSelectorEventResponse.Force;
   }
 
   // rest of game here
}

The component provides a number of constructors, properties, and events to help you handle the various scenarios in a custom fashion.

Source Code

The following is the entire code for the StorageDeviceManager.

/// <summary>
/// Defines an action in response to a StorageDeviceEventArgs
/// </summary>
public enum StorageDeviceSelectorEventResponse
{
   /// <summary>
   /// Do nothing.
   /// </summary>
   None,
 
   /// <summary>
   /// Prompt the user to select a new storage device.
   /// </summary>
   Prompt,
 
   /// <summary>
   /// Force the user to select a new storage device.
   /// </summary>
   Force
}
 
public class StorageDeviceEventArgs : EventArgs
{
   /// <summary>
   /// Gets or sets the desired response to the event.
   /// </summary>
   public StorageDeviceSelectorEventResponse EventResponse { get; set; }
}
 
public class StorageDevicePromptEventArgs : EventArgs
{
   /// <summary>
   /// Gets whether or not the user has chosen to select a new device.
   /// If true, the StorageDeviceManager will automatically prompt for 
   /// the new device.
   /// </summary>
   public bool PromptForDevice { get; set; }
}
 
public class StorageDeviceManager : GameComponent
{
   // the text used for the four prompts used by the manager
   private const string ReselectStorageDeviceText =
      "No storage device was selected. Would you like to re-select the storage device?";
   private const string DisconnectReselectDeviceText =
      "An active storage device has been disconnected. Would you like to select a new storage device?";
   private const string ForceReselectDeviceText =
      "No storage device was selected. A storage device is required to continue. Select Ok to choose a storage device.";
   private const string ForceDisconnectReselectText =
      "An active storage device has been disconnected. " +
      "A storage device is required to continue. Select Ok to choose a storage device.";
 
   // was the device connected last frame?
   private bool wasDeviceConnected;
 
   // should the Guide.BeginShowStorageDeviceSelector be called?
   private bool showDeviceSelector;
 
   // should we prompt the user to optionally have them select a new device for canceling the selector?
   private bool promptToReSelectDevice;
 
   // should we prompt the user to force them to select a new device for canceling the selector?
   private bool promptToForceReselect;
 
   // should we prompt the user to optionally have them select a new device after a device disconnect?
   private bool promptForDisconnect;
 
   // should we prompt the user to force them to select a new device after a device disconnect?
   private bool promptForDisconnectForced;
 
   // keep one instance of each of the event arguments to avoid garbage creation
   private readonly StorageDeviceEventArgs eventArgs = new StorageDeviceEventArgs();
   private readonly StorageDevicePromptEventArgs promptEventArgs = new StorageDevicePromptEventArgs();
 
   /// <summary>
   /// Fired when a StorageDevice is successfully selected.
   /// </summary>
   public event EventHandler DeviceSelected;
 
   /// <summary>
   /// Fired when the StorageDevice selector is canceled.
   /// </summary>
   public event EventHandler<StorageDeviceEventArgs> DeviceSelectorCanceled;
 
   /// <summary>
   /// Fired when the non-forced reselect prompt is closed.
   /// </summary>
   public event EventHandler<StorageDevicePromptEventArgs> DevicePromptClosed;
 
   /// <summary>
   /// Fired when the StorageDevice becomes disconnected.
   /// </summary>
   public event EventHandler<StorageDeviceEventArgs> DeviceDisconnected;
 
   /// <summary>
   /// Gets the StorageDevice being managed.
   /// </summary>
   public StorageDevice Device { get; private set; }
 
   /// <summary>
   /// Gets the player (if any) used for the StorageDevice.
   /// </summary>
   public PlayerIndex? Player { get; private set; }
 
   /// <summary>
   /// Gets or sets the player to prompt if the storage device is player-agnostic.
   /// </summary>
   public PlayerIndex PlayerToPrompt { get; set; }
 
   /// <summary>
   /// Gets the amount of space required on the StorageDevice.
   /// </summary>
   public int RequiredBytes { get; private set; }
 
   /// <summary>
   /// Creates a new player-agnostic StorageDevice with no required amount of free space.
   /// </summary>
   /// <param name="game">
   /// The game to which the StorageDeviceManager will be added. The component does not add itself.
   /// </param>
   public StorageDeviceManager(Game game)
      : this(game, null, 0) { }
 
   /// <summary>
   /// Creates a new player-specific StorageDevice wiht no required amount of free space.
   /// </summary>
   /// <param name="game">
   /// The game to which the StorageDeviceManager will be added. The component does not add itself.
   /// </param>
   /// <param name="player">The player to prompt for the StorageDevice.</param>
   public StorageDeviceManager(Game game, PlayerIndex player)
      : this(game, player, 0) { }
 
   /// <summary>
   /// Creates a new player-agnostic StorageDevice with a required amount of free space.
   /// </summary>
   /// <param name="game">
   /// The game to which the StorageDeviceManager will be added. The component does not add itself.
   /// </param>
   /// <param name="requiredBytes">The amount of space (in bytes) required.</param>
   public StorageDeviceManager(Game game, int requiredBytes)
      : this(game, null, requiredBytes) { }
 
   /// <summary>
   /// Creates a new player-specific StorageDevice with a required amount of free space.
   /// </summary>
   /// <param name="game">
   /// The game to which the StorageDeviceManager will be added. The component does not add itself.
   /// </param>
   /// <param name="player">The player to prompt for the StorageDevice.</param>
   /// <param name="requiredBytes">The amount of space (in bytes) required.</param>
   // we cast the player argument to a PlayerIndex? to tell the compiler to call the private constructor.
   public StorageDeviceManager(Game game, PlayerIndex player, int requiredBytes)
      : this(game, (PlayerIndex?)player, requiredBytes) { }
 
   private StorageDeviceManager(Game game, PlayerIndex? player, int requiredBytes)
      : base(game)
   {
      // store the arguments
      Player = player;
      RequiredBytes = requiredBytes;
 
      PlayerToPrompt = PlayerIndex.One;
   }
 
   /// <summary>
   /// Instructs the manager to prompt the user to select a new device.
   /// </summary>
   public void PromptForDevice()
   {
      // simply flip to true
      showDeviceSelector = true;
   }
 
   public override void Update(GameTime gameTime)
   {
      // if the device has just become disconnected, fire the event to see if we need to prompt for a new one
      if (Device != null && !Device.IsConnected && wasDeviceConnected)
         FireDeviceDisconnectedEvent();
 
      // use a try/catch in case of the following conditions:
      // 1) GamerServicesComponent is not added. In this case Guide.IsVisible throws an exception.
      // 2) Guide.IsVisible returns false but Guide opens (from user input) before the code displays
      //    the Guide. This would cause the Guide to throw an exception.
      try
      {
         // if the Guide is not visible...
         if (!Guide.IsVisible)
         {
            // if we are to show the device selector...
            if (showDeviceSelector)
            {
               // don't show device selector next frame; necessary if the user
               // has only one storage device.
               showDeviceSelector = false;
 
               // show the selector based on whether we have a player-specific or
               // player-agnostic storage device.
               if (Player.HasValue)
               {
                  Guide.BeginShowStorageDeviceSelector(
                     Player.Value,
                     RequiredBytes,
                     0,
                     deviceSelectorCallback,
                     null);
               }
               else
               {
                  Guide.BeginShowStorageDeviceSelector(
                     RequiredBytes,
                     0,
                     deviceSelectorCallback,
                     null);
               }
            }
 
            // if we are prompting to see if the user wants a new device due to canceling the selector...
            else if (promptToReSelectDevice)
            {
                if (Player.HasValue)
                {
                    Guide.BeginShowMessageBox(
                       Player.Value,
                       "Reselect Storage Device?",
                       ReselectStorageDeviceText,
                       new[] { "Yes. Select new device.", "No. Continue without device." },
                       0,
                       MessageBoxIcon.None,
                       reselectPromptCallback,
                       null);
                }
                else
                {
                    Guide.BeginShowMessageBox(
                       "Reselect Storage Device?",
                       ReselectStorageDeviceText,
                       new[] { "Yes. Select new device.", "No. Continue without device." },
                       0,
                       MessageBoxIcon.None,
                       reselectPromptCallback,
                       null);
                }
            }
 
            // if we are prompting to see if the user wants a new device due to a disconnect...
            else if (promptForDisconnect)
            {
                if (Player.HasValue)
                {
                    Guide.BeginShowMessageBox(
                       Player.Value,
                       "Storage Device Disconnected",
                       DisconnectReselectDeviceText,
                       new[] { "Yes. Select new device.", "No. Continue without device." },
                       0,
                       MessageBoxIcon.None,
                       reselectPromptCallback,
                       null);
                }
                else
                {
                    Guide.BeginShowMessageBox(
                       "Storage Device Disconnected",
                       DisconnectReselectDeviceText,
                       new[] { "Yes. Select new device.", "No. Continue without device." },
                       0,
                       MessageBoxIcon.None,
                       reselectPromptCallback,
                       null);
                }
            }
 
            // if we are prompting to force a reselect of the device due to canceling the selector...
            else if (promptToForceReselect)
            {
                if (Player.HasValue)
                {
                    Guide.BeginShowMessageBox(
                       Player.Value,
                       "Reselect Storage Device",
                       ForceReselectDeviceText,
                       new[] { "Ok" },
                       0,
                       MessageBoxIcon.None,
                       forcePromptCallback,
                       null);
                }
                else
                {
                    Guide.BeginShowMessageBox(
                       "Reselect Storage Device",
                       ForceReselectDeviceText,
                       new[] { "Ok" },
                       0,
                       MessageBoxIcon.None,
                       forcePromptCallback,
                       null);
                }
            }
 
            // if we are prompting to force a reselect of the device due to a disconnect...
            else if (promptForDisconnectForced)
            {
                if (Player.HasValue)
                {
                    Guide.BeginShowMessageBox(
                       Player.Value,
                       "Storage Device Disconnected",
                       ForceDisconnectReselectText,
                       new[] { "Ok" },
                       0,
                       MessageBoxIcon.None,
                       forcePromptCallback,
                       null);
                }
                else
                {
                    Guide.BeginShowMessageBox(
                       "Storage Device Disconnected",
                       ForceDisconnectReselectText,
                       new[] { "Ok" },
                       0,
                       MessageBoxIcon.None,
                       forcePromptCallback,
                       null);
                }
            }
         }
      }
 
      // catch and write out any relevant exceptions for later debugging
      catch (GamerServicesNotAvailableException e)
      {
         Debug.WriteLine(e.Message);
      }
 
      catch (GuideAlreadyVisibleException e)
      {
         Debug.WriteLine(e.Message);
      }
 
      // store the state of the device's connection
      wasDeviceConnected = Device != null && Device.IsConnected;
   }
 
   /// <summary>
   /// The callback used for either of our forced reselect prompts.
   /// </summary>
   /// <param name="ar">The prompt results.</param>
   private void forcePromptCallback(IAsyncResult ar)
   {
      // no more need to prompt
      promptToForceReselect = false;
      promptForDisconnectForced = false;
 
      // just have to end it.
      Guide.EndShowMessageBox(ar);
 
      // get the device
      showDeviceSelector = true;
   }
 
   /// <summary>
   /// The callback used for either of our non-forced reselect prompts.
   /// </summary>
   /// <param name="ar">The prompt results.</param>
   private void reselectPromptCallback(IAsyncResult ar)
   {
      // no more need to prompt
      promptForDisconnect = false;
      promptToReSelectDevice = false;
 
      // get the result of the message box
      int? choice = Guide.EndShowMessageBox(ar);
 
      // get the device if the user chose the first option
      showDeviceSelector = choice.HasValue && choice.Value == 0;
 
      // fire an event for the game to know the result of the prompt
      promptEventArgs.PromptForDevice = showDeviceSelector;
      if (DevicePromptClosed != null)
         DevicePromptClosed(this, promptEventArgs);
   }
 
   /// <summary>
   /// The callback used for the device selector.
   /// </summary>
   /// <param name="ar">The selector results.</param>
   private void deviceSelectorCallback(IAsyncResult ar)
   {
      // get the chosen device
      Device = Guide.EndShowStorageDeviceSelector(ar);
 
      // if a device was chosen...
      if (Device != null)
      {
         // fire the event
         if (DeviceSelected != null)
            DeviceSelected(this, EventArgs.Empty);
      }
 
      // otherwise
      else
      {
         // initialize the event args
         eventArgs.EventResponse = StorageDeviceSelectorEventResponse.Prompt;
 
         // fire the cancelation event to allow customization of the process
         if (DeviceSelectorCanceled != null)
            DeviceSelectorCanceled(this, eventArgs);
 
         // handle the results of the event
         HandleEventArgResults();
      }
   }
 
   /// <summary>
   /// Fires off the event for a device becoming disconnected and handles the result.
   /// </summary>
   private void FireDeviceDisconnectedEvent()
   {
      // initialize the event args
      eventArgs.EventResponse = StorageDeviceSelectorEventResponse.Prompt;
 
      // fire the disconnection event to allow customization of the process
      if (DeviceDisconnected != null)
         DeviceDisconnected(this, eventArgs);
 
      // handle the results of the event
      HandleEventArgResults();
   }
 
   /// <summary>
   /// Handles the result of the DeviceSelectorCanceled or DeviceDisconnected events.
   /// </summary>
   private void HandleEventArgResults()
   {
      // clear the Device reference
      Device = null;
 
      // determine the next action...
      switch (eventArgs.EventResponse)
      {
         // will have the manager prompt the user with the option of reselecting the storage device
         case StorageDeviceSelectorEventResponse.Prompt:
            if (wasDeviceConnected)
                 promptForDisconnect = true;
            else
                 promptToReSelectDevice = true;
            break;
 
         // will have the manager prompt the user that the device must be selected
         case StorageDeviceSelectorEventResponse.Force:
            if (wasDeviceConnected)
                 promptForDisconnectForced = true;
            else
                 promptToForceReselect = true;
            break;
 
         // will have the manager do nothing
         default:
            promptForDisconnect = false;
            promptForDisconnectForced = false;
            promptToForceReselect = false;
            showDeviceSelector = false;
            break;
      }
   }
}

Note on Garbage

This method will poll your StorageDevice constantly to see if it is still connected. Unfortunately, calling StorageDevice.IsConnected creates quite a bit of garbage in the form of string references. This can cause your game to stutter.

The above code will actually call this property twice, which can be refactored into a single time by storing the result in a local variable:

bool deviceIsConnected = false;
 
if (Device != null)
    deviceIsConnected = Device.IsConnected;
 
// if the device has just become disconnected, fire the event to see if we need to prompt for a new one
if (Device != null && !deviceIsConnected && wasDeviceConnected)
    FireDeviceDisconnectedEvent();
 
// skip a bunch of code
 
// store the state of the device's connection
wasDeviceConnected = Device != null && deviceIsConnected;

This won't fix the problem completely, but at least it will halve the amount of garbage created. A better solution is to only query for connection when the storagedevice is actually needed, but that requires additional rewriting of the original code.