К основному контенту

Game menu structure (Game State Management)

Понеслась! Пора описывать алгоритмы в играх. Сам я не профессиональный программист, просто в последнее время мне стала интересна тема игр, всегда хотелось написать что то, как и каждому «компьютерщику». Но, когда садишься что-то сделать, оказывается, что это ой как не просто.
Сделав несколько рабочих набросков игр, код становится плохо читаемым и много чего хочется переделать. Вот и настает тот момент, когда нужно читать книги по алгоритмам и разбираться в чужом коде.
Начнем разбираться со структурой игровых экранов и меню, походу поймете, что это некий костяк игры. Пример, расскажет, как организовано меню и окно игрового процесса. Я взял и разобрал пример http://creators.xna.com/en-US/samples/gamestatemanagement, выбросил из него эффекты экранов, эффекты меню, другие сложности, - осталась та основа, которую легко читать для понимания. Комментариями я не частил, буду некоторые моменты описывать после кода, подробно объяснять не буду – в основах вы должны разбираться. Использую я C# 2008 Express Edition, XNA 3.1.

Структура файлов в проекте:
Теперь краткая иерархия классов с основными параметрами:
namespace MenuGame.GameScreenManager
class GameScreen
      public bool IsActive;
      public CGameScreenManager gameScreenManager;
class GameScreenInput
class GameScreenMenuElement
      public delegate void dHandler(object sender);
      public event dHandler Selected;
class GameScreenLoading: GameScreen
GameScreen[] screensToLoad;
public static void Load(CGameScreenManager screenManager, params GameScreen[] screensToLoad)
class GameScreenMenu: GameScreen
public List<GameScreenMenuElement>Elements = new List<GameScreenMenuElement>();
class CGameScreenManager : DrawableGameComponent
public List<GameScreen> Screens = new List<GameScreen>();
      public SpriteBatch spriteBatch;
      public bool IsInitialized;
      public SpriteFont Font;
      public GameScreenInput input = new GameScreenInput();
      public GraphicsDeviceManager graphicsDeviceManager;

namespace MenuGame.Screen
class Background: GameScreen
class MainGame:GameScreen
class Credits : GameScreenMenu
class HowToPlay : GameScreenMenu
class MainMenu:GameScreenMenu
class Options: GameScreenMenu
class PauseMenu : GameScreenMenu
Теперь перейдем к более подробному описанию. Основные директивы using я не буду давать для экономии глаз, учитывайте, что к каждому файлу прилагаются:
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
GameScreen.cs
namespace MenuGame.GameScreenManager
{
    class GameScreen
    {
        public bool IsActive;
        public CGameScreenManager gameScreenManager;

        public GameScreen()
        {
            IsActive = true;
        }

        public virtual void LoadContent()
        { }

        public virtual void UnLoadContent()
        { }

        public virtual void Update(GameTime gameTime)
        { }

        public virtual void Draw(GameTime gameTime)
        { }

        public virtual void HandleInput(GameScreenInput input)
        { }

        public void ExitGameScreen()
        {
            UnLoadContent();
            IsActive = false;
            gameScreenManager.RemoveScreen(this);
        }
    }
}
Базовый класс, самый основной представляет единицу экрана. Параметр IsActive говорит об активном окне, если активно - то обновляем и рисуем его, при создании окна оно активно (см. Конструктор). Все методы кроме конструктора и ExitGameScreen - выход виртуальны и в наследуемом классе будут переопределяться по желанию. Каждый экран будет иметь ссылку на менеджера экранов gameScreenManager для указания явной и неявной связи, чтобы была видна иерархия, и не собирался мусор в программе, по ходу поймете…
GameScreenInput.cs
namespace MenuGame.GameScreenManager
{
    class GameScreenInput
    {
        public bool Up;
        public bool Down;
        public bool Left;
        public bool Right;
        public bool Fire;
        public bool Menu;
        private Keys keyUp;
        private Keys keyDown;
        private Keys keyLeft;
        private Keys keyRight;
        private Keys keyFire;
        private Keys keyMenu;
        private KeyboardState keyboardState;
        private KeyboardState oldKeyboardState;

        public GameScreenInput()
        {
            keyUp = Keys.Up;
            keyDown = Keys.Down;
            keyLeft = Keys.Left;
            keyRight = Keys.Right;
            keyFire = Keys.Enter;
            keyMenu = Keys.Escape;
        }

        public void Update()
        {
            keyboardState = Keyboard.GetState();
            if (keyboardState.IsKeyDown(keyUp) && !oldKeyboardState.IsKeyDown(keyUp)) Up = true; else Up = false;
            if (keyboardState.IsKeyDown(keyDown) && !oldKeyboardState.IsKeyDown(keyDown)) Down = true; else Down = false;
            if (keyboardState.IsKeyDown(keyLeft) && !oldKeyboardState.IsKeyDown(keyLeft)) Left = true; else Left = false;
            if (keyboardState.IsKeyDown(keyRight) && !oldKeyboardState.IsKeyDown(keyRight)) Right = true; else Right = false;
            if (keyboardState.IsKeyDown(keyFire) && !oldKeyboardState.IsKeyDown(keyFire)) Fire = true; else Fire = false;
            if (keyboardState.IsKeyDown(keyMenu) && !oldKeyboardState.IsKeyDown(keyMenu)) Menu = true; else Menu = false;

            oldKeyboardState = keyboardState;
        }
    }
}
Независимый класс, здесь я постарался представить методику использования разных контроллеров. Имеются булевые переменные (Up, Down, Left, …) основных действий, устанавливает их в соответствии с нажатыми клавишами метод контроллера (клавиатура, мышка, гейпад). Тут только клавиатура, додумать (если вам нужно) другие контроллеры не сложно.
GameScreenLoading.cs
namespace MenuGame.GameScreenManager
{
    class GameScreenLoading: GameScreen
    {
        GameScreen[] screensToLoad;
        bool IsExit=false;
        int ExTime = 500;

        public static void Load(CGameScreenManager screenManager, params GameScreen[] screensToLoad)
        {
            foreach (GameScreen screen in screenManager.GetScreens())
                screen.ExitGameScreen();
            GameScreenLoading loading = new GameScreenLoading();
            screenManager.AddScreen(loading);
            loading.screensToLoad = screensToLoad;
        }
        public override void Update(GameTime gameTime)
        {
            if (screensToLoad != null)
            {
                foreach (GameScreen screen in screensToLoad)
                {
                    screen.IsActive = false;
                    gameScreenManager.AddScreen(screen);
                }
                screensToLoad = null;
            }
            ExTime -= gameTime.ElapsedGameTime.Milliseconds;
            if (ExTime < 0)
            {
                IsExit = true;
                gameScreenManager.RemoveScreen(this);
                foreach (GameScreen screen in gameScreenManager.Screens)
                    screen.IsActive = true;
                gameScreenManager.Game.ResetElapsedTime();
            }
        }
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
            if (!IsExit)
            {
                gameScreenManager.Game.GraphicsDevice.Clear(Color.Black);
                gameScreenManager.spriteBatch.Begin();
                gameScreenManager.spriteBatch.DrawString(gameScreenManager.Font, "Loading...", new Vector2(100, 100), Color.White);
                gameScreenManager.spriteBatch.End();
            }
        }
    }
}
Окно загрузки. Если вам нужно загрузить один или несколько экранов, которые содержат много контента - используйте этот класс. В основном используется для перехода от меню к игре. Обязательно статический метод Load, сюда передается родитель CGameScreenManager screenManager и массив экранов которые нужно загрузить. Перед тем как загрузить переданные экраны выгружаем все экраны что есть, и ставим на показ собственный экран (Loading…). Чтобы все не происходило мгновенно, и пользователь мог полюбоваться «шикарной» надписью, я сделал минимальную задержку в миллисекундах ExTime. После загрузки всех экранов нужно обязательно выгрузить себя из списка экранов и сделать gameScreenManager.Game.ResetElapsedTime() – обнулить счетчик прошедшего времени, чтобы загруженные экраны не «ринулись» если они зависят от времени.
GameScreenManager.cs
namespace MenuGame.GameScreenManager
{
    class CGameScreenManager : DrawableGameComponent
    {
        public List<GameScreen> Screens = new List<GameScreen>();
        public SpriteBatch spriteBatch;
        public bool IsInitialized;
        public SpriteFont Font;
        public GameScreenInput input = new GameScreenInput();
        public GraphicsDeviceManager graphicsDeviceManager;

        public CGameScreenManager(Game game, GraphicsDeviceManager graphicDeviceManager)
            : base(game)
        {
            graphicsDeviceManager = graphicDeviceManager;
        }
        
        public override void Initialize()
        {
            base.Initialize();
            IsInitialized = true;
        }

        protected override void LoadContent()
        {
            ContentManager content = Game.Content;
            spriteBatch = new SpriteBatch(GraphicsDevice);
            Font = content.Load<SpriteFont>("Font\\FontMenu");
            foreach (GameScreen screen in Screens)
            {
                screen.LoadContent();
            }
        }

        protected override void UnloadContent()
        {
            foreach (GameScreen screen in Screens)
            {
                screen.UnLoadContent();
            }
        }
        
        public override void Update(GameTime gameTime)
        {
            for (int i = 0; i < Screens.Count; i++) if (Screens[i].IsActive) Screens[i].Update(gameTime);
        }
        
        public override void Draw(GameTime gameTime)
        {
            foreach (GameScreen screen in Screens)
            {
                if (screen.IsActive)
                    screen.Draw(gameTime);
            }    
        }

        public void AddScreen(GameScreen gameScreen)
        {
            gameScreen.gameScreenManager = this;
            if (IsInitialized)
                gameScreen.LoadContent();
            Screens.Add(gameScreen);
        }

        public void RemoveScreen(GameScreen gameScreen)
        {
            gameScreen.UnLoadContent();
            Screens.Remove(gameScreen);
        }
        
        public GameScreen[] GetScreens()
        {
            return Screens.ToArray();
        }
    }
}
Главный класс – менеджер экранов. Содержит список экранов Screens. Некоторые данные: графическое устройство, шрифт, SpriteBatch. Принцип любого метода не сложный – перебрать все экраны, которые есть в коллекции, и сделать с ними действие, соответствующее методу. Есть нюанс в методе AddScreen нужно проверить, был ли сам менеджер инициализирован перед загрузкой контента экранов, нужен для этих строк
gameScreenManager = new CGameScreenManager(this, graphics);
Components.Add(gameScreenManager);
gameScreenManager.AddScreen(new Background());
gameScreenManager.AddScreen(new MainMenu());
GameScreenMenuElement.cs
namespace MenuGame.GameScreenManager
{
    class GameScreenMenuElement
    {
        public string Text;

        public delegate void dHandler(object sender);
        public event dHandler Selected;
        
        public GameScreenMenuElement(string text)
        {
            Text = text;
        }

        public virtual void Update()
        { }

        public virtual void Draw(GameScreen screen, Vector2 Position, GameTime gameTime, bool IsSelected)
        {
            Color color;
            if (IsSelected)
                color = Color.Red;
            else
                color = Color.White;
            screen.gameScreenManager.spriteBatch.DrawString(screen.gameScreenManager.Font, Text, Position, color);
        }
        public void OnSelect()
        {
            if (Selected != null) Selected(this);
        }
    }
}
Независимый класс элемента меню. Элемент основан на тексте – то есть меню будет из текстовых строчек. Модифицировать под спрайты не сложно. Принцип такой – если курсор на позиции меню то оно отображается красным цветов, если нет – белым. Если вы нажали на меню - то будет вызываться метод OnSelect, который в свою очередь будет вызывать event по описанию делегата dHandler. Методы обработки элемента здесь нет – в этом то и смысл event что вы добавите его, например:
GameScreenMenuElement BackGameMenu = new GameScreenMenuElement("Back");
            BackGameMenu.Selected += Back;
            Elements.Add(BackGameMenu);
        }

        public void Back(object sender)
        {
            gameScreenManager.AddScreen(new MainMenu());
            this.ExitGameScreen();
        }
Нужно проверять Selected != null если элемент меню создан, но в event ничего не добавлено.
GameScreenMenu.cs
namespace MenuGame.GameScreenManager
{
    class GameScreenMenu:GameScreen
    {
        public List<GameScreenMenuElement>Elements = new List<GameScreenMenuElement>();
        public string ScreenTitle;
        public int SelectedIndex=0;

        public GameScreenMenu(string screenTitle)
        {
            ScreenTitle = screenTitle;
        }

        public override void Update(GameTime gameTime)
        {
            gameScreenManager.input.Update();
            if (gameScreenManager.input.Up) SelectedIndex--;
            if (gameScreenManager.input.Down) SelectedIndex++;
            if (SelectedIndex < 0) SelectedIndex = 0;
            if (SelectedIndex > Elements.Count-1) SelectedIndex = Elements.Count-1;
            if (gameScreenManager.input.Fire) Elements[SelectedIndex].OnSelect();
        }

        public override void Draw(GameTime gameTime)
        {
            gameScreenManager.spriteBatch.Begin();
            gameScreenManager.spriteBatch.DrawString(gameScreenManager.Font, ScreenTitle, new Vector2(100, 10), Color.DarkRed);
            for (int i=0;i<Elements.Count;i++)
            {
                Elements[i].Draw(this, new Vector2(100, 100)+ new Vector2(0,i*gameScreenManager.Font.LineSpacing), gameTime, i==SelectedIndex);
            }
            gameScreenManager.spriteBatch.End();
        }

    }
}
Класс меню содержит список элементов меню ListElements. В переопределяемом методе обновления Update – описывается, что нужно делать при нажатии клавиш – пускать курсор вверх, вниз, не допускать вывод индекса SelectedIndex за пределы списка элементов, а по нажатию на клавишу Fire выполнять event через метод OnSelect.

Game1.cs
using MenuGame.GameScreenManager;
using MenuGame.Screen;

namespace MenuGame
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        CGameScreenManager gameScreenManager;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 720;
            gameScreenManager = new CGameScreenManager(this, graphics);
            Components.Add(gameScreenManager);
            gameScreenManager.AddScreen(new Background());
            gameScreenManager.AddScreen(new MainMenu());
        }

        protected override void Initialize()
        {
            base.Initialize();
            gameScreenManager.Initialize();
        }

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
        }
    }
}
Файлы основной программы, генериться самой XNA, изменения сделаны в конструкторе и списке свойств класса. Теперь базовые классы созданы, нужно показать, как их использовать.

Background.cs
using MenuGame.GameScreenManager;

namespace MenuGame.Screen
{
    class Background: GameScreen
    {
        public Texture2D backgroundTexture;
        public byte Alpha=160;
        private ContentManager Content;

        public Background()
            : base()
        { IsActive = true; }
        public override void LoadContent()
        {
            base.LoadContent();
            if (Content==null)
                Content = new ContentManager(gameScreenManager.Game.Services, "Content");
            backgroundTexture = Content.Load<Texture2D>("Sprite\\Background\\bg");
        }
        public override void UnLoadContent()
        {
            base.UnLoadContent();
            if (Content!=null)
            Content.Unload();
        }
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
            SpriteBatch spriteBatch = gameScreenManager.spriteBatch;
            Viewport viewport = gameScreenManager.GraphicsDevice.Viewport;
            Rectangle fullscreen = new Rectangle(0, 0, viewport.Width,viewport.Height);
            spriteBatch.Begin(SpriteBlendMode.None);
            spriteBatch.Draw(backgroundTexture, fullscreen, new Color(255, 255, 255, Alpha));
            spriteBatch.End();
        }

    }
}
Экран, который ничего не делает кроме как отображает фоновый рисунок 1280х720, является наследником базового GameScreen.
Credits.cs
using MenuGame.GameScreenManager;

namespace MenuGame.Screen
{
    class Credits : GameScreenMenu
    {
        string creditsText;

        public Credits()
            : base("Credits")
        {
            creditsText = "maded by Vitukhin Sergey\nbla bla bla\nbla";

            GameScreenMenuElement BackGameMenu = new GameScreenMenuElement("Back");
            
            BackGameMenu.Selected += Back;
            
            Elements.Add(BackGameMenu);
        }

        public void Back(object sender)
        {
            gameScreenManager.AddScreen(new MainMenu());
            this.ExitGameScreen();
        }
        
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
            gameScreenManager.spriteBatch.Begin();
            gameScreenManager.spriteBatch.DrawString(gameScreenManager.Font,creditsText,new Vector2(200,150),Color.White);
            gameScreenManager.spriteBatch.End();
        }
    }
}
Класс «О себе любимом» Заголовок Credits, который передаем в базовый класс GameScreenMenu. Содержит текст creditsText, который и должен отображать, кроме меню. По элементу BackGameMenu будет выполняться выход в главное меню (удаление самого себя и добавление в менеджер MainMenu()).

HowToPlay.cs
namespace MenuGame.Screen
{
    class HowToPlay : GameScreenMenu
    {
        string creditsText;

        public HowToPlay()
            : base("How To Play")
        {
            creditsText = "To play to win ... \nYou can do it!";
            GameScreenMenuElement BackGameMenu = new GameScreenMenuElement("Back");
            BackGameMenu.Selected += Back;
            Elements.Add(BackGameMenu);
        }

        public void Back(object sender)
        {
            gameScreenManager.AddScreen(new MainMenu());
            this.ExitGameScreen();
        }
        
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
            gameScreenManager.spriteBatch.Begin();
            gameScreenManager.spriteBatch.DrawString(gameScreenManager.Font,creditsText,new Vector2(200,150),Color.White);
            gameScreenManager.spriteBatch.End();
        }
    }
}
Полная аналогия с предыдущим классом. Можете добавить картинки и т.д.
MainGame.cs
namespace MenuGame.Screen
{
    class MainGame:GameScreen
    {
        Random random;
        GameScreenInput GInput;

        public MainGame()
        {
            random = new Random();
            GInput = new GameScreenInput();
        }

        public override void Update(GameTime gameTime)
        {
            GInput.Update(); //если использовать инпут менеджера то затираються данные в апдейте (вызов со второго скрина)
            if (GInput.Menu)
            {
                gameScreenManager.AddScreen(new PauseMenu());
                IsActive = false;
            }

        }

        public override void Draw(GameTime gameTime)
        {
            gameScreenManager.Game.GraphicsDevice.Clear(Color.Black);
            gameScreenManager.spriteBatch.Begin();
            gameScreenManager.spriteBatch.DrawString(gameScreenManager.Font, "Game", new Vector2(random.Next(100, 200), random.Next(100, 200)), Color.Yellow);
            gameScreenManager.spriteBatch.End();
        }
    }
}
Экран игры. Тут собственно ничего особенного не происходит – просто в случайном порядке выводиться надпись "Game" – это показательная программа, а не полноценная игра. По нажатию на Menu будет происходить вызов меню паузы. Нюанс в том, что нужно использовать свой GameScreenInput GInput иначе после двойного вызова обновления нажатая клавиша пропадет после вызова второго метода из-за строчки:

oldKeyboardState = keyboardState;
MainMenu.cs
using MenuGame.GameScreenManager;

namespace MenuGame.Screen
{
    class MainMenu:GameScreenMenu
    {
        public MainMenu()
            : base("Main menu")
        {
            GameScreenMenuElement PlayGameMenu = new GameScreenMenuElement("Play Game");
            GameScreenMenuElement OptionsGameMenu = new GameScreenMenuElement("Optons");
            GameScreenMenuElement HowToPlayGameMenu = new GameScreenMenuElement("How to play");
            GameScreenMenuElement CreditsGameMenu = new GameScreenMenuElement("Credits");
            GameScreenMenuElement ExitGameMenu = new GameScreenMenuElement("Exit Game");

            ExitGameMenu.Selected += ExitGame;
            CreditsGameMenu.Selected += Credits;
            HowToPlayGameMenu.Selected += HowToPlay;
            OptionsGameMenu.Selected += Options;
            PlayGameMenu.Selected += NewGame;

            Elements.Add(PlayGameMenu);
            Elements.Add(OptionsGameMenu);
            Elements.Add(HowToPlayGameMenu);
            Elements.Add(CreditsGameMenu);
            Elements.Add(ExitGameMenu);
        }

        void Options(object sender)
        {
            this.ExitGameScreen();
            gameScreenManager.AddScreen(new Options());
        }

        void HowToPlay(object sender)
        {
            this.ExitGameScreen();
            gameScreenManager.AddScreen(new HowToPlay());
        }

        void Credits(object sender)
        {
            this.ExitGameScreen();
            gameScreenManager.AddScreen(new Credits());
        }

        void ExitGame(object sender)
        {
            gameScreenManager.Game.Exit();
        }
        void NewGame(object sender)
        {
            GameScreenLoading.Load(gameScreenManager, new MainGame());
        }
    }
}
Главное меню. Хоть оно и главное тут ничего особенного, - создаются элементы меню, добавляются к event описанные методы. Каждый метод описывает удаление этого окна и добавление соответствующего экрана по пункту меню к менеджеру.

PauseMenu.cs
using MenuGame.GameScreenManager;

namespace MenuGame.Screen
{
    class PauseMenu : GameScreenMenu
    {
        public PauseMenu()
            : base("Pause Menu")
        {
            GameScreenMenuElement ResumeGameMenu = new GameScreenMenuElement("Resume game");
            GameScreenMenuElement ToMainMenuMenu = new GameScreenMenuElement("Main menu");
            
            ResumeGameMenu.Selected += ResumeGame;
            ToMainMenuMenu.Selected += ToMainMenu;
            
            Elements.Add(ResumeGameMenu);
            Elements.Add(ToMainMenuMenu);
            IsActive = true;
        }

        void ResumeGame(object sender)
        {
            foreach (GameScreen screen in gameScreenManager.Screens)
                screen.IsActive = true;
            this.ExitGameScreen();
        }
        
        void ToMainMenu(object sender)
        {
             GameScreenLoading.Load(gameScreenManager, new GameScreen[] { new Background(), new MainMenu() });
        }
    }
}
Меню паузы, наследник GameScreenMenu. Два метода выход в главное меню через GameScreenLoading.Load - потому что экрану игры нужно тоже сделать немаленький выгруз контента. Продолжить игру ResumeGame – делает все окна активными (игра на паузе – значит не обновляется и не рисуется), а себя выгружает.

Options.cs
using MenuGame.GameScreenManager;

namespace MenuGame.Screen
{
    class Options: GameScreenMenu
    {
        public bool IsFullScreen=false;
        GameScreenMenuElement FullScreenGameMenu;
        GameScreenMenuElement ScreenResolutionMenu;
        GameScreenMenuElement ApplyMenu;
        string tFullScreenGameMenu="Full Screen: ";

        string tReSolution="Resolution: ";
        string[] Resolutions = { "480p (640x480)", "720p (1280x720)", "1080i/1080p (1920x1080)" };
        int[] ResolutionWidth = { 640, 1280, 1920 };
        int[] ResolutionHeight = {480, 720, 1080};
        int CurrentResolution = 1;

        public Options()
            : base("Options")
        {
            ScreenResolutionMenu = new GameScreenMenuElement(tReSolution + Resolutions[CurrentResolution]);
            FullScreenGameMenu = new GameScreenMenuElement(tFullScreenGameMenu+(IsFullScreen?"yes":"no"));
            ApplyMenu = new GameScreenMenuElement("Apply");

            GameScreenMenuElement BackGameMenu = new GameScreenMenuElement("Back");

            ScreenResolutionMenu.Selected += ChangeResolution;
            FullScreenGameMenu.Selected += FullScreen;
            ApplyMenu.Selected += Apply;
            BackGameMenu.Selected += Back;

            Elements.Add(ScreenResolutionMenu);
            Elements.Add(FullScreenGameMenu);
            Elements.Add(ApplyMenu);
            Elements.Add(BackGameMenu);
        }

        public void Back(object sender)
        {
            gameScreenManager.AddScreen(new MainMenu());
            this.ExitGameScreen();
        }

        public void Apply(object sender)
        {
            gameScreenManager.graphicsDeviceManager.PreferredBackBufferWidth = ResolutionWidth[CurrentResolution];
            gameScreenManager.graphicsDeviceManager.PreferredBackBufferHeight = ResolutionHeight[CurrentResolution];
            gameScreenManager.graphicsDeviceManager.ApplyChanges();
            if (IsFullScreen != gameScreenManager.graphicsDeviceManager.IsFullScreen)
                gameScreenManager.graphicsDeviceManager.ToggleFullScreen();
        }
        public void ChangeResolution(object sender)
        {
            CurrentResolution = (CurrentResolution + 1) % Resolutions.Length;
            ScreenResolutionMenu.Text = tReSolution + Resolutions[CurrentResolution];
        }

        public void FullScreen(object sender)
        {
            if (!IsFullScreen)
            {
                IsFullScreen = true;
                FullScreenGameMenu.Text = tFullScreenGameMenu + "yes";
            }
            else
            {
                IsFullScreen = false;
                FullScreenGameMenu.Text = tFullScreenGameMenu + "no";
            }
        }
    }
}
Экран опции: задаем разрешение экрана, делаем игру полноэкранной, для списка разрешений несколько массивов с данными о разрешениях.
Вот и все в примере на creators.xna.com код серьезней, если вы осмыслили этот текст то можно приступить к модификации: добавить эффекты к тексту, плавная смена экранов, привязка меню к игроку который ее вызвал, добавление других устройств управление и т.д.

Исходный код проекта

Комментарии