Движение спрайтов в пространстве Одной из главных составляющих любой игры является перемещение объектов в игровом пространстве. Часть таких объектов может перемещаться в соответствии с командами, олучаемыми от пользователя через устройство ввода информации (клавиатура, джойстик или мышь). Как правило, это либо главный герой игры, либо определенный инструмент, играющий главную роль в игровом процессе, как это задумано в нашей игре. Как вы помните, у нас в качестве такого инструмента выступает ковер-самолет, парящий над пропастью и ловящий различные падающие объекты. Другая часть объектов игры может перемещаться на основе своей логики, запрограммированной создателем. Такая логика в играх называется игровым скусственным интеллектом. В этой главе мы наделим наши игровые объекты ростейшими алгоритмами игровой логики, научив их перемещаться в заданном направлении, а также выбирать себе случайное место в игровом пространстве. Использование более мощных алгоритмов искусственного интеллекта в нашей игре не понадобится, но если вы интересуетесь этим вопросом серьезно, то рекомендую вам изучить книгу здательства «ДМК Пресс» «Программирование искусственного интеллекта в приложениях», автор М. Тим Джонс. Итак, что такое движение в пространстве? Каждый спрайт при выводе на экран телевизора получает свои координаты по двум осям X и Y, например: Vector2 spritePosition = new Vector2(100, 300); Также может иметь место и следующая запись: Vector2 spritePosition; spritePosition.X = 100; spritePosition.Y = 300; В этих строках кода позиция для спрайта задается по оси X в 100 пикселей (от левого верхнего угла экрана) и по оси Y в 300 пикселей вниз все от того же верхнего левого угла. Чтобы переместить спрайт в заданном направлении, необходимо просто изменять (увеличивать или уменьшать) позицию спрайта. То есть если вы желаете переместить спрайт сверху вниз (только по оси Y), то необходимо на каждой итерации игрового цикла (один такт) или в один проход исходного кода метода Update() класса Game1 увеличить позицию спрайта на определенное количество пикселей, например: // Обновляем состояние игры protected override void Update(GameTime gameTime) { spritePosition.Y += 3; } В этом примере мы на каждой итерации игрового цикла, то есть за один такт, увеличиваем позицию спрайта на 3 пикселя. Циклическое увеличение позиции спрайта по оси Y на 3 пикселя в каждой итерации игрового цикла (перерисовка спрайта на новом месте) создаст эффект движения спрайта по экрану сверху вниз. Получается, что значение в 3 пикселя задает фактическую скорость для движения спрайта по экрану. Меньшее значение от 3 пикселей (например, 1 пиксель) уменьшит скорость перемещения спрайта по экрану, а большее значение (например, 5 пикселей) соответственно увеличит скорость движения спрайта. В целом выбирать скорость для движения объектов в игре необходимо аккуратно и на основе большого количества тестов игры. Дело в том, что очень много хороших и красивых игр из-за не правильно выбранной скорости объектов и главного героя проигрывают менее удачным играм. Если выбрать маленькую скорость для объекта, то пользователю может казаться, что игра несколько тормозит и лишена динамики. Если, в свою очередь, скорость будет сильно большой, то осуществлять контроль над игрой будет чрезвычайно трудно. Но в любом случае все зависит именно от той задачи, которую вы реализовываете в вашей игре. Задавая скорость только по одной оси координат, вы соответственно будете передвигать спрайт вдоль этой оси, а если задавать скорость по обеим осям координат, то спрайт будет перемещаться под определенным углом. Например, если скорость по осям X и Y равна по 3 пикселя, то этот угол будет равен четко 45 градусам. Если используются неравные значения по осям, то угол движения будет либо меньше, либо больше. Дополнительно можно использовать и отрицательные значения для скорости, в этом случае позиция спрайта будет уменьшаться. Например, в приведенном выше примере это будет выглядеть следующим образом: Vector2 spritePosition; spritePosition.X = 100; spritePosition.Y = 300; ... // Обновляем состояние игры protected override void Update(GameTime gameTime) { spritePosition.Y -= 3; } В этом исходном коде мы получаем движение спрайта снизу вверх по оси Y. Если такой подход использовать для оси X, то движение будет происходить справа налево. Посмотрите на рис. 8.1, где схематически представлен механизм движения спрайта в разных направлениях со скоростью в 3 пикселя.
Рис. 8.1. Техника движения спрайтов на экране Надеюсь, теперь смысл перемещения спрайтов в пространстве вам стал понятен, поэтому мы можем смело переходить к работе над проектами этой главы. В частности, вашему вниманию будут предложены два проекта: MoveSprite и MoveSpriteArray. В первом проекте MoveSprite нашей целью будет перемещение спрайта по экрану сверху вниз. При этом как только спрайт будет достигать нижней кромки экрана, то он будет исчезать из нашего поля зрения, поскольку его текущая позиция станет уже за пределами нашей зоны видимости. Чтобы корректно обрабатывать данную ситуацию, мы создадим простой механизм, который должен будет следить за спрайтом на экране, и как только объект исчезнет с экрана, ему будет назначаться новая позиция в верхней части экрана и перемещение возобновится снова в том же ключе. Таким образом, спрайт в игре будет двигаться циклично сверху вниз. Во втором проекте MoveSpriteArray задача несколько усложнится, и в игру добавятся еще четыре дополнительных спрайта. В итоге в игре задействуются сразу пять различных объектов. Для всех пяти объектов будет организован механизм обработки ситуации выхода за пределы экрана в нижней части телевизора и установка на новые позиции. Теперь перейдем к новому проекту MoveSprite, который вы должны создать на базе последнего проекта предыдущей главы. 8.1. Проект MoveSprite Прежде всего в новом сформированном проекте MoveSprite в классе Sprite необходимо добавить новую переменную speedSprite, которая будет задавать скорость движения спрайта в пикселях. public Vector2 speedSprite = new Vector2(0, 6); В этой строке кода мы задаем скорость по оси X равной нулю, а скорость по оси Y равной 6 пикселям. Это означает, что по оси X положение спрайта будет оставаться всегда неизменным, а вот по оси Y на каждой новой итерации или на одном проходе метода Update() класса Game1 текущее положение спрайта будет увеличиваться на 6 пикселей. Теперь перейдем к исходному коду класса Game1 проекта MoveSprite. Посмотрим на весь исходный код этого класса в листинге 8.1, после чего приступим к его детальному анализу. //========================================================================= /// /// Листинг 8.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 8 /// Проект: MoveSprite /// Класс: Game1 /// Движение спрайта по экрану /// //========================================================================= #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 MoveSprite { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; SpriteBatch spriteBatch; Sprite sprite; private Texture2D background; int screenWidth, screenHeight; Random rand = new Random(); /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; sprite = new Sprite(12, 10); } /// /// Инициализация /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, «Content\\Textures\\sprite»); background = content.Load(«Content\\Textures\\background»); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { … } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; sprite.UpdateFrame(elapsed); MoveSprite(); base.Update(gameTime); } /// /// Движение спрайта по вертикали /// public void MoveSprite() { sprite.spritePosition += sprite.speedSprite; if (sprite.spritePosition.Y > screenHeight) { sprite.spritePosition = new Vector2(rand.Next(50, screenWidth - sprite.spriteTexture.Width / 12 - 50), -200); } } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, 0), Color.White); sprite.DrawAnimationSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } } В самом начале исходного кода класса Game1 происходит объявление гло- бальных объектов и добавляются две новые строки кода. int screenWidth, screenHeight; Random rand = new Random(); В первой строке кода мы создаем две дополнительные переменные, которые впоследствии будут содержать соответственно ширину и высоту разрешения экрана, установленную нами в конструкторе класса Game1. graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; screenWidth = graphics.PreferredBackBufferWidth; screenHeight = graphics.PreferredBackBufferHeight; В свою очередь, системный класс Random позволяет в программах генерировать для заданной переменной какое-то новое случайное значение. В состав класса входит метод Next(), который при каждом новом вызове формирует последовательность случайных чисел. Метод Next() имеет три следующие модификации: * Random.Next() – возвращает положительное случайное число; * Random.Next(int32) – возвращает положительное случайное число, где единственный параметр метода – это верхняя граница, или максимальное число, выше которого не может быть сгенерировано новое значение. Например, если вызвать метод вот так: Next(20), то сгенерированное число не будет выходить из диапазона от 0 до 20; * Random.Next(int32, int32) – этот вид метода с двумя параметрами позволяет задавать диапазон от выбранного минимального числа до максимального числа. Например, вызов метода со значениями Next(15, 20) задаст диапазон для случайного числа в пределах от 15 до 20. Класс Random и метод Next() нам будут необходимы для задания новой позиции на экране телевизора в методе MoveSprite() класса Game1. Это новый метод, который добавляется в текущий проект и выглядит следующим образом: public void MoveSprite() { sprite.spritePosition += sprite.speedSprite; if (sprite.spritePosition.Y > screenHeight) { sprite.spritePosition = new Vector2(rand.Next(50, screenWidth - sprite.spriteTexture.Width / 12 - 50), -200); } } В первой строке кода метода MoveSprite() мы изменяем позицию объекта по оси Y, а значит, перемещаем спрайт на экране сверху вниз. Если развернуть эту строку записи (чтобы вам было понятно), то исходный код движения спрайта бу- дет выглядеть следующим образом: sprite.spritePosition.X += sprite.speedSprite.X; // где скорость по X равна 0 sprite.spritePosition.Y += sprite.speedSprite.Y; // где скорость по Y равна 6 Такая запись также может иметь место в исходном коде метода MoveSprite(), но представленный первый вариант несколько профессиональнее и быстрее работает. Оставшиеся три строки кода метода MoveSprite() обрабатывают ситуацию выхода спрайта за пределы экрана с нижней стороны экрана. Как только позиция спрайта оказывается больше, чем максимальный размер высоты экрана, то в исходном коде метода MoveSprite() происходит вход в конструкцию if/else, где определяется новая точка вывода спрайта на экран. sprite.spritePosition = new Vector2(rand.Next(50, screenWidth - sprite.spriteTexture.Width / 12 - 50), -200); Здесь мы используем метод Next(), с помощью которого при определении координаты по оси X задаем случайное значение в диапазоне от 50 пикселей до screenWidth - sprite.spriteTexture.Width / 12 – 50 или в числовом эквиваленте 1280 - один фрейм анимационной последовательности – 50 пикселей от кромки экрана для красоты, что позволяет нам при каждом новом определении места вывода спрайта на экране по оси X задавать новое значение в пределах видимой области экрана. С выбором случайных значений для новой позиции спрайта игрок не будет знать, в каком месте должен выводиться спрайт на экран, и впоследствии не сможет приспособиться к вашей игре. Новое место будет всегда неожиданным как для игрока, так, собственно, и для вас, потому что используется метод Next() с двумя параметрами, задающими определенный диапазон значений. Дополнительные 50 пикселей, которые мы отняли от ширины дисплея с двух сторон, необходимы просто для красоты, дабы спрайты не рисовались вплотную к краям экрана телевизора. Что касается задания координаты по оси Y, то она, как вы заметили, отрицательна. Это позволяет рисовать спрайт в верхней части экрана за пределами видимости. После чего спрайт мы перемещаем вниз (рис. 8.2). Таким образом достигается эффект плавного появления, или выезда спрайта из верхней части экрана и всего игрового пространства. Это красиво, эффектно, и если этого не делать и рисовать спрайт в положительных значениях оси Y, то объекты будут появляться резко и как бы ни с того ни с сего. Так делать нельзя, по крайней мере в нашей игре точно. В других играх могут быть свои задачи, поэтому этот подход имеет право на жизнь. После создания метода MoveSprite() необходимо поместить вызов этого метода в метод Update(GameTime gameTime) класса Game1. Тогда на каждой итерации игрового цикла будет происходить вызов метода MoveSprite() и соответственно обновление состояния спрайта, а также обработка ситуации с выходом спрайта из зоны видимости. Исходный код проекта вы найдете на компактдиске в папке Code\Chapter8\MoveSprite, а мы переходим к рассмотрению следующего проекта MoveSpriteArray. 8.2. Проект MoveSpriteArray Задача этого проекта заключается в том, чтобы добавить в игру дополнительные объекты. Сделать это можно несколькими способами, но самый «продвинутый» и профессиональный способ сводится к организации простого массива данных. Поэтому в проекте MoveSpriteArray мы реорганизуем способ объявления и создания спрайта на создание массива данных, состоящего из пяти спрайтов. Для этого в исходном коде класса Game1 проекта MoveSpriteArray вместо одиночного объявления спрайта появляется объявление массива спрайтов.
Рис. 8.2. Назначение спрайту новой позиции в игровом пространстве Sprite[] sprite = new Sprite[5]; Здесь мы объявляем массив данных, исчисляемый пятью спрайтами. Все эти пять спрайтов, а точнее каждый из спрайтов будет загружать в игру свое уникальное изображение и представлять тот или иной объект. Каждый спрайт использует анимационную последовательность для создания динамики в игре, посмотрите на рис. 8.3, где представлены пять изображений, загружаемых в игру. Все изображения, загружаемые в игру, размещаются в рабочем каталоге проекта в папке Content\Textures. Не забывайте, что все пять рисунков необходимо явно добавить в проект посредством команд Add -> Exiting Item, как мы это делали в предыдущих главах. Далее в конструкторе класса Game1 происходит создание пяти объектов. Для этих целей используется обычный цикл for. for (int i = 0; sprite.Length > i; i++) { sprite = new Sprite(12, 10); }
Рис. 8.3. Изображения, загружаемые в игру Единственное условие в этой конструкции кода заключается в том, чтобы все изображения имели одинаковую анимационную последовательность, то есть количество фреймов всех изображений должно быть одинаково (как у нас в игре). Если одно и более изображений имеют различное количество фреймов, то придется загружать каждый спрайт по отдельности, сделать это можно, например, следующим образом: sprite[0] = new Sprite(5, 3); sprite[1] = new Sprite(8, 2); sprite[2] = new Sprite(2, 2); sprite[3] = new Sprite(3, 1); sprite[4] = new Sprite(15, 2); После создания массива данных переходим к методу LoadGraphicsContent() и к загрузке в массив данных графических изображений. 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»); Названия для изображений я специально задал в виде цифровых значений, чтобы был понятен механизм загрузки графики для каждого элемента массива данных. Кстати, поскольку имена изображений совпадают с данными массива, то можно реорганизовать вышеприведенный код в цикл, но эти действия выполняйте самостоятельно – домашнее задание ;) Теперь перейдем к первичной установке всех спрайтов на свои игровые позиции, которые реализованы в методе Initialize(). protected override void Initialize() { j = 0; for (int i = 0; sprite.Length > i; i++) { sprite.spritePosition = new Vector2(rand.Next(10, screenWidth - 150), j = j - 300); } base.Initialize(); } Для инициализации используется цикл. В качестве координаты по оси X для каждого спрайта применяется механизм случайного выбора позиции по ширине игровой области экрана. Этот механизм мы подробно рассмотрели в предыдущем проекте. А вот установка точки отсчета по оси Y проходит в несколько другом ключе с использованием переменной j = j - 300. Здесь мы используем промежуточную переменную j, которая инициализируется нулевым значением. На каждой итерации цикла значение этой переменной уменьшается на 300 пикселей, что позволяет установить все спрайты как бы друг за другом на расстоянии 300 пикселей, и они не накладываются один на другой (рис. 8.4). Этот простой алгоритм позволяет нам избежать наложений спрайтов друг на друга в начальной стадии инициализации игры. Далее в методе MoveSprite() переделываем код для движения уже массива объектов, а не одного спрайта. public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite.spritePosition += sprite.speedSprite; if (sprite.spritePosition.Y > screenHeight) { sprite.spritePosition = new Vector2(rand.Next(50, screenWidth - sprite.spriteTexture.Width / 12 - 50), -500); } } }
Рис. 8.4. Установка спрайтов на игровые позиции в момент первого запуска игры
Обработка ситуации с выходом спрайта из зоны видимости реализована, как и в предыдущем проекте, но по оси Y задается значение в –500 пикселей. Делается это для того, чтобы уже исчезнувшие с экрана спрайты ставились на новые позиции повыше (или подальше) и дали возможность пройти всем предыдущим спрайтам, еще не достигнувшим конца экрана. Затем в методе Update() класса Game1 мы вызываем метод UpdateFrame() класса Sprite для обновления анимационной последовательности спрайтов и метод MoveSprite() для движения спрайтов по экрану. /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite.UpdateFrame(elapsed); } MoveSprite(); base.Update(gameTime); } Для обновления анимационной последовательности методом UpdateFrame() используется цикл. Точно такой же цикл мы применяем и в методе Draw() для рисования спрайтов на экране. Полный исходный код класса Game1 приведен в листинге 8.2 //========================================================================= /// /// Листинг 8.2 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 8 /// Проект: MoveSpriteArray /// Класс: Game1 /// Массив спрайтов /// //========================================================================= #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 MoveSpriteArray { 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; /// /// Конструктор /// 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); } } /// /// Инициализация /// i; i++) { sprite.spritePosition = new Vector2(rand.Next(10, screenWidth - 150), j = j - 300); } base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); background = content.Load(«Content\\Textures\\background»); 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»); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { … } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite.UpdateFrame(elapsed); } MoveSprite(); base.Update(gameTime); } /// /// Движение спрайтов по вертикали /// public void MoveSprite() { for (int i = 0; sprite.Length > i; i++) { sprite.spritePosition += sprite.speedSprite; if (sprite.spritePosition.Y > screenHeight) { sprite.spritePosition = new Vector2(rand.Next(50, screenWidth - sprite.spriteTexture.Width / 12 - 50), -500); } } } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) graphics.GraphicsDevice.Clear(Color.Black); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(background, new Vector2(0, 0), Color.White); for (int i = 0; sprite.Length > i; i++) { sprite.DrawAnimationSprite(spriteBatch); } spriteBatch.End(); base.Draw(gameTime); } } } Далее >>
533 Прочтений • [Движение спрайтов в пространстве [Xbox 360]] [19.05.2012] [Комментариев: 0]