MonoGame笔记(X5)Game State Management (二)

上一篇笔记中提到的进阶版, 官方有一个样例http://xbox.create.msdn.com/en-US/education/catalog/sample/game_state_management, 是对该版本的一个加强, 实用程度和评价都很高, 非常具有参考价值. XNA官方样例中不少都是采用了这种方式, 例如RolePlayingGameStarterKit.

官方的介绍如下:

The ScreenManager class is a reusable component that maintains a stack of one or more GameScreen instances. It coordinates the transitions from one screen to another, and takes care of routing user input to whichever screen is on top of the stack.

Each screen class (including the actual gameplay, which is just another screen) derives from GameScreen. This provides Update, HandleInput, and Draw methods, plus some logic for managing the transition state. GameScreen doesn’t actually implement any transition rendering effects, however: it merely provides information such as “you are currently 30% of the way through transitioning off,” leaving it up to the derived screen classes to do something sensible with that information in their drawing code. This makes it easy for screens to implement different visual effects on top of the same underlying transition infrastructure.

这里提到的transitioning off是指淡出状态, transitioning on是淡入, 共有四种状态.

1
2
3
4
5
6
7
public enum ScreenState
{
TransitionOn,
Active,
TransitionOff,
Hidden,
}

类图和上一篇笔记中的类图结构差不多. 样例中有以下几种GameScreen, 是一个游戏当中必须的游戏界面. 其他各种和具体游戏相关的Screen见RolePlayingGameStarterKit

BackgroundScreen
GameplayScreen
LoadingScreen
MenuScreen (MenuEntry)
—–MainMenuScreen
—–OptionsMenuScreen
—–PauseMenuScreen
MessageBoxScreen

BackgroundScreen

1
2
3
4
/// The background screen sits behind all the other menu screens.
/// It draws a background image that remains fixed in place regardless
/// of whatever transitions the screens on top of it may be doing.
/// 和菜单界面一起

GameplayScreen

1
2
3
/// This screen implements the actual game logic. It is just a
/// placeholder to get the idea across: you'll probably want to
/// put some more interesting gameplay in here!

LoadingScreen

1
2
3
4
5
6
7
8
9
10
11
12
/// The loading screen coordinates transitions between the menu system and the
/// game itself. Normally one screen will transition off at the same time as
/// the next screen is transitioning on, but for larger transitions that can
/// take a longer time to load their data, we want the menu system to be entirely
/// gone before we start loading the game. This is done as follows:
///
/// - Tell all the existing screens to transition off.
/// - Activate a loading screen, which will transition on at the same time.
/// - The loading screen watches the state of the previous screens.
/// - When it sees they have finished transitioning off, it activates the real
/// next screen, which may take a long time to load its data. The loading
/// screen will be the only thing displayed while this load is taking place.
1
/// The main menu screen is the first thing displayed when the game starts up.

OptionsMenuScreen

1
2
3
/// The options screen is brought up over the top of the main menu
/// screen, and gives the user a chance to configure the game
/// in various hopefully useful ways.

PauseMenuScreen

1
2
/// The pause menu comes up over the top of the game,
/// giving the player options to resume or quit.

MessageBoxScreen

1
2
/// A popup message box screen, used to display "are you sure?"
/// confirmation messages.

初始游戏启动时只有MainMenuScreen存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// The main game constructor.
/// </summary>
public GameStateManagementGame()
{
//...

// Create the screen manager component.
screenManager = new ScreenManager(this);

Components.Add(screenManager);

// Activate the first screens.
screenManager.AddScreen(new BackgroundScreen(), null);
screenManager.AddScreen(new MainMenuScreen(), null);
}

方向键选择Play Game, 按下Enter键, 调用的是MainMenuScreen的

1
2
3
4
5
6
7
8
/// <summary>
/// Event handler for when the Play Game menu entry is selected.
/// </summary>
void PlayGameMenuEntrySelected(object sender, PlayerIndexEventArgs e)
{
LoadingScreen.Load(ScreenManager, true, e.PlayerIndex,
new GameplayScreen());
}

LoadingScreen的Load方法和Update方法. Update会等其他GameScreen都transitioning Off(淡出), 然后加载GamePlayScreen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/// <summary>
/// Activates the loading screen.
/// </summary>
public static void Load(ScreenManager screenManager, bool loadingIsSlow,
PlayerIndex? controllingPlayer,
params GameScreen[] screensToLoad)
{
// Tell all the current screens to transition off.
foreach (GameScreen screen in screenManager.GetScreens())
screen.ExitScreen();

// Create and activate the loading screen.
LoadingScreen loadingScreen = new LoadingScreen(screenManager,
loadingIsSlow,
screensToLoad);

screenManager.AddScreen(loadingScreen, controllingPlayer);
}

/// <summary>
/// Updates the loading screen.
/// </summary>
public override void Update(GameTime gameTime, bool otherScreenHasFocus,
bool coveredByOtherScreen)
{
base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

// If all the previous screens have finished transitioning
// off, it is time to actually perform the load.
if (otherScreensAreGone)
{
ScreenManager.RemoveScreen(this);

foreach (GameScreen screen in screensToLoad)
{
if (screen != null)
{
ScreenManager.AddScreen(screen, ControllingPlayer);
}
}

// Once the load has finished, we use ResetElapsedTime to tell
// the game timing mechanism that we have just finished a very
// long frame, and that it should not try to catch up.
ScreenManager.Game.ResetElapsedTime();
}
}

ScreenManager的AddScreen方法内部会调用GamePlayScreen的LoadContent方法, 加载各类游戏资源.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Adds a new screen to the screen manager.
/// </summary>
public void AddScreen(GameScreen screen, PlayerIndex? controllingPlayer)
{
screen.ControllingPlayer = controllingPlayer;
screen.ScreenManager = this;
screen.IsExiting = false;

// If we have a graphics device, tell the screen to load content.
if (isInitialized)
{
screen.LoadContent();
}

screens.Add(screen);

// update the TouchPanel to respond to gestures this screen is interested in
TouchPanel.EnabledGestures = screen.EnabledGestures;
}

GamePlayScreen的LoadContent方法, 注意ResetElapsedTime用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// Load graphics content for the game.
/// </summary>
public override void LoadContent()
{
if (content == null)
content = new ContentManager(ScreenManager.Game.Services, "Content");

gameFont = content.Load<SpriteFont>("gamefont");

// A real game would probably have more content than this sample, so
// it would take longer to load. We simulate that by delaying for a
// while, giving you a chance to admire the beautiful loading screen.
Thread.Sleep(1000);

// once the load has finished, we use ResetElapsedTime to tell the game's
// timing mechanism that we have just finished a very long frame, and that
// it should not try to catch up.
ScreenManager.Game.ResetElapsedTime();
}

Game的ResetElapsedTime方法

1
2
3
4
5
6
7
8
9
public void ResetElapsedTime()
{
Platform.ResetElapsedTime();
_gameTimer.Reset();
_gameTimer.Start();
_accumulatedElapsedTime = TimeSpan.Zero;
_gameTime.ElapsedGameTime = TimeSpan.Zero;
_previousTicks = 0L;
}

至此, ScreenManager的Update方法会调用GamePlayScreen的Update方法, ScreenManager的Draw方法会调用GamePlayScreen的Draw方法.

GamePlayScreen的HandleInput方法中有对Escape键的轮询:

1
2
3
4
if (input.IsPauseGame(ControllingPlayer) || gamePadDisconnected)
{
ScreenManager.AddScreen(new PauseMenuScreen(), ControllingPlayer);
}

PauseMenuScreen中选择退出游戏, 弹出一个MessageBoxScreen, 点击确定, 会调用ConfirmQuitMessageBoxAccepted方法, 确认退出游戏, 然后退回到主菜单界面.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// Event handler for when the Quit Game menu entry is selected.
/// </summary>
void QuitGameMenuEntrySelected(object sender, PlayerIndexEventArgs e)
{
const string message = "Are you sure you want to quit this game?";

MessageBoxScreen confirmQuitMessageBox = new MessageBoxScreen(message);

confirmQuitMessageBox.Accepted += ConfirmQuitMessageBoxAccepted;

ScreenManager.AddScreen(confirmQuitMessageBox, ControllingPlayer);
}

/// <summary>
/// Event handler for when the user selects ok on the "are you sure
/// you want to quit" message box. This uses the loading screen to
/// transition from the game back to the main menu screen.
/// </summary>
void ConfirmQuitMessageBoxAccepted(object sender, PlayerIndexEventArgs e)
{
LoadingScreen.Load(ScreenManager, false, null, new BackgroundScreen(),
new MainMenuScreen());
}

最后点击MainMenuScreen的Exit菜单项, 关闭游戏

1
2
3
4
5
6
7
8
/// <summary>
/// Event handler for when the user selects ok on the "are you sure
/// you want to exit" message box.
/// </summary>
void ConfirmExitMessageBoxAccepted(object sender, PlayerIndexEventArgs e)
{
ScreenManager.Game.Exit();
}

以上是一个大致的过程, 从主菜单进入游戏, 然后从游戏中退出. 还有不少细节值得推敲, 见下一篇笔记.