В отличие от двухмерной игры, созданной нами в предыдущей части книги, в игре «Футбольный стрелок» все установки объектов ведутся в основном от центра экрана телевизора. Для экрана меню, а также других информационных заставок необходимо предусмотреть универсальный механизм представления графики. Это необходимо для того, чтобы при изменении размера экрана, например с 720p на 1080i, все элементы меню и другие заставки смотрелись одинаково. Способов решения этой задачи немало, и в каждом конкретном решении можно использовать любые интересные идеи и находки. Достаточно просто запустить несколько профессионально сделанных игр и посмотреть, как делают меню и заставки сильные мира сего.
21.1. Схема работы меню и показ заставок
Механизм представления заставок почти во всех играх одинаков и сводится к выводу того или иного изображения в центре экрана, а все свободное пространство дисплея красится одним из цветов. Меню игры также основано на этой технике, но здесь набор доступных команд меню (Новая игра, Помощь, Об игре, Опции…) часто смещается к краю дисплея (влево, вправо) или к верхней и нижней частям кромок экрана либо располагается в его центре. Это не столь важно, к какой из сторон дисплея смещаются команды, – важен сам механизм смещения команд. Экраны телевизоров бывают различных размеров, поэтому очень важно иметь четкую точку отсчета, от которой вы можете отталкиваться в своих расчетах и производить вывод команд меню и другой графики на экран. В игре «Футбольный стрелок» такой точкой отсчета служит центр экрана. Все основные графические элементы заставок и меню устанавливаются в центр экрана. Например, в меню игры от центра экрана происходит расчет вывода курсора, а соответственно и дальнейший переход по командам меню основан на точке в центре телевизора. Подробнее об этом мы поговорим позже в этой главе, а сейчас давайте набросаем на листке бумаги необходимые нам компоненты для создания меню и заставок. Обычно последовательность заставок и механизм работы меню традиционны во всех играх. Посмотрите на рис. 21.1, где вашему вниманию предложена схема работы заставок и меню игры «Футбольный стрелок».
Рис. 21.1. Схема работы меню и заставок игры
Первая игровая заставка, появляющаяся на экране телевизора в игре «Футбольный стрелок», – это так называемая титульная заставка. На этой заставке может присутствовать различная информация, но в основном эта информация связана с разработчиком игры, спонсорами и т. д. У нас в игре титульная заставка появляется первой, после чего пользователю будет предложено нажать кнопку с буквой А для перехода в меню игры. Игровое меню «Футбольного стрелка» содержит пять команд: Игра, Помощь, Об игре, Книги и Выход. * Игра – эта команда запускает игровой процесс. * Помощь – выбор этой команды откроет новый экран, где пользователь может ознакомиться с правилами игры и другой полезной информацией об игровом процессе. Возврат с этого экрана в меню игры возможен только по нажатии кнопки с буквой В. * Об игре – эта команда также открывает свой экран, в котором игрок найдет информацию о людях, создавших эту игру. На этом экране обычно присутствует полный перечень всех тех людей, которые внесли свой вклад в создание игры. Возврат с этого экрана в меню игры осуществляется по нажатии кнопки В. * Книги – эта команда оригинальна только для данной игры, и здесь находится рекламная информация о книгах по программированию игр для Windows и Xbox 360. Возврат с этого экрана в меню игры доступен по нажатии кнопки В. * Выход – это последняя команда, и, как видно из названия, ее назначение заключается в закрытии программы и выходе из игры. Начнем мы с титульной заставки, потом создадим три заставки: Помощь, Об игре и Книги, затем перейдем к созданию меню игры и к механизму смены игровых состояний в классе Game1.
21.2. Титульная заставка
Титульная заставка – это самая первая заставка, которую пользователь видит на своем экране, поэтому важно на этой заставке указать информацию о своей компании или о себе лично. Как правило, все создатели игр на титульной заставке размещают логотип своей компании, а также может быть показана информация о спонсорах или других рекламных партнерах. Если вы делаете игру сами и не имеете своей компании, то разместите на этой заставке информацию, которая впоследствии могла бы идентифицировать ваши игры от других игр. Пользователь должен запомнить эту заставку и в дальнейшем (если, конечно, игра будет иметь хоть какой-то спрос) знать, что эта компания или этот программист делает хорошие игры. В игре «Футбольный стрелок» основная задача титульной заставки заключается в рекламе книг по программированию игр для Windows и Xbox 360 (рис. 21.2). Рис. 21.2. Титульная заставка игры «Футбольный стрелок» В связи с этим на титульную заставку была вынесена информация о книгах, а также даны Интернет-адреса, где пользователь смог бы найти больше информации по интересующей его теме. То есть все сделано для продвижения книг на издательском рынке, которые, в свою очередь, двигают эту игру. Рычаги любой рекламы необходимо использовать на полную катушку, тем более когда эта реклама бесплатна. Оформление титульной заставки игры может выполняться в любом ключе, здесь все зависит от политики компании или самого программиста. Любые украшательства (в меру, конечно) или хитроумные способы представления игры только приветствуются. Рассмотрим компоненты, из которых состоит титульная заставка игры «Футбольный стрелок».
21.2.1. Как сделана титульная заставка
Все пространство экрана титульной заставки закрашивается темно-зеленым цветом. На фоне этого цвета в центре экрана рисуется футбольное поле, на котором располагается некоторая информация о книгах, издательстве и авторе, а также даны обложки самих книг (рис. 21.2). Рисунок футбольного поля – это отдельный графический файл размером 800×500 пикселей. В верхней части экрана рисуется титульная надпись игры с текстом: Футбольный стрелок. Эта надпись сделана отдельным графическим файлом в формате PNG и размером 800×80 пикселей. В нижней части экрана более мелким шрифтом рисуется надпись с текстом: «Нажмите – А», для того чтобы пользователь знал, какую из кнопок на джойстике ему нужно нажать для перехода в меню игры. Еще один элемент заставки – это входные отверстия пуль. Идея такая: рисуется изображение, напоминающее входное отверстие пули (рис. 21.3). Затем в исходном коде программы создается массив данных, содержащий определенное количество таких изображений. В качестве позиций на экране этим изображениям задаются случайные позиции, и в момент показа на экране заставки все отверстия будут раскиданы по экрану случайным образом. В итоге у пользователя создастся впечатление, что экран телевизора изрешечен выстрелами (рис. 21.2). Последний элемент заставки – это мячик из игры, который выводится в центр экрана и вращается по оси Y. То есть мячик стоит на месте в самом центре экрана и крутится вокруг своей оси с заданной скоростью. Мячик в сцене рисуется последним, и все входные отверстия пуль, даже если попадут в мяч, будут нарисованы под ним. Это простое и самое обыкновенное украшательство заставки титульного листа. Таких идей можно придумать много, но главное – не «переборщить» с насыщенностью графических компонентов. Понятно, что вам хочется показать людям, какой вы хороший программист, но много – это совсем не значит хорошо. Например, в этой титульной заставке я уже нахожусь где-то на грани того, чтобы перебрать с насыщенностью игровой сцены для простой титульной заставки…
Рис. 21.3. Изображение входного отверстия пули
21.2.2. Разрабатываем класс SplashScreen
Пришла пора поработать над исходным кодом класса, представляющего титульную заставку игры. Для этих целей создается класс SplashScreen, который впоследствии послужит нам прототипом для других классов заставок и основного меню игры. Создаем последний проект в этой книге под названием Football_arrows. На компакт-диске этот проект находится в папке Code\Chapter21\Football_arrows. Исходный код класса SplashScreen приведен в листинге 21.1. Для простоты рассмотрения исходного кода я перейду прямо в листинг и по ходу изучения прокомментирую составляющую этого класса. Так вам будет намного проще и интереснее.
#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 { class SplashScreen {
В области глобальных переменных объявляем три объекта, которые необходимы для загрузки изображений футбольного поля, названия игры и надписи «Нажмите – А». Все три объекта представлены классом Texture2D.
public Texture2D screen; public Texture2D title; // клавиша с буквой А public Texture2D enter;
Теперь нам необходимо объявить массив объектов для хранения графического изображения входного отверстия пули. Количество элементов массива может быть каким угодно. Сначала я попробовал использовать массив из десяти изображений, но этого оказалось маловато. Двадцать изображений тоже особо не впечатлили, а вот тридцать разбросанных по экрану отверстий выглядели в самый раз, но, конечно, здесь все зависит от разрешения экрана.
public Texture2D[] hole = new Texture2D[30]; public Vector2[] position = new Vector2[30]; Random rand = new Random();
Следующий относительно большой блок кода должен быть вам знаком по работе с трехмерными объектами и матрицами. Ничего нового в этих строках нет, за исключением самой последней строки кода.
В последней строке кода мы создаем еще одну дополнительную матрицу, которую впоследствии будем использовать для вращения мяча по оси Y. Для загрузки графики в будущий объект класса SplashScreen создается метод InitializeSplashScreen() с тремя параметрами. Первый параметр content принимает участие в загрузке графики из рабочего каталога игры, а два других – x и y – будут передавать в этот метод текущие значения ширины и высоты экрана телевизора. Эти значения нам понадобятся для определения позиций вывода отверстий на экран.
/// /// Загрузка компонентов заставки /// public void InitializeSplashScreen(ContentManager content, int x, int y) {
Загружаем в программу все четыре графических изображения.
screen = content.Load(«Content\\Textures\\splash»); title = content.Load(«Content\\Textures\\title»); enter = content.Load(«Content\\Textures\\enter»); for (int i = 0; hole.Length > i; i++) { hole[i] = content.Load(«Content\\Textures\\hole»); }
А вот в следующем цикле мы выбираем случайные позиции на экране для всех тридцати отверстий и таким образом как бы раскидываем по экрану отверстия в случайном порядке. В этом случае при каждом новом запуске игры отверстия будут иметь новые позиции, что само по себе неплохо – разнообразие в играх, пусть даже в представлении титульной заставки, нам не помешает.
for (int i = 0; position.Length > i; i++) {
Область вывода отверстий задается по оси X от 20 пикселей до ширины экрана минус 60 пикселей. Эти самые 60 пикселей примерно равны ширине одного отверстия.
position[i].X = rand.Next(20, x - 60); position[i].Y = rand.Next(100, y - 60); }
Затем происходят загрузка в игру мяча и выбор позиции на экране, которая находится точно в центре экрана.
ball = new ModelClass(); ball.Load(content, «Content\\Models\\SoccerballRed»); ball.position = new Vector3(0, 0, 0);
Переменная aspectRatio получает текущие значения ширины и высоты экрана и в дальнейшем будет участвовать в матричных расчетах.
aspectRatio = (float)x / (float)y; }
Следующий метод DrawScreen() рисует графику на экране и имеет целых пять параметров. Первые два параметра spriteBatch и graphics, как вы помните, нужны нам для работы с графикой. Параметры x и y передадут в тело метода ширину и высоту экрана, а последний параметр gameTime позволит нам получать прошедшее время за один такт игры, которое мы будем использовать для вращения мяча по оси Y.
/// /// Рисуем заставку /// public void DrawScreen(SpriteBatch spriteBatch, GraphicsDeviceManager graphics, int x, int y, GameTime gameTime) {
Позиция глаз для мячика удалена на 230 пикселей. Таким образом, мы удалили мяч от себя и в то же время уменьшили его в размерах. При желании можно сам мяч уменьшить, например, в 3ds Max, и тогда позиция камеры может быть намного меньше, вплоть до единичного значения по оси Z. Тут все зависит от физического размера самой модели.
Переменная angle получает прошедшее время за один такт, которое мы умножаем на взятую с неба цифру 3.0f. Здесь цифра может быть любой, но меньшее значение всегда соответствует меньшей скорости вращения, а большая цифра – большей скорости вращения модели.
Если вы вспомните главу 15, где мы разбирали основы программирования трехмерной графики и, в частности, раздел, связанный с матрицей вращения по оси Y, то вам сразу станет понятно, как работает метод CreateRotationY().
Для того чтобы все установки вращения вступили в силу, нужно умножить матрицу вращения на матрицу трансляции и сохранить итоговый результат в мировой матрице.
При желании можно также вращать мячик сразу по всем трем осям. Для этого достаточно создать еще две матрицы для осей X и Z, умножить их на какое-то число, а затем элементарно перемножить матрицы между собой, сохранив полученное значение в мировой матрице. Например, вот так:
На этом разработка класса SplashScreen окончена, и мы можем переходить к разработке трех других заставок игры.
21.3. Заставки Помощь, Об игре
Для создания трех оставшихся заставок игры мы возьмем исходный код класса SplashScreen и добавим в него ряд изменений. Основным отличием оставшихся заставок от титульной заставки (за исключением смены текста на футбольных полях) является наличие не одного мяча, а трех. Идея следующая. Вместо того чтобы вращать в центре экрана только один мяч, мы добавим в заставку еще два мяча, а для определения позиций мячей на экране зададим любые ненулевые координаты. Вращать мячи будем по двум осям, а поскольку мы задаем позиции для мячей не в центре экрана, мячики будут перемещаться по всему экрану. В этом случае область вращения мячей задается начальными позициями объектов. На рис. 21.4 представлена одна из заставок игры. В листинге 21.2 вашему вниманию представлен класс HelpScreen. Этот класс аналогичен двум другим классам – AboutScreen и BooksScreen. В этих классах изменяется только текст, размещенный на футбольных полях. Полный исходный код проекта находится в папке Code\Chapter21\Football_arrows
//========================================================================= /// /// Листинг 21.2 /// Класс: HelpScreen /// //========================================================================= #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 { class HelpScreen { public Texture2D screen; public Texture2D title;
Название текстуры для клавиши было оставлено прежним – esc. Нет смысла изменять одно название во всех классах игры. Если вы создаете игру одновре менно для ПК и консоли, то можно придумать универсальные названия командам, текстурам, но это особого значения не имеет, поскольку предназначено, так сказать, для внутреннего пользования. Игрок всего этого не видит, и ему абсолютно все равно, как вы в программном коде обозвали ту или иную команду, важно, чтобы вы могли вспомнить через полгода-год, для чего эта команда предназначалась.
public Texture2D esc; public Texture2D[] hole = new Texture2D[30]; public Vector2[] position = new Vector2[30]; Random rand = new Random(); private ModelClass[] ball = new ModelClass[3]; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; float angle; Matrix view; Matrix proj; Matrix world; Matrix rotationMatrixY; Matrix rotationMatrixZ; /// /// Загрузка компонентов заставки /// public void InitializeHelpScreen(ContentManager content, int x, int y) { screen = content.Load(«Content\\Textures\\help»); title = content.Load(«Content\\Textures\\title»); esc = content.Load(«Content\\Textures\\esc»); for (int i = 0; hole.Length > i; i++) { hole[i] = content.Load(«Content\\Textures\\hole»); } for (int i = 0; position.Length > i; i++) { position[i].X = rand.Next(20, x - 60); position[i].Y = rand.Next(100, y - 60); } for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); }
Позиции для мячей заданы, что называется, с потолка, главным было здесь одно условие: не задавать слишком большие значения по двум осям X и Y, дабы мячики при вращении не вышли из области видимости экрана.
ball[0].Load(content, «Content\\Models\\Soccerball»); ball[0].position = new Vector3(-30, 40, -30); ball[1].Load(content, «Content\\Models\\SoccerballGreen»); ball[1].position = new Vector3(50, 30, -50); ball[2].Load(content, «Content\\Models\\SoccerballRed»); ball[2].position = new Vector3(-50, -30, 40); aspectRatio = (float)x / (float)y; } /// /// Рисуем заставку /// public void DrawScreen(SpriteBatch spriteBatch, GraphicsDeviceManager graphics, int x, int y, GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.DarkGreen); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(title, new Vector2(x / 2 - title.Width / 2, 30), Color.White); spriteBatch.Draw(screen, new Vector2(x / 2 - screen.Width / 2, y / 2 – screen.Height / 2), Color.White); spriteBatch.Draw(esc, new Vector2(x / 2 - esc.Width / 2, y - 30 – esc.Height), Color.White); for (int i = 0; hole.Length > i; i++) { spriteBatch.Draw(hole[i], position[i], Color.White); } spriteBatch.End(); graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(new Vector3(0.0f, 0.0f, 260.0f), Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); Здесь мы используем уже две матрицы вращения по осям X и Z. angle += (float)(gameTime.ElapsedGameTime.TotalSeconds * 1.0f); rotationMatrixY = Matrix.CreateRotationY((float)angle); rotationMatrixZ = Matrix.CreateRotationZ((float)angle); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world * rotationMatrixY * rotationMatrixZ, view, proj); } } } }
21.4. Создаем меню игры
Задача книги состоит в том, чтобы по возможности показать вам как можно больше различных механизмов реализации игр, в том числе и меню, поэтому в игре «Футбольный стрелок» мы сделаем новое меню, отличное от предыдущей игры.
Для формирования игрового меню в проекте Football_arrows создается отдельный класс MenuGame. Это класс основан на исходном коде классов HelpScreen, AboutScreen и BooksScreen, но с небольшими дополнениями. Прежде чем переходить к изучению исходного кода игрового меню, давайте сначала рассмотрим, из каких графических элементов состоит меню, а также проанализируем общий принцип его работы. Для оформления меню используются название игры «Футбольный стрелок», надпись «Нажмите – А», три мячика, которые, как и в заставках Помощь, Об игре и Книги, будут летать по экрану, а также четыре новых графических изображения. Два изображения – это нарисованные девушки, стоящие слева и справа по бокам экрана телевизора (для красоты). Еще один новый графический файл – это футбольное поле, но значительно меньшего размера, чем в заставках, которое повернуто на 90 градусов (рис. 21.5). Последний графический элемент меню – это простаяn прямоугольная текстура размером 220 × 40 пикселей, выкрашенная в желтый цвет.
Рис. 21.5. Меню игры
На футбольном поле располагаются пять команд меню: Игра, Помощь, Об игре, Книги и Выход. Все буквы команд меню вырезаны из изображения футбольного поля редактором Photoshop. Это как если взять листок бумаги и нарисовать на нем любое слово, а затем аккуратно вырезать по контуру все буквы этого слова ножницами. Абсолютно идентичный механизм применяется и у нас, а вот зачем это сделано, давайте разбираться. Вы точно играли в игры, в меню которых одна из выбранных команд, напри- мер New Game, красилась одним из цветов, отличным от не активных в данный момент команд. В этом меню переход по командам происходит по нажатии кнопок с командами Вверх и Вниз (Down и Up на GamePadDPad). В тот момент, когда вы переходите на новую команду меню, эта строка текста закрашивается одним из цветов, и таким образом вы знаете, какая из команд активна в данный момент. Вот точно такое же меню мы реализуем в игре «Футбольный стрелок». Способов, как всегда, очень много, но мы остановимся на одном из них. Вернемся к листку бумаги. Давайте возьмем простой листок бумаги, покрасим его в темно-зеленый цвет и положим на стол – это будет у нас фон меню. Затем возьмем еще один листок бумаги, меньший по размеру, покрасим его в светло-зеленый цвет и нарисуем на нем футбольное поле. Потом на этом футбольном поле мы нарисуем команды меню и вырежем их аккуратно ножницами. Положив второй листок бумаги с футбольным полем поверх основного фона, мы увидим, что все команды меню стали хорошо заметны, поскольку они приняли более темный цвет фона. Теперь нам нужно сделать так, чтобы одна из команд (активная в данный момент) закрашивалась в желтый цвет. Для этих целей вырежем прямоугольник, размер которого будет по своей высоте на несколько миллиметров больше, чем высота букв в командах, а по ширине – соответствовать (или даже на пару миллиметров больше) самой длинной команде в меню. После этого закрасим прямоугольник желтым цветом и поместим его между фоном и футбольным полем, получив своего рода подложку для команд меню. При этом прямоугольник должен размещаться точно под одной из команд. Тогда эта команда получит в качестве основного цвета желтый, а все остальные команды – зеленый цвет. То есть мы элементарно подсвечиваем желтым цветом активную на данный момент команду. Теперь нам необходимо только расчетливо перемещать этот прямоугольник по командам меню. Для этого необходимо создать простой механизм в классе MenuGame, который будет устанавливать желтый прямоугольник на определенную позицию в пространстве под активную в текущий момент команду. Задача не сложная, и все расчеты мы будем вести от центра экрана, поскольку заставка футбольного поля привязана именно к этой части экрана. Переходим к исходному коду класса MenuGame, который дан в листинге 21.3. По мере изучения программного кода будем комментировать заслуживающие внимания нововведения.
//========================================================================= /// /// Листинг 21.3 /// Глава 21 /// Класс: MenuGame /// //========================================================================= #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 { class MenuGame {
В отличие от заставок, в меню игры мы загружаем меньшее по размеру футбольное поле, в котором команды меню вырезаны средствами Photoshop.
public Texture2D screen; public Texture2D title;
Объект cursor представляет в меню желтую прямоугольную текстуру или подложку меню размером 220 × 40 пикселей. Именно это изображение мы будем передвигать под командами меню, создавая эффект закрашивания команды желтым цветом.
public Texture2D cursor; public Texture2D enter;
Два объекта g1 и g2 представляют в игре изображение двух нарисованных девушек, которые располагаются по бокам меню.
public Texture2D g1; public Texture2D g2; public Texture2D[] hole = new Texture2D[30]; public Vector2[] position = new Vector2[30]; Random rand = new Random(); private ModelClass[] ball = new ModelClass[3]; static float aspectRatio; static float FOV = MathHelper.PiOver4; static float nearClip = 1.0f; static float farClip = 1000.0f; float angle; Matrix view; Matrix proj; Matrix world; Matrix rotationMatrixY; Matrix rotationMatrixZ; /// /// Загрузка компонентов заставки /// public void InitializeMenuGameScreen(ContentManager content, int x, int y) { screen = content.Load(«Content\\Textures\\menu»); title = content.Load(«Content\\Textures\\title»); enter = content.Load(«Content\\Textures\\enter»); cursor = content.Load(«Content\\Textures\\cursorMenu»); g1 = content.Load(«Content\\Textures\\g1»); g2 = content.Load(«Content\\Textures\\g2»); for (int i = 0; hole.Length > i; i++) { hole[i] = content.Load(«Content\\Textures\\hole»); } for (int i = 0; position.Length > i; i++) { position[i].X = rand.Next(200, x - 250); position[i].Y = rand.Next(100, y - 60); } for (int i = 0; ball.Length > i; i++) { ball[i] = new ModelClass(); } ball[0].Load(content, «Content\\Models\\Soccerball»); ball[0].position = new Vector3(-30, 40, -30); ball[1].Load(content, «Content\\Models\\SoccerballGreen»); ball[1].position = new Vector3(50, 30, -50); ball[2].Load(content, «Content\\Models\\SoccerballRed»); ball[2].position = new Vector3(-50, -30, 40); aspectRatio = (float)x / (float)y; }
В метод DrawScreen() добавляется еще один новый параметр state. На базе переданного значения этого параметра в методе DrawScreen() посредством оператора switch мы будем выбирать, в каком месте рисовать желтую подложку для меню.
/// /// Рисуем заставку /// public void DrawScreen(SpriteBatch spriteBatch, GraphicsDeviceManager graphics, int x, int y, GameTime gameTime, int state) { graphics.GraphicsDevice.Clear(Color.DarkGreen); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(title, new Vector2(x / 2 - title.Width / 2, 30), Color.White); spriteBatch.Draw(enter, new Vector2(x / 2 - enter.Width / 2, y - 30- enter.Height), Color.White);
Выбираем для девушек позиции на экране телевизора. Первая девушка рисуется с левой стороны экрана, а вторая – с правой стороны. Очень важно не задавать жесткие числовые значения для всех элементов меню, поскольку разрешение экранов у разных пользователей будет, естественно, разным. Всегда пользуйтесь для своих вычислений точкой в центре экрана, углами и четырьмя сторонами телевизора. В этом случае компоновка графики в игре будет одинаково выглядеть на всех экранах.
spriteBatch.Draw(g1, new Vector2(0, y / 2 - g1.Height/2), Color.White); spriteBatch.Draw(g2, new Vector2(x - g2.Width, y / 2 – g2.Height/2), Color.White);
В меню имеются пять команд, соответственно у нас есть пять состояний, или пять вариантов выбора позиций для подложки меню. В классе Game1 с помощью джойстика и GamePadDPad в игре мы будем переходить по командам меню, а значит, выбирать одну из команд. Каждой команде меню сверху вниз назначена своя цифра, поэтому выбор одной из команд меню будет соответствовать порядковому числовому значению. Метод DrawScreen() вызывается на каждой итерации игрового цикла, и состояние переменной state будет всегда «свежим». А уже на базе полученного значения оператор switch выберет, какой из блоков исходного кода должен работать в данный момент и в точности в каком месте дисплея должна рисоваться желтая подложка меню.
switch (state) { case 1: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 – 115), Color.White); break; case 2: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 – 65), Color.White); break; case 3: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 – 15), Color.White); break; case 4: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 + 30), Color.White); break; case 5: spriteBatch.Draw(cursor, new Vector2(x / 2 - cursor.Width / 2, y / 2 + 80), Color.White); break; }
Если вы не совсем разобрались с расчетами позиций подложки на экране, то возьмите обычный листок бумаги и карандаш. Нарисуйте виртуальный экран телевизора и проследите за изменениями позиций желтой текстуры.
Для смены игровых состояний в классе Game1 мы применяем структуру CurentGameState и структурную переменную gameState. До этого момента эта переменная нас «выкидывала» сразу в игровой процесс. Теперь пришла пора изменить ее значение, которое будет запускать титульную заставку.
С титульной заставки по нажатии на джойстике кнопки с буквой А и смене значения переменной gameState с CurentGameState.SplashScreen на значение CurentGameState.GameScree в игре включится показ меню. В дальнейшем для смены состояний игры нам достаточно присвоить переменной gameState необходимое значение, и мы выберем то или иное игровое состояние. Теперь поговорим о механизме смены состояний и о показе заставок.
21.5.1. Создаем объекты для меню и заставок
Для вывода на экран меню и заставок нам необходимо в классе Game1 объявить объекты классов SplashScreen, MenuGame, HelpScreen, AboutScreen и BooksScreen.
private SplashScreen splash; private MenuGame menuGame; private HelpScreen help; private AboutScreen about; private BooksScreen books; А затем и создать эти объекты. splash = new SplashScreen(); menuGame = new MenuGame(); help = new HelpScreen(); about = new AboutScreen(); books = new BooksScreen();
После чего в методе Initialize() класса Game1 мы можем спокойно вызы- вать соответствующие методы классов SplashScreen, MenuGame, HelpScreen, AboutScreen и BooksScreen для загрузки в игру графики.
21.5.2. Обновляем состояние игры в методе Update()
Обработка нажатия кнопки А в меню и титульной заставке, а также обработка GamePadDPad для перехода по меню – это основная и самая главная проблема, которую нам нужно решить. Дело в том, что любое нажатие кнопки на джойстике и ее удерживание ведет к цикличному выполнению определенного условия, назначенного на эту клавишу. Например, посмотрите на блок кода, приведенный ниже.
if(currentState.Buttons.A == ButtonState.Pressed) { state += 1; }
Если создать такое условие, то нажатие кнопки А и ее удерживание ведет к постоянному увеличению переменной state. И даже если вы нажмете кнопку и моментально ее отпустите, совсем не факт, что вы отпустили эту кнопку в тот момент, когда переменная увеличится всего на одну единицу. Любая задержка нажатия кнопки А ведет к увеличению переменной state, и проследить значение этой переменной и тем более управлять ею просто не возможно. В связи с этим необходимо создать условие, которое вне зависимости от длительности нажатия и удерживания кнопки А (в данном случае) увеличивало бы переменную state ровно на одну единицу. Чтобы приведенный выше код работал корректно, а переменная state изме- нялась ровно на одну единицу за одно нажатие кнопки А, можно использовать следующую конструкцию кода.
В этой версии кода мы имеем дополнительную булеву переменную aReleased, значение которой всегда будет равно true, если кнопка А не нажата. Как только кнопка нажата и не отпущена, значение переменной моментально изменяется на false и блок кода, следующий за конструкцией операторов if/else, не выполняется. Все очень просто. Такой подход в отслеживании состояния дополнительной булевой переменной позволяет создать идеальную систему проверки отпускания любой нажатой кнопки или рычажка. Для работы с нашим меню и заставками нам необходимо в исходном коде класса Game1 создать три булевых переменных для отслеживания нажатий на кнопки А и GamePadDPad.
В момент запуска игры значение переменной state у нас равно единице.
public int state = 1;
Каждое нажатие GamePadDPad вверх или вниз автоматически будет измнять значение переменной state. Если нажат GamePadDPad с командой вниз, то значение переменной state увеличивается на единицу.
if (currentState.DPad.Down == ButtonState.Pressed && downReleased == true) { state += 1;
Для того чтобы значение переменной не было больше пяти или не выходило за пределы пяти команд, создана следующая проверка условия.
if (state > 5) state = 1; }
В этом случае если state станет больше пяти, то ей будет присвоено значение, равное единице. То есть как только позиция подложки на экране станет больше позиции, отведенной для пятой и последней команды меню, желтая текстура получит позицию, равную первой команде. Таким образом, желтая подложка меню будет перемещаться по командам меню циклично сверху вниз, а в следующем блоке кода – и снизу вверх.
if (currentState.DPad.Up == ButtonState.Pressed && upReleased == true) { state -= 1; if (state < 1) state = 5; }
В этой конструкции кода мы создали обработку условия для нажатия GamePadDPad. Механизм изменения значения переменной state одинаков, как и в случае с командой вниз, с той лишь разницей, что изменение state ведется в обратную сторону. В следующем блоке кода происходит обработка нажатия кнопки А. В зависимости от текущего значения переменной state (которая меняется по нажатии GamePadDPad и соответствует одной из выбранных команд меню) происходит изменение значения gameState, а значит, и выбор нового игрового состояния.
Далее в исходном коде класса Game1 и метода Update() в зависимости от значения структурной переменной gameState происходит обновление состоя- ния игры.
case CurentGameState.HelpScreen: { if (currentState.Buttons.B == ButtonState.Pressed) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.AboutScreen: { if (currentState.Buttons.B == ButtonState.Pressed) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.BooksScreen: { if (currentState.Buttons.B == ButtonState.Pressed) gameState = CurentGameState.MenuScreen; break; } case CurentGameState.GameScreen: { if (currentState.Buttons.Back == ButtonState.Pressed) gameState = CurentGameState.MenuScreen; MoveBall(); GamePadClick(); break; } case CurentGameState.GameOverScreen: { // переход по меню вверх и вниз if (currentState.DPad.Down == ButtonState.Pressed && downReleased == true) { stateGameOver += 1; if (stateGameOver > 3) stateGameOver = 1; } if (currentState.DPad.Up == ButtonState.Pressed && upReleased == true) { stateGameOver -= 1; if (stateGameOver < 1) stateGameOver = 3; } // Нажимаем кнопку А if (currentState.Buttons.A == ButtonState.Pressed && aReleased == true && stateGameOver == 1) { this.NewGame(level); gameState = CurentGameState.GameScreen; } if (currentState.Buttons.A == ButtonState.Pressed && aReleased == true && stateGameOver == 2) { gameState = CurentGameState.MenuScreen; } if (currentState.Buttons.A == ButtonState.Pressed && aReleased == true this.Exit(); } break; } case CurentGameState.VictoryScreen: { // переход по меню вверх и вниз if (currentState.DPad.Down == ButtonState.Pressed && downReleased == true) { stateVictory += 1; if (stateVictory > 3) stateVictory = 1; } if (currentState.DPad.Up == ButtonState.Pressed && upReleased == true) { stateVictory -= 1; if (stateVictory < 1) stateVictory = 3; } // Нажимаем кнопку А if (currentState.Buttons.A == ButtonState.Pressed && aReleased == true && stateVictory == 1) { this.NewGame(level); gameState = CurentGameState.GameScreen; } if (currentState.Buttons.A == ButtonState.Pressed && aReleased == true && stateVictory == 2) { gameState = CurentGameState.MenuScreen; } if (currentState.Buttons.A == ButtonState.Pressed && aReleased == true && stateVictory == 3) { this.Exit(); } break; } } // Проверяем, нажата кнопка А или отпущена if (currentState.Buttons.A == ButtonState.Pressed) { aReleased = false; } else if (currentState.Buttons.A == ButtonState.Released) { aReleased = true; } // Проверяем, нажат DPad.Down или отпущен if (currentState.DPad.Down == ButtonState.Pressed) { downReleased = false; } else if (currentState.DPad.Down == ButtonState.Released) { downReleased = true; } // Проверяем, нажат DPad.Up или отпущен if (currentState.DPad.Up == ButtonState.Pressed) { upReleased = false; } else if (currentState.DPad.Up == ButtonState.Released) { upReleased = true; } base.Update(gameTime);
21.5.3. Обновляем графику в методе Draw()
В методе Draw() класса Game1 на основе значения структурной переменной gameState происходит рисование того или иного экрана. Изначально при запуске игры переменная gameState равна CurentGameState.SplashScreen, и это значит, что первой на экране рисуется титульная заставка. После нажатия кнопки А, обработка нажатия которой происходит в только что рассмотренном методе Update(), значение переменной меняется на CurentGameState.MenuScreen. После чего на экране рисуется меню игры, в котором в зависимости от выбранной команды (вверх или вниз) и нажатия кнопки А происходит изменение состояния всей игры.
В меню игры мы передаем переменную state. В зависимости от ее текущего значения для желтой подложки меню выбирается соответствующая позиция на экране телевизора, которая указывает пользователю на активную в данный момент команду меню.
case CurentGameState.MenuScreen: { menuGame.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime, state); break; } case CurentGameState.HelpScreen: { help.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime); break; } case CurentGameState.AboutScreen: { about.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime); break; } case CurentGameState.BooksScreen: { books.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, gameTime); break; } case CurentGameState.GameScreen: { spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, -50), Color.White); spriteBatch.End(); graphics.GraphicsDevice.RenderState.DepthBufferEnable = true; view = Matrix.CreateLookAt(camera, Vector3.Zero, Vector3.Up); proj = Matrix.CreatePerspectiveFieldOfView(FOV, aspectRatio, nearClip, farClip); for (int i = 0; ball.Length > i; i++) { world = Matrix.CreateTranslation(ball[i].position); ball[i].DrawModel(world, view, proj); } world = Matrix.CreateTranslation(stadium.position); stadium.DrawModel(world, view, proj); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); cursor.DrawSprite(spriteBatch); spriteBatch.End(); break; } case CurentGameState.GameOverScreen: { gameOver.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, stateGameOver); break; } case CurentGameState.VictoryScreen: { victory.DrawScreen(spriteBatch, graphics, screenWidth, screenHeight, stateVictory); break; } base.Draw(gameTime); }
21.6. Добавим в игру логику
Смысл всей игры заключается в том, чтобы не дать мячикам упасть на футбольное поле, но стрелять по мячам можно до бесконечности. Поэтому ключом для окончания уровня будет служить двадцать точных попаданий в каждый мяч. Игроку на одном уровне необходимо попасть в каждый мяч как минимум двадцать раз, и при этом ни один из мячей за это время не должен коснуться земли. Если мяч касается земли, то игра останавливается, и пользователю будет предложено пройти текущий уровень с начала. Нужно помнить о том, что по истечении двадцати попаданий в один мяч этот самый мяч по-прежнему будет падать вниз, и его попрежнему нужно будет поддерживать на весу. Таким образом, все мячики остаются активными в сцене на протяжении уровня, что придаст игре динамику.
Подсчет попаданий в каждый мячик мы будем вести на табло (рис. 21.6). Это табло размещается в левом верхнем углу экрана с отступом от его краев по 80 пикселей для каждой оси. Не забывайте о возможных искажениях текста на экране телевизора, да и сам текст должен быть значительно крупнее, чем в компьютерных играх (рис. 21.7).
Рис. 21.6. Табло игры
В нашем табло нарисованы три мяча, и, как уже говорилось, точка его вывода – это отступ в 80 пикселей по двум осям. Три мячика, нарисованные на табло, соответствуют по цвету трем мячам, используемым в игре. Каждое попадание в мяч увеличит счетчик на единицу, и над каждым мячом будет рисоваться текущее количество попаданий в цель. По достижении цифры двадцать для одного или более мячей на табло дополнительно будет выводиться крестик, который рисуется точно поверх одного из мячей (рис. 21.6).
Рис. 21.7. Зона искажения телевизора
В результате игрок будет знать, какой из мячей ему нужно подбивать меньше и на каких мячах необходимо сосредоточить свое внимание. Дополнительно в нижней части выво- дится текущий уровень игры. На каждом новом уровне мы будем увеличивать скорость падения мячей. Для контроля окончания игры по причине выигрыша уровня используется следующая конструкция кода:
В коде экран VictoryScreen представляет экран победы. Исходный код этого класса фактически идентичен заставкам и меню игры, с той лишь разницей, что на экране присутствуют всего три команды: Продолжить, Меню и Выход. Техника обработки этих команд идентична схеме работы меню игры. Этот код нужно вызвать в основном цикле игры, чтобы он обновлялся на каждой итерации. Полный исходный код окончательной версии игры вы найдете на компакт-диске в папке Code\Chapter21\Football_arrows. Если игрок упустит мяч на поле, то в работу включается уже другая конструкция кода.
Здесь мы имеет также новую заставку GameOverScreen и новый класс GameOverScreen. Это класс однотипен и похож на все заставки и меню игры, разобраться самостоятельно с ним вам не составит труда. Единственное, на что я еще хочу обратить ваше внимание, – так это на новый механизм обработки нажатия кнопки джойстика Triggers.Right в классе Game1. Дело в том, что ситуация с длительностью удержания нажатия этой кнопки абсолютно аналогична рассмотренной нами ситуации с нажатием кнопки А. В связи с этим нам также необходимо добавить в код идентификатор отпускания нажатой кнопки Triggers.Right. Для этих целей используется та же методика, что и для кнопки. Сначала объявляем в глобальных переменных новую булеву переменную triggersReleased со значением true.
bool triggersReleased = true;
А затем в методе GamePadClick() создаем проверку нажатия и отпускания кнопки Triggers.Right. Дополнительно в обработке событий по нажатии кнопки GamePadThumbSticks.Left было изменено цифровое значение, определяющее степень нажатия этой кнопки и скорость движения прицела по экрану.
Тестирование конечной версии игры выявило неудачный подбор числа, опрделяющего степень нажатия кнопки (по сравнению с предыдущими примерами). Поэтому значение степени нажатия кнопки было уменьшено, а также уменьшена скорость движения прицела, что дало возможность в игре производить более точное прицеливание по мячам.
// GamePadThumbSticks Left if (currentState.ThumbSticks.Left.X < -0.35f) cursor.spritePosition.X -= 5; else if (currentState.ThumbSticks.Left.X > 0.35f) cursor.spritePosition.X += 5; if (currentState.ThumbSticks.Left.Y > 0.35f) cursor.spritePosition.Y -= 5; else if (currentState.ThumbSticks.Left.Y < -0.35f) cursor.spritePosition.Y += 5; // Обработка выхода курсора за пределы экрана if (cursor.spritePosition.X < 0) cursor.spritePosition.X = 0; else if (cursor.spritePosition.X > screenWidth – cursor.spriteTexture.Width) cursor.spritePosition.X = screenWidth - cursor.spriteTexture.Width; if (cursor.spritePosition.Y < 0) cursor.spritePosition.Y = 0; else if (cursor.spritePosition.Y > screenHeight – cursor.spriteTexture.Height) cursor.spritePosition.Y =screenHeight- cursor.spriteTexture.Height; for (int i = 0; bb.Length > i; i++) { bb[i].Center = ball[i].position; bb[i].Radius = ball[i].radius; } if (currentState.Triggers.Right > 0.5f && triggersReleased == true) { Sound.PlayCue(soundList.Shot); Ray pickRay = GetPickRay(); … } // Проверяем, нажат Triggers.Right или отпущен if (currentState.Triggers.Right > 0.5f) { triggersReleased = false; } else { triggersReleased = true; } // если было по 20 попаданий в мяч, то переходим на следующий уровень if (score0 >= 20 && score1 >= 20 && score2 >= 20) { gameState = CurentGameState.VictoryScreen; Sound.PlayCue(soundList.Applause); level += 1; if (level > 8) level = 1; } }
Пожалуй, это все значащие нововведения в игре. В финальную версию игры «Футбольный стрелок» еще были добавлены звуковое оформление, которое основано на эффектах из Spacewar Windows Starter Kit, а также код по выводу на экран табло, текста и логика игры. Все эти дополнительные действия мы изучали еще во второй части главы.