Добавляем в игру новые уровни Играя в любую игру, пользователь всегда надеется на длительное продолжение игрового процесса. Поэтому большинство игр имеют определенный набор уровней. По прохождении одного из уровней игроку предлагается пройти следующий уровень и т. д. Если один из уровней был не пройден, то игрок должен пройти его вновь либо начать игру с места последнего игрового сохранения или пройденной контрольной точки. Любая из перечисленных методик прохождения уровней в конечном счете определяет общую стратегию прохождения игры в целом. Поэтому на этапе проектирования игры этому очень важному аспекту нужно уделить много времени. В нашей игре данному вопросу на начальном этапе мы не уделяли должного внимания, поскольку вы тогда еще не знали, как делаются игры и из чего они состоят. Но прежде чем начинать работать над этой книгой, я сначала разработал схему игрового меню, механизм перехода с уровня на уровень, потом сконструировал модель игровых классов. Затем попросил нарисовать к игре несколько концептов игровой графики и только после всех этих стадий приступил к написанию исходного кода игры. По окончании работы над игрой был придуман подход в представлении вам материала книги (от простого к сложному), и соответственно весь исходный код был поделен на главы. И только потом я приступил к написанию самой книги. То есть за всей книгой (как и самой программой) скрывается на самом деле большая работа, которая практически идентична любой предварительной работе по созданию игры. Нельзя просто сесть за компьютер и написать игру с чистого листа, точнее, сделать это, конечно, можно, но делать вы эту игру будете точно долго. Это как в том старом анекдоте, где программист спрашивает другого программиста: что пишешь, а тот отвечает: а вот откомпилируем – и узнаем. Чтобы не пойти по тропинке программиста из анекдота, изначально необхо- димо уделить проектированию игры столько времени, сколько это еобходимо. И только после того как вы определитесь со всеми винтиками, гайками и шайбами вашей игры, можно приступать к ее написанию. В нашей игре для перехода с уровня на уровень также была изначально придумана определенная схема. В чем эта схема заключается и как ее реализовать, вы узнаете из этой главы. Вашему вниманию будет представлен последний проект в этой части книги под названием NewLevels. В этом проекте мы добавим в игру новые уровни и создадим механизм перехода с уровня на уровень.
14.1. Переход с уровня на уровень Продолжение игры, или переход с уровня на уровень, обязан происходить по заданному (естественно, вами как программистом) событию. Такое событие может быть ликвидацией всех врагов на уровне или главного босса, окончанием уровня по прошествии определенного промежутка времени или после выполнения игровой задачи на этом уровне либо по достижении заданного количества очков и т. д. То есть важно создать определенное условие, после выполнения которого игрок может перейти к своей следующей миссии. В нашей игре переход с уровня на уровень происходит по количеству набранных очков. Каждый раз при переходе на следующий уровень планка набранных очков будет повышаться. Дополнительно на каждом уровне будет устанавливаться различное количество начисляемых очков за пойманный объект. В итоге каждый новый уровень будет несколько усложнен в отличие от предыдущего. Но в идеале, конечно, в каждом новом уровне игры вы можете изменять игровой фон, падающие объекты, добавить различных сложностей и артефактов и т. д. Сейчас моя задача заключается лишь в том, чтобы научить и показать вам, как это делается, а все остальное – это дело вашей фантазии. В игре «Летящие в прерии» после набора заданного количества очков пользователь сможет перейти на новый уровень. Для того чтобы реализовать этот механизм, нам понадобится еще одна булева переменная, которая представит промежуточный экран перехода с уровня на уровень, так, как мы это делали с меню. На этом экране мы покажем информацию о набранных очках, а также предложим игроку на выбор три действия: продолжение игры, выход в меню игры и закрытие программы. Единственное, что сейчас необходимо, – это проработать механизм набора очков и задать верхнюю планку, пройдя которую игроку будет представлен этот самый промежуточный экран или экран с выбором нового уровня. Посмотрите на рис. 14.1, где представлен дополнительный элемент в игровом механизме.
Рис. 14.1. Игровые состояния
14.2. Набранные очки В предыдущих примерах для подсчета очков нами использовались пять следующих переменных:
int score0, score1, score2, score3, score4;
Эти переменные вели простой подсчет пойманных объектов, а затем мы выводили эту информацию на экран. В новом проекте NewLevels мы изменим назначение этих переменных. Теперь все перечисленные переменные будут содержать определенное количество баллов и будут привязаны к каждому из падающих объектов. Когда ковер-самолет поймает один из объектов, то согласно значению одной из переменных будет выполнен набор или снятие очков с игрока. Соответственно для подсчета общего количества очков в исходный код вводится новая переменная totalScore. Дополнительно мы введем в программу еще одну переменную под названием endScore. В этой переменной мы будем хранить то количество очков, которое необходимо набрать пользователю для перехода к следующему уровню. Тогда, чтобы перейти на следующий уровень, нам необходимо всего лишь создать следующую проверку условия:
if(totalScore >= endScore) { // механизм показа экрана с выбором нового уровня. }
Такую конструкцию кода достаточно поместить в игровой цикл, и проверка условия будет осуществляться на каждой итерации игрового цикла. В свою очередь, увеличение переменной totalScore может происходить следующим образом:
Переменные score0, score1, score2, score3 и score4 мы жестко привяжем к каждому падающему объекту и сделаем это в соответствии с числовыми обозначениями объектов в массиве данных и самих переменных. Касание объекта и ковра будет приводить к увеличению или уменьшению общего количества очков. Инициализация всех перечисленных переменных производится непосредственно в методе NewGame(), который также несколько видоизменяется. Но давайте обо всем по порядку и перейдем к работе над проектом NewLevels.
14.3. Проект NewLevels Между уровнями игры пользователю будет показан промежуточный экран. Этот экран представлен новой табличкой (рис. 14.2). В этой табличке будут выводиться различные сведения информационного характера по текущему состоянию игры. Когда пользователь пройдет очередной уровень, то игра будет остановлена и появится промежуточный экран с этой табличкой, где пользователю будет предложено продолжить уровень, выйти в меню или закрыть программу.
Рис. 14.2. Табличка перехода с уровня на уровень
Последнее графическое нововведение в игре касается изменения режима паузы. Ранее в этом режиме мы писали на экране слово Пауза. Теперь вместо этого слова на экране будет появляться табличка с информацией о том, какие команды необходимо выполнить для продолжения игры или для выхода в меню программы (рис. 14.3).
Рис. 14.3. Табличка для режима паузы
14.3.1. Изменения в классе Game1 Начинаем работать над новым проектом и классом Game1. Прежде всего добавим в область глобальных переменных этого класса несколько новых переменных.
private int totalScore; private int endScore; private bool gameState; private bool levelState; private int level; private int tempLevel; private Texture2D gameLevel; private Texture2D pausedTexture;
Первые две переменные, как мы уже выяснили, служат соответственно для подсчета общего количества набранных очков и количества очков, которое необходимо набрать пользователю для перехода на следующий уровень. Следующие две булевы переменные gameState и levelState понадобятся нам для отслеживания состояния игры. О них мы поговорим подробнее чуть позже в этой главе, когда дойдем до игрового цикла. Следующие две переменные в этом блоке кода level и tempLevel представляют текущий уровень игры. Зачем нужны сразу две переменные для представления одного уровня? Дело в том, что переменная level будет изменяться по прошествии одного из уровней (увеличиваться на единицу), но для информационной таблички, появляющейся между уровнями, нам понадобится показать, какой из уровней был только что пройден, а переменная level к этому времени уже увеличится на единицу. Поэтому и применяется дополнительная переменная tempLevel. Два последних объекта gameLevel и pausedTexture класса Texture2D представляют соответственно междууровневую табличку (рис. 14.2) и табличку режима паузы (рис. 14.3). В методе LoadGraphicsContent() мы загрузим эти два графических файла, которые по традиции располагаются в рабочем каталоге проекта в папке \Content\Textures, после явного добавления их в проект. В конструкторе класса Game1 происходит инициализация объявленных переменных.
На начальном этапе игры мы задаем значение переменной level, равное единице, для того чтобы игрок мог начать играть непосредственного с первого уровня. Если в игре предусмотрена система сохранения прошедших уровней, то это как раз то место, где можно прочитать из памяти сохраненные данные и присвоить переменной level уровень, с которого пользователь должен продолжить играть.
В справочной системе XNA Game Studio Express (Programming Guide => Storage) вы найдете всю необходимую информацию о механизме сохранения различных данных на жестком диске. Особенно советую обратить внимание на пример под названием How to: Serialize Data. В этом примере созданы два универсальных метода для записи данных на диск и их чтения. Эти методы очень легко использовать для сохранения игровых данных на жестком диске приставки Xbox 360. Исходный код примеров не сложен, и, надеюсь, вы уже в силах разобраться с ним самостоятельно.
Две булевы переменные gameState и levelState устанавливаются в состоя- ние false. Перейдем к игровому циклу в метод Update() и посмотрим, для чего нам понадобились эти две новые переменные.
В этом блоке кода происходит выбор состояния игры на основе булевых переменных. Всего имеются три состояния: * показывать меню, если menuState равно true; * играть в игру, если gameState равно true; * показывать межуровневый экран, если levelState равна true. После того как пользователь вошел в игру, запускается работа меню. Если пользователь выбрал команду Играть, то происходит выполнение следующего блока кода:
В коде происходит присвоение переменной level значения уровня, на котором пользователь будет играть (здесь также можно читать данные из памяти, сохраненные ранее). Затем вызывается метод NewGame(), в параметр которого передается значение переменной level, а значит, и текущий уровень. В методе NewGame() добавляется конструкция кода с оператором switch. На базе входящей переменной level оператор switch выбирает новые значения для каждого последующего уровня.
switch(curentLevel) { case 1: score0 = 5; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 100; break; case 2: score0 = 10; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 200; break; case 3: … break; case 4: … break; case 5: … break; case 6: … break; case 7: … break; case 8: … break; }
В игре было задано восемь уровней, и, честно говоря, я не сильно усердствовал, когда назначал баллы за подборы объектов и порог набранных очков. В реальной игре вам необходимо обязательно уделить этому моменту очень много времени и протестировать игру и все имеющиеся в ней уровни. Это очень важно, и от этого зависят интересность и состоятельность вашей игры. Естественно, что количество уровней в игре может быть любым, кроме того, вы можете добавить на каждый новый уровень новые объекты, фон и многое другое.
Когда будете создавать конструкцию кода с использованием оператора switch, не забывайте в конце очередного блока case прописывать ключевое слово break. Иначе у вас вместо блочной работы этой конструкции будет происходить ее построчное выполнение.
В игровом процессе, как только пользователь достигает верхней планки очков, назначенных на текущий уровень, происходит вызов следующего блока исходного кода:
Здесь мы присваиваем переменной gameState значение false для вывода из игрового процесса пользователя. Переменная menuState также имеет значение false, но она и имела это значение раньше, тут мы просто подстраховываемся на всякий случай. Далее переменная levelState получает значение, равное true, что приведет нас в дальнейшем (после выхода из этого блока кода) к выполнению метода LevelSelect(). В конце блока кода переменной tempLevel присваивается значение текущего уровня. Сам уровень увеличивается на единицу, а в блоке кода
if(level > 8) { level = 1; }
происходит проверка, достиг ли игрок последнего уровня. Если достиг, то сбрасываем level к первому уровню, если нет, то оставляем все как прежде. Метод LevelSelect() следит за нажатыми пользователем кнопками джойстика и выбирает в связи с полученной информацией с устройства ввода определенные действия.
public void LevelSelect() { GamePadState currentState = GamePad.GetState(PlayerIndex.One);
else if (currentState.Buttons.X == ButtonState.Pressed) { this.Exit(); } }
В работе этой конструкции кода, думается, все предельно ясно, интересно другое: что показывает нам игра, когда работает межуровневый экран. В методе Draw() класса Game1, где происходит прорисовка графической составляющей, также формируется логическая конструкция, позволяющая выбирать, что именно показывать в конкретный игровой момент. Особенно интересно состояние игры на межуровневом промежутке, когда переменная levelState равна значению true. Этот приведенный ниже блок кода относится к межуровневому состоянию игры.
Ранее в классе Menu мы имели всего два состояния в методе Draw() этого класса. В первом и во втором состояниях на экран поверх основного фона меню выводились две таблички. Теперь в дополнительном третьем состоянии эти таблички не выводятся, а на фоне меню рисуется табличка для межуровневого состояния.
В идеале в игре вы можете создать базовую фоновую заставку и использовать ее на любом участке игровых состояний, определяя, что именно вы хотите видеть поверх этой заставки. И не забывайте об очередности рисования на экране графики, все то, что вызывается в методе Draw() позже, соответственно и рисуется позже. В листинге 14.1 представлен полный исходный код класса Game1. Файлы последнего проекта двухмерной игры для приставки Xbox 360 находятся на диске в папке Code\Chapter14\NewLevels.
#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 NewLevels { 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;
private int totalScore; private int endScore; private bool gameState; private bool levelState; private int level; private int tempLevel; private Texture2D gameLevel;
/// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; for (int i = 0; sprite.Length > i; i++) { sprite = new Sprite(12, 10); } platform = new Sprite(); menu = new Menu(); menuState = true; cursorState = 1; level = 1; tempLevel = level; gameState = false; levelState = false; } /// /// Инициализация /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background = content.Load(«Content\\Textures\\background»); platform.Load(content, «Content\\Textures\\platform»); sprite[0].Load(content, «Content\\Textures\\0»); sprite[1].Load(content, «Content\\Textures\\1»); sprite[2].Load(content, «Content\\Textures\\2»); sprite[3].Load(content, «Content\\Textures\\3»); sprite[4].Load(content, «Content\\Textures\\4»); menu.Load(content); pausedTexture = content.Load(«Content\\Textures\\paused»); gameLevel = content.Load(«Content\\Textures\\game»); }
} /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { … } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { GamePadState currentState = GamePad.GetState(PlayerIndex.One); // Переход на новый уровень if (levelState == true) { LevelSelect(); } // Показываем меню 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) { level = 1; this.NewGame(level); menuState = false; levelState = false; gameState = true; } elseif(currentState.Buttons.A==ButtonState.Pressed&&cursorState==2) { this.Exit(); } } // Запускаем игру
if (gameState == true) { // Выход в меню if (currentState.Buttons.Back == ButtonState.Pressed) { levelState = false; gameState = false; menuState = true; } Pause(); if (paused == false) { if (totalScore >= endScore) { gameState = false; menuState = false; levelState = true; tempLevel = level; level += 1; if (level > 8) { level = 1; } } double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite.UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); } } Sound.Update(); base.Update(gameTime); } /// /// Движение спрайта по вертикали /// public void MoveSprite() { … } /// /// Движение ковра-самолета /// public void MovePlatform() { …
} /// /// Пауза в игре /// public void Pause() { … } /// /// Столкновения /// public void Collisions() { … } /// /// Новая игра /// public void NewGame(int curentLevel) { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite.spritePosition = new Vector2(rand.Next(10, screenWidth - 150), j = j - 300); } platform.spritePosition = new Vector2(screenWidth / 2, screenHeight - 90); switch (curentLevel) { case 1: score0 = 5; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 200; break; case 2: score0 = 10; score1 = -20; score2 = 5; score3 = -20; score4 = 5; totalScore = 0; endScore = 200; break; case 3: