Создаем игровое меню Все хорошо сделанные игры не обходятся без меню, и это не просто стартовая страница всего приложения, это хорошо отлаженный механизм, позволяющий пользователю управлять работой программы. В связи с этим необходимо очень тщательно продумывать и планировать работу меню. Старайтесь избегать множественных вложений, непонятных команд и лишних неоправданных диалоговых окон плана: «А вы действительно хотите выйти?», «А вы точно не передумали?» и т. д. Все должно быть очень просто и в то же время красиво оформлено, и не забывайте о том, что страница меню – это первая страница, на которую попадет игрок после игрового вступления. Если ваше меню будет вызывать у человека полное уныние или он будет «блукать» по нему в поисках кнопки для старта игры, то ваша игра точно надолго не задержится на приставке пользователя. Что касается способов создания и реализации меню, то тут все зависит только от вашей фантазии. Можно придумать что угодно и сколько угодно, но главное – знать и понимать, как работает в целом вся система меню. В этой главе мы займемся изучением этой темы и начнем с того, что разберемся с тем, где именно необходимо вызывать игровое меню и как правильно спланировать его вывод на экран телевизора.
12.1. Планируем запуск меню Сейчас при запуске игры мы сразу попадаем в игровой процесс, минуя какие-либо заставки. Ход игрового процесса начинается с вызова метода Update() класса Game1, и на основании полученных данных метод Draw() рисует графику на экране телевизора или монитора. Посему именно в методе Update(), где происходит обновление состояния игры, нам необходимо изначально запускать показ игрового меню, через которое проводить старт или выход из игры. То есть сначала необходимо вывести на экран меню и только потом запускать течение игрового процесса, а меню должно стать механизмом управления всей игры. Методов реализации смены игровых состояний очень много. В этой главе мы воспользуемся одним из простейших алгоритмов, основанных на проверке состояния булевой переменной menuState, которую объявим и инициализируем в исходном коде класса Game1. Эта переменная при запуске программы будет иметь значение true. В этом случае при старте игры в методе Update() достаточно создать проверку следующего условия. Если menuState равно true, то показываем меню. Если нет, то начинаем игровой процесс, или на языке программирования С#:
if(menuState == true) { // попадаем в меню игры } else{ // запускаем игру }
Тогда при старте игры пользователь всегда будет попадать в меню, а уже с меню после выполнения заданной команды запускать непосредственно сам игровой процесс. Чтобы реализовать из меню запуск игры, достаточно по определенной команде просто изменить состояние переменной menuState с true на false, и тогда блок кода с вызовом меню будет пропускаться, а в работу включится другой блок кода, следующий за ключевым словом else. Как видите, в технике запуска меню особых сложностей нет. Теперь давайте перейдем к реализации нового проекта с названием Menu и поговорим о том, каким способом, или, точнее, какими кнопками и рычажками джойстика, мы будем управлять командами меню.
12.2. Проект Menu Какие команды и кнопки у нас были отведены для управления игрой? Кнопка Back закрывает игру, а кнопка Start включает режим паузы, и ее повторное нажатие отменяет паузу в игре. Этого нам будет мало и придется добавить в игру еще несколько команд, при этом необходимо строго разграничить их работу. Посмотрите на рис. 12.1, где схематично показан предлагаемый механизм функционирования игры. Идея следующая. Происходит запуск игры, и пользователь попадает в меню. В меню мы нарисуем на экране две таблички с надписями Игра и Выход (рис. 12.2). Фактически этим мы создадим две команды для запуска игры и выхода из игры. Переход по обеим командам будет осуществляться командами Вверх и Вниз на GamePadDPad. Выполняя команду Вверх (Игра), мы будем активировать данное состояние и дезактивировать команду Выход. Для переключения этого состояния можно использовать дополнительную переменную – как булеву, так и целочисленную, например cursorState.
Рис. 12.1. Схема работы игры
Единственное отличие при выборе типа переменной – это то, что в булевой переменной есть только два состояния, тогда как целочисленной переменной можно присваивать сколько угодно состояний. Например, если cursorState равна 1, то активируем игру, если cursorState равна 2, то активируем экран опций, а если cursorState равна 3, активируем заставку «Об авторе» и т. д. В итоге если выполнена команда Вверх, то мы присваиваем переменной cursorState значение, равное 1, а если Вниз, то значение этой переменной из-меняется на 2. Что нам это дает? А дает нам это многое, в частности далее мы будем проверять нажатие пользователем кнопки с буквой А. Если кнопка А нажата и переменная cursorState равна 1, то выполняется запуск игры, а вот если переменная cursorState равна 2, то по нажатии этой кнопки будет осуществлен выход из игры. Таким образом, для одной кнопки А мы создаем два разных условия, а пользователю будет комфортно управлять работой игры. Теперь о том, как после выполнения команд Вверх (Игра) и Вниз (Выход) на GamePadDPad дать понять пользователю, какая из команд сейчас активна. Способов здесь очень много, и все зависит только от вашей фантазии. В одних играх текущая активная команда подсвечивается, в других играх команда анимируется, в третьих существует курсор, указывающий на то, какая из команд меню сейчас активна, и т. д. То есть это дело фантазии и стилистики оформления всей игры в целом. В нашей с вами игре в качестве команд Игра и Выход применяются две таблички, или доски с соответствующими надписями (рис. 12.2). Чтобы показать пользователю, какая из команд активна в данный момент, мы будем сдвигать дощечку с надписью выбранной команды влево на 50 пикселей и подкрашивать табличку желтым цветом. Таким образом, пользователь сразу заметит, какая из команд активна, а выбрав одну из команд, нажмет кнопку А, для которой, как вы помните, мы назначаем различные состояния. Теперь еще пара слов о выходе в меню непосредственно из игрового процесса. Итак, игрок зашел в меню, выбрал команду Игра и нажал кнопку А. Произошел запуск игрового процесса.
Рис. 12.2. Графическая заставка меню игры
запуск игрового процесса. В режиме паузы мы ничего не меняем, здесь и так все нормально, а вот для выхода из игры в меню нужна дополнительная команда. Как правило, для выхода в меню используется кнопка Back или Start. Это весьма распространенная и стандартная практика. Мы будем использовать кнопку Back. Ранее эта кнопка у нас служила для закрытия игры, но теперь мы эту кнопку переопределим. Теперь по нажатии Back будет производиться выход в меню игры, а уже из самого меню пользователь может как выйти из игры, так и запустить игру сначала. Переходим к работе над проектом Menu, исходный код которого вы можете найти на компакт-диске в папке Code\Chapter12\Menu.
12.2.1. Класс Menu Начнем с того, что сформируем новый проект с названием Menu, в который скопируем исходный код из предыдущей главы. Вы также можете просто модифицировать наш последний проект Font, где мы рассматривали работу со шрифтом и вывод текста на экран телевизора. Очевидно, что для описания игрового меню лучше всего создать отдельный класс. Мы так и поступим, создав специальный класс Menu, которому отведем роль представления меню игры. В листинге 12.1 показан полный исходный код класса Menu.
//========================================================================= /// <summary> /// Листинг 12.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Глава 12 /// Проект: Menu /// Класс: Menu /// Добавляем в игру меню /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; #endregion namespace Menu { class Menu { public Texture2D menuTexture; public Vector2 menuPosition; public Texture2D cursorGame; public Vector2 cursorPositionGame; public Texture2D cursorExit; public Vector2 cursorPositionExit; /// <summary> /// Конструктор /// <summary> public Menu() { menuPosition = new Vector2(0, 0); cursorPositionGame = new Vector2(850, 380); cursorPositionExit = new Vector2(900, 500); } /// <summary> /// Загрузка спрайтов /// public void Load(ContentManager content) { menuTexture = content.Load(«Content\\Textures\\menu»); cursorGame = content.Load(«Content\\Textures\\cursorGame»); cursorExit = content.Load(«Content\\Textures\\cursorExit»); } /// <summary> /// Рисуем меню /// <summary> public void DrawMenu(SpriteBatch spriteBatch, int state) { spriteBatch.Draw(menuTexture, menuPosition, Color.White); switch(state) { case 1: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.Yellow); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.White); break; case 2: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.White); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.Yellow); break; } } } }
С помощью класса Menu нам необходимо загрузить в игру общий фон меню, а также две дополнительные таблички, или доски, с командами Игра и Выход. Для этих целей нужно определить ряд объектов и переменных. Поэтому в начале исходного файла Menu.cs в области глобальных переменных следует шесть строк кода.
// Фон меню и его позиция на экране public Texture2D menuTexture; public Vector2 menuPosition; // Доска с командой Игра и ее позиция на экране public Texture2D cursorGame; public Vector2 cursorPositionGame; // Доска с командой Выход и ее позиция на экране public Texture2D cursorExit; public Vector2 cursorPositionExit;
Затем в конструкторе класса Menu мы задаем позиции на экране как для фона, так и для табличек. Поскольку фон меню идет с размером в 1280 x720 пикселей (HD 720p), то в качестве точки отсчета для фона назначаются нулевые координаты по обеим осям системы.
menuPosition = new Vector2(0, 0);
Для дощечек с командами Игра и Выход позиции задаются в правой части экрана и чуть ближе к его нижней кромке. Эти значения были вычислены еще на этапе проектирования меню, но их всегда можно подкорректировать в исходном коде, если вам вдруг не понравилось их расположение.
cursorPositionGame = new Vector2(850, 380); cursorPositionExit = new Vector2(900, 500);
Заметьте, что верхняя табличка с командой Игра изначально сдвинута влево по отношению к нижней табличке. Это говорит о том, что при входе в меню команда Игра будет активироваться первой. Впоследствии, естественно, мы добавим код для этой команды меню в классе Game1, а также закрасим активную команду желтым цветом. Затем в методе Load() класса Menu происходит загрузка всех трех графических изображений в игру, которые предварительно явно добавляются в каталог проекта в папку Content\Textures. И в самом конце исходного кода класса Menu создается метод DrawMenu() для вывода графики на экран телевизора.
public void DrawMenu(SpriteBatch spriteBatch, int state) { spriteBatch.Draw(menuTexture, menuPosition, Color.White); switch(state) { case 1: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.Yellow); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.White); break; case 2: spriteBatch.Draw(cursorGame, cursorPositionGame, Color.White); spriteBatch.Draw(cursorExit, cursorPositionExit, Color.Yellow); break; } }
Принцип работы метода DrawMenu() следующий. Вызывая этот метод в классе Game1, в качестве второго параметра мы будем передавать в метод переменную cursorState, которая, как вы помните, показывает, какая из команд в текущий момент выбрана. Затем на базе оператора switch происходит отбор нужного ветвления для выполнения исходного кода метода DrawMenu(). Если переменная cursorState равна 1, то это значит, что в данный момент активна команда Игра и выбрана верхняя табличка. Поэтому эту табличку необходимо подкрасить желтым цветом. Как только состояние переменной cursorState изменяется на двойку, то работает блок кода case 2 – и так до бесконечности, пока одна из команд не будет выбрана (кнопка А). В качестве цвета, закрашивающего табличку, был выбран желтый, и мы закрашиваем именно активную в данный момент команду, но можно делать и наоборот, выделять неактивную команду цветом. Дополнительно рассмотренный механизм закрашивания спрайтов цветом можно использовать прямо в игровом процессе, накладывая на текстуры различные оттенки, гаммы цветов и даже новые текстуры.
12.2.2. Загружаем в игру меню С исходным классом Menu мы разобрались, переходим к основному коду игры и классу Game1. Начнем с того, что объявим в области глобальных переменных класса Game1 объект класса Menu и создадим две переменные menuState и cursorState для отслеживания состояния меню и выбранной пользователем команды.
Menu menu; private bool menuState; private int cursorState;
После этих объявлений переходим в конструктор класса Game1, где создаем полноценный объект menu класса Menu и инициализируем обе переменные.
menu = new Menu(); menuState = true; cursorState = 1;
Переменная menuState при запуске игры получает значение true, поэтому первым делом на экран будет выводиться меню игры. В свою очередь, переменная cursorState равна 1, а значит, в игре изначально активируется табличка с надписью Игра. Далее в методе LoadGraphicsContent() мы загружаем всю игровую графику, включая меню. В предыдущих версиях игры в методе Initialize() класса Game1 мы определяли все игровые позиции объектов. Сейчас также можно использовать этот метод, но был реализован улучшенный механизм, в частности создан отдельный метод NewGame(), исходный код которого выглядит следующим образом:
/// <summary> /// Новая игра /// <summary> public void NewGame() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite.spritePosition = new Vector2(rand.Next(10, Window.ClientBounds.Width - 150), j = j - 300); } platform.spritePosition = new Vector2(Window.ClientBounds.Width/ 2, Window.ClientBounds.Height - 90); score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0; base.Initialize(); }
Зачем это нужно? Дело в том, что нам необходимо реализовать обратный старт игры в момент, когда пользователь выйдет из игры в меню и потом захочет опять начать игру сначала. Для этих целей и создается метод NewGame(). Вызов этого метода будет происходить непосредственно в меню игры, и каждый раз, когда пользователь выходит в меню, а потом вновь запускает игру, происходит установка объектов на новые игровые позиции. Исходный код метода NewGame() почти не претерпел изменений, за исключением того, что переменная j, увеличивающая координату по отрицательной оси Y, инициализируется именно в этом методе нулевым значением. Сделано это по одной причине. Как вы знаете, переменная j помогает нам удалить друг от друга спрайты по вертикали на 300 пикселей. Каждое ее увеличение будет содержать большое отрицательное значение, например для пяти спрайтов это значение будет равно –1500 пикселей. Если пользователь зайдет в игру, потом выйдет в меню, а затем опять вернется в игру, то переменная j по-прежнему будет содержать значение в –1500 пикселей и установка игровых объектов будет происходить от этого значения. В результате при повторном старте игры пользователю придется подождать, пока на экране появится первый спрайт, а при третьем или даже десятом старте игры на это уйдет еще больше времени. Поэтому при каждом новом вызове метода NewGame() необходимо обнулять все игровые переменные. Учтите этот нюанс во всех ваших последующих играх. Каждый новый старт игры должен обязательно обнулять все предыдущие игровые состояния, если, конечно, не предусмотрена система сохранения игры или перехода на новый уровень. Теперь переходим к методу Update() класса Game1. В этом методе мы поменяем конструкцию обновления состояния игры с учетом появления в игре меню и сделаем это следующим образом.
protected override void Update(GameTime gameTime) { GamePadState currentState = GamePad.GetState(PlayerIndex.One); // Показываем меню if (menuState == true) { // Переход курсора по меню if (currentState.DPad.Up == ButtonState.Pressed) { menu.cursorPositionGame = new Vector2(850, 380); menu.cursorPositionExit = new Vector2(900, 500); cursorState = 1; } else if (currentState.DPad.Down == ButtonState.Pressed) { menu.cursorPositionGame = new Vector2(900, 380); menu.cursorPositionExit = new Vector2(850, 500); cursorState = 2; } // Обрабатываем нажатие кнопки А if (currentState.Buttons.A ==ButtonState.Pressed&&cursorState == 1) { this.NewGame(); menuState = false; } elseif(currentState.Buttons.A==ButtonState.Pressed&&cursorState==2) { this.Exit(); } } else // Запускаем игру { // Выход в меню if (currentState.Buttons.Back == ButtonState.Pressed) { menuState = true; } … } } base.Update(gameTime); }
Когда вы только запускаете игру, то попадаете в меню игры, потому что переменная menuState равна значению true. Следовательно, вы попадаете в первый блок кода после ключевого слова if, а все, что следует после ключевого слова else, пропускается и не выполняется. В первом блоке кода, следующем после ключевого слова if, все связано только с работой меню. Здесь мы организуем механизм перехода курсора по табличкам, а также обрабатываем нажатие кнопки А. Если на джойстике выполнена команда Вверх, то мы сдвигаем позицию таблички с надписью Игра влево и присваиваем переменной cursorState значение, равное единице. Когда пользователь в этом игровом состоянии нажмет кнопку с буквой А, то сработает блок кода, запускающий игру.
// Обрабатываем нажатие кнопки А if (currentState.Buttons.A == ButtonState.Pressed&&cursorState == 1) { this.NewGame(); menuState = false; }
В этом коде мы вызываем метод NewGame() для определения новых игровых позиций и меняем значение булевой переменной menuState на false, что автоматически выводит работу программы из блока кода, следующего за ключевым словом if. В данной ситуации уже будет выполняться исходный код, следующий за ключевым словом else. Таким образом, из меню игры мы переходим непосредственно в игровой процесс, где будем постоянно отслеживать состояние переменной menuState. Как только пользователь нажмет кнопку Back, то переменная menuState меняет свое состояние на true, а значит, игра останавливается, и мы выходим из игры в меню. Последние дополнения в исходном коде текущего проекта происходят в методе Draw(), который рисует всю игровую графику.
if (menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, cursorState); spriteBatch.End(); }
Здесь мы также добавляем ветвление с конструкцией if/else, и если переменная menuState равна true, то отображаем меню, а если нет, то рисуем на экране игровую графику. Дополнительно в метод DrawMenu() передается значение переменной cursorState, на основе которой, как вы помните, происходит определение, какую из табличек в текущий момент необходимо затенять цветом. Исходный код класса Game1 содержится в листинге 12.2, а полный код проекта располагается на компакт-диске в папке Code\Chapter12\Menu.
//========================================================================= /// <summary> /// Листинг 12.2 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Глава 12 /// Проект: Menu /// Класс: Game1 /// Добавляем в игру меню /// <summary> //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace Menu { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; SpriteBatch spriteBatch; Sprite[] sprite = new Sprite[5]; private Texture2D background; Random rand = new Random(); int screenWidth, screenHeight; int j = 0; Sprite platform; private bool paused = false; private bool pauseButtonsStart = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; int score0, score1, score2, score3, score4; Menu menu; private bool menuState; private int cursorState; /// <summary> /// Конструктор /// <summary> public Game1() { … menu = new Menu(); menuState = true; cursorState = 1; } /// <summary> /// Инициализация /// i; i++) { sprite.spritePosition = new Vector2(rand.Next(10, screenWidth - 150), j = j - 300); } platform.spritePosition = new Vector2(screenWidth / 2, screenHeight - 90); score0 = 0; score1 = 0; score2 = 0; score3 = 0; score4 = 0; } /// <summary> /// Рисуем на экране /// <summary> protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); if (menuState == true) { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); menu.DrawMenu(spriteBatch, cursorState); spriteBatch.End(); } else { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite.DrawAnimationSprite(spriteBatch); } platform.DrawSprite(spriteBatch); … spriteBatch.End(); } base.Draw(gameTime); } } }