#region CPL License /* Nuclex Framework Copyright (C) 2002-2011 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion using System; using System.Collections.Generic; using Microsoft.Xna.Framework; namespace Nuclex.Game.States { /// Manages the game states and updates the active game state public class GameStateManager : DrawableComponent, IGameStateService, IDisposable { /// Initializes a new game state manager public GameStateManager() { this.gameStates = new List>(); this.updateableStates = new List(); this.drawableStates = new List(); } /// Initializes a new game state manager /// /// Services container the game state manager will add itself to /// public GameStateManager(GameServiceContainer gameServices) : this() { this.gameServices = gameServices; gameServices.AddService(typeof(IGameStateService), this); } /// Immediately releases all resources used by the component public void Dispose() { leaveAllActiveStates(); // Unregister the service if we have registered it before if (this.gameServices != null) { object registeredService = this.gameServices.GetService(typeof(IGameStateService)); if (ReferenceEquals(registeredService, this)) { this.gameServices.RemoveService(typeof(IGameStateService)); } } } /// /// Whether the game state manager should automatically dispose game states /// that are dropped from its stack /// public bool DisposeDroppedStates { get { return this.disposeDroppedStates; } set { this.disposeDroppedStates = value; } } /// Pauses the currently active state public void Pause() { if (this.gameStates.Count > 0) { this.gameStates[this.gameStates.Count - 1].Key.Pause(); } } /// Resumes the currently active state public void Resume() { if (this.gameStates.Count > 0) { this.gameStates[this.gameStates.Count - 1].Key.Resume(); } } /// Pushes the specified state onto the state stack /// State that will be pushed onto the stack public void Push(IGameState state) { Push(state, GameStateModality.Exclusive); } /// Pushes the specified state onto the state stack /// State that will be pushed onto the stack /// /// Behavior of the game state in relation to the state(s) below it on the stack /// public void Push(IGameState state, GameStateModality modality) { Pause(); // If this game state is modal, take all game states that came before it // from the draw and update lists if (modality == GameStateModality.Exclusive) { this.drawableStates.Clear(); this.updateableStates.Clear(); } // Add the new state to the update and draw lists if it implements // the required interfaces this.gameStates.Add(new KeyValuePair(state, modality)); appendToUpdateableAndDrawableList(state); // State is set, now try to enter it try { state.Enter(); } catch (Exception) { Pop(); throw; } } /// Takes the currently active game state from the stack /// The game state that has been popped from the stack public IGameState Pop() { int lastStateIndex = this.gameStates.Count - 1; if (lastStateIndex < 0) { throw new InvalidOperationException("No game states are on the stack"); } KeyValuePair old = this.gameStates[lastStateIndex]; IGameState oldState = old.Key; // Notify the currently active state that it's being left and take it // from the stack of active states oldState.Leave(); this.gameStates.RemoveAt(lastStateIndex); // Now we need to remove the popped state from our update and draw lists. // If the popped state was exclusive, our lists are empty and we need to // rebuild them. Otherwise, we can simply remove the lastmost entry. if (old.Value == GameStateModality.Exclusive) { this.updateableStates.Clear(); this.drawableStates.Clear(); rebuildUpdateableAndDrawableListRecursively(lastStateIndex - 1); } else { removeFromUpdateableAndDrawableList(old.Key); } // If the user desires so, dispose the dropped state disposeIfSupportedAndDesired(old.Key); // Resume the state that has now become the top of the stack Resume(); return oldState; } /// Switches the game to the specified state /// State the game will be switched to /// The game state that was replaced on the stack /// /// This replaces the running game state on the stack with the specified state. /// public IGameState Switch(IGameState state) { return Switch(state, GameStateModality.Exclusive); } /// Switches the game to the specified state /// State the game will be switched to /// /// Behavior of the game state in relation to the state(s) below it on the stack /// /// The game state that was replaced on the stack /// /// This replaces the running game state on the stack with the specified state. /// public IGameState Switch(IGameState state, GameStateModality modality) { int stateCount = this.gameStates.Count; if (stateCount == 0) { Push(state, modality); return null; } int lastStateIndex = stateCount - 1; KeyValuePair old = this.gameStates[lastStateIndex]; IGameState previousState = old.Key; // Notify the previous state that it's being left and kill it if desired previousState.Leave(); disposeIfSupportedAndDesired(previousState); // If the switched-to state is exclusive, we need to clear the update // and draw lists. If not, depending on whether the previous state was // a popup state, we might have to if (old.Value == GameStateModality.Popup) { removeFromUpdateableAndDrawableList(previousState); } else { this.updateableStates.Clear(); this.drawableStates.Clear(); } // Now swap out the state and put it in the update and draw lists. If we're // switching from an exclusive to a pop-up state, the draw and update lists need // to be rebuilt. var newState = new KeyValuePair(state, modality); this.gameStates[lastStateIndex] = newState; if (old.Value == GameStateModality.Exclusive && modality == GameStateModality.Popup) { rebuildUpdateableAndDrawableListRecursively(lastStateIndex); } else { appendToUpdateableAndDrawableList(state); } // Let the state know that it has been entered state.Enter(); return previousState; } /// The currently active game state. Can be null. public IGameState ActiveState { get { int count = this.gameStates.Count; if (count == 0) { return null; } else { return this.gameStates[count - 1].Key; } } } /// Updates the active game state /// Snapshot of the game's timing values public override void Update(GameTime gameTime) { for (int index = 0; index < this.updateableStates.Count; ++index) { var updateable = this.updateableStates[index]; if (updateable.Enabled) { updateable.Update(gameTime); } } } /// Draws the active game state /// Snapshot of the game's timing values public override void Draw(GameTime gameTime) { for (int index = 0; index < this.drawableStates.Count; ++index) { var drawable = this.drawableStates[index]; if (drawable.Visible) { this.drawableStates[index].Draw(gameTime); } } } /// /// Disposes the specified state if disposal is enabled and the state implements /// the IDisposable interface /// /// State that will be disposed if desired and supported private void disposeIfSupportedAndDesired(IGameState state) { if (this.disposeDroppedStates) { var disposable = state as IDisposable; if (disposable != null) { disposable.Dispose(); } } } /// /// Rebuilds the updateable and drawable lists by recursively going up /// the stacked game states until the top or an exclusive game state /// is reached /// /// Index of the game state to start at private void rebuildUpdateableAndDrawableListRecursively(int index) { if (index < 0) { return; } if (this.gameStates[index].Value != GameStateModality.Exclusive) { rebuildUpdateableAndDrawableListRecursively(index - 1); } appendToUpdateableAndDrawableList(this.gameStates[index].Key); } /// /// Removes the specified state from the update and draw lists if it is on /// the top of those lists /// /// /// State that will be removed from the update and draw lists /// private void removeFromUpdateableAndDrawableList(IGameState state) { int lastDrawableIndex = this.drawableStates.Count - 1; if (lastDrawableIndex > -1) { if (ReferenceEquals(this.drawableStates[lastDrawableIndex], state)) { this.drawableStates.RemoveAt(lastDrawableIndex); } } int lastUpdateableIndex = this.updateableStates.Count - 1; if (lastUpdateableIndex > -1) { if (ReferenceEquals(this.updateableStates[lastUpdateableIndex], state)) { this.updateableStates.RemoveAt(lastUpdateableIndex); } } } /// Leaves all currently active game states private void leaveAllActiveStates() { for (int index = this.gameStates.Count - 1; index >= 0; --index) { IGameState state = this.gameStates[index].Key; state.Leave(); disposeIfSupportedAndDesired(state); this.gameStates.RemoveAt(index); } this.drawableStates.Clear(); this.updateableStates.Clear(); } /// Appends the specified state to the update and draw lists /// State that will be appended to the lists private void appendToUpdateableAndDrawableList(IGameState state) { IUpdateable updateable = state as IUpdateable; if (updateable != null) { this.updateableStates.Add(updateable); } IDrawable drawable = state as IDrawable; if (drawable != null) { this.drawableStates.Add(drawable); } } /// /// Game service container the game state manager has registered itself in /// private GameServiceContainer gameServices; /// Whether the game state manager should dispose dropped states private bool disposeDroppedStates; /// Currently active game states /// /// The game state manager supports multiple active game states. For example, /// a menu might appear on top of the running game. Only the topmost active /// state receives input through the game /// private List> gameStates; /// Currently active game states implementing IUpdateable private List updateableStates; /// Currently active game states implementing IDrawable private List drawableStates; } } // namespace Nuclex.Game.States