//----------------------------------------------------------------------------- // NetworkBusyScreen.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using System.Threading.Tasks; using Microsoft.Xna.Framework.Input; namespace NetworkStateManagement { /// /// When an asynchronous network operation (for instance searching for or joining a /// session) is in progress, this screen displays a busy indicator and blocks input. /// It monitors a Task<T> returned by the async call, showing the indicator while /// the task is running. When the task completes, it raises an event with the /// operation result (or null on failure), then automatically dismisses itself. /// Because this screen sits on top while the async operation is in progress, it /// captures all user input to prevent interaction with underlying screens until /// the operation completes. /// class NetworkBusyScreen : GameScreen { readonly Task task; readonly System.Threading.CancellationTokenSource cts; bool completionRaised; Texture2D gradientTexture; Texture2D catTexture; event EventHandler operationCompleted; public event EventHandler OperationCompleted { add { operationCompleted += value; } remove { operationCompleted -= value; } } /// /// Constructs a network busy screen for the specified asynchronous operation. /// Accepts a Task<T> representing the in-flight operation. /// public NetworkBusyScreen(Task task) { this.task = task; this.cts = null; IsPopup = true; TransitionOnTime = TimeSpan.FromSeconds(0.1); TransitionOffTime = TimeSpan.FromSeconds(0.2); } /// /// Overload that accepts a CancellationTokenSource to allow user cancellation. /// public NetworkBusyScreen(Task task, System.Threading.CancellationTokenSource cts) { this.task = task; this.cts = cts; IsPopup = true; TransitionOnTime = TimeSpan.FromSeconds(0.1); TransitionOffTime = TimeSpan.FromSeconds(0.2); } /// /// Loads graphics content for this screen. This uses the shared ContentManager /// provided by the Game class, so the content will remain loaded forever. /// Whenever a subsequent NetworkBusyScreen tries to load this same content, /// it will just get back another reference to the already loaded data. /// public override void LoadContent() { ContentManager content = ScreenManager.Game.Content; gradientTexture = content.Load("gradient"); catTexture = content.Load("cat"); } /// /// Updates the NetworkBusyScreen, checking whether the underlying Task has /// completed and raising OperationCompleted when it does. /// public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); // Optional: allow user to cancel (Esc or B) if (!completionRaised && cts != null) { var kb = Keyboard.GetState(); if (kb.IsKeyDown(Keys.Escape)) { cts.Cancel(); } var gp = GamePad.GetState(PlayerIndex.One); if (gp.IsConnected && gp.IsButtonDown(Buttons.B)) { cts.Cancel(); } } // Has our asynchronous operation completed? if (!completionRaised && task != null && task.IsCompleted) { object resultObject = default(T); Exception error = null; try { // Accessing Result will throw if the task faulted/canceled. // We catch here and allow handlers to present error UI. resultObject = task.Result; } catch (Exception ex) { // Leave result as default (usually null) to signal failure to handlers. error = (task?.Exception?.GetBaseException()) ?? ex; } var handler = operationCompleted; if (handler != null) { handler(this, new OperationCompletedEventArgs(resultObject, error)); } completionRaised = true; // Clear handlers to avoid reentry if this screen lingers during transition off operationCompleted = null; ExitScreen(); } } /// /// Draws the NetworkBusyScreen. /// public override void Draw(GameTime gameTime) { SpriteBatch spriteBatch = ScreenManager.SpriteBatch; SpriteFont font = ScreenManager.Font; string message = Resources.NetworkBusy; const int hPad = 32; const int vPad = 16; // Center the message text in the viewport. Viewport viewport = ScreenManager.GraphicsDevice.Viewport; Vector2 viewportSize = new Vector2(viewport.Width, viewport.Height); Vector2 textSize = font.MeasureString(message); // Add enough room to spin a cat. Vector2 catSize = new Vector2(catTexture.Width); textSize.X = Math.Max(textSize.X, catSize.X); textSize.Y += catSize.Y + vPad; Vector2 textPosition = (viewportSize - textSize) / 2; // The background includes a border somewhat larger than the text itself. Rectangle backgroundRectangle = new Rectangle((int)textPosition.X - hPad, (int)textPosition.Y - vPad, (int)textSize.X + hPad * 2, (int)textSize.Y + vPad * 2); // Fade the popup alpha during transitions. Color color = Color.White * TransitionAlpha; spriteBatch.Begin(); // Draw the background rectangle. spriteBatch.Draw(gradientTexture, backgroundRectangle, color); // Draw the message box text. spriteBatch.DrawString(font, message, textPosition, color); // Draw the spinning cat progress indicator. float catRotation = (float)gameTime.TotalGameTime.TotalSeconds * 3; Vector2 catPosition = new Vector2(textPosition.X + textSize.X / 2, textPosition.Y + textSize.Y - catSize.Y / 2); spriteBatch.Draw(catTexture, catPosition, null, color, catRotation, catSize / 2, 1, SpriteEffects.None, 0); spriteBatch.End(); } } }