Игровые столкновения В играх объекты могут пересекаться друг с другом, или сталкиваться между собой. Такой вид пересечения объектов называется игровым столкновением. Для определения столкновения объектов в игре программисты пишут исходный код, который создает своего рода детектор столкновений. Если такое столкновение имеет место, то в игре необходимо производить определенные действия. Обработка ситуации при столкновении двух и более объектов между собой в играх может быть различной. В одних играх при столкновении вам придется уничтожать объекты, в других необходимо будет оттолкнуться от этого объекта, в третьих захватить этот объект и т. д., здесь все зависит от логики игры. В нашей игре все объекты падают с неба (сверху вниз). В нижней части дисплея курсирует ковер-самолет, который обязан ловить людей и уворачиваться от других падающих объектов. В связи с этим нам нужно реализовать механизм, который должен определять, коснулся один из объектов ковра-самолета или нет. После касания объекта и ковра-самолета мы будем устанавливать объект на новую позицию, за верхней кромкой экрана, и перемещать его опять вниз. На первый взгляд, проблема сложна, но на самом деле все решается достаточно легко и просто, в чем вы сейчас и убедитесь.
10.1. Структура BoundingBox В XNA Framework детектором столкновений между объектами может послужить структура BoundingBox. Дословный перевод этой структуры звучит как ограничивающий прямоугольник. Идея определения столкновений структурой BoundingBox состоит в следующем. В игре создается структурная переменная структуры BoundingBox. Структура BoundingBox в своей сущности описывает невидимый прямоугольник, а значит, структурная переменная представляет этот самый прямоугольник. Размер прямоугольника вы определяете сами с помощью элементов структуры. Далее вы надеваете этот прямоугольник (ограничивающий прямоугольник) на определенный спрайт, максимально подгоняя размер прямоугольника под размер спрайта. То есть фактически размер ограничивающего прямоугольника, представленный переменной структуры BoundingBox, должен соответствовать размеру одного фрейма изображения спрайта. Такую операцию необходимо проделать с каждым спрайтом, участвующим в обработке событий по столкновению объектов. Затем встроенными механизмами структуры BoundingBox вы определяете, пересекаются между собой ограничивающие прямоугольники, надетые на спрайты, или нет. Если пересеклись, то объекты столкнулись, а значит, необходимо выполнять определенные события, если нет, то столкновение не имело места и ничего делать не нужно. Просто, не правда ли? Создать переменную структуры BoundingBox очень просто, как и просто определить размер ограничивающего прямоугольника, смотрим на приведенный пример блока кода.
// Создаем структурную переменную public BoundingBox bb; // Задаем размер прямоугольника bb.Min = new Vector3(10, 20, 0); bb.Max = new Vector3(60, 80, 0);
Структурные переменные Min и Max определяют две точки в пространстве, на основе которых происходит построение ограничивающего прямоугольника. Точка Min задает левый верхний угол ограничивающего прямоугольника, что соответствует левому верхнему углу изображения. В свою очередь, точка Max определяет правый нижний угол прямоугольника, что уже соответствует правому нижнему углу изображения. На основе этих данных сервисами XNA в пространстве строится прямоугольник. Если этот прямоугольник подогнать к размеру спрайта, то мы получаем тот самый ограничивающий прямоугольник, представленный структурой BoundingBox (рис. 10.1).
Рис. 10.1. Создание ограничивающего прямоугольника
Чем плотнее вы наденете ограничивающий прямоугольник на спрайт, тем точнее будет обрабатываться столкновение между объектами. Дополнительно можно искусственно уменьшать или увеличивать ограничивающий прямоугольник, чтобы соответственно уменьшить или увеличить зону столкновения. Здесь все зависит от игровых задач. Например, прямоугольник для ковра-самолета мы специально уменьшим, чтобы столкновения были реалистичными. Единственное, еще о чем следует досказать, – это то, что элементы Min и Max структуры BoundingBox задаются трехмерным вектором, состоящим из трех координат осей X, Y и Z.
Vector3(10, 20, 0);
В двухмерном пространстве ось Z присутствует, но она работает несколько иначе, чем в трехмерных играх. Поэтому для простых двухмерных спрайтов третий параметр вектора ставится всегда в ноль и не используется. В трехмерных играх ось Z используется, а в векторе обязательно задается значение по оси Z. Тогда мы получим уже не ограничивающий прямоугольник, а ограничивающий куб (все модели объемные).
10.2. Проект Collision Итак, приступим к работе над проектом Collision, в котором мы добавляем обработку столкновений между ковром-самолетом и падающими объектами. В листинге 10.1 представлен полный программный код класса Game1. Сначала давайте посмотрим на весь исходный код этого класса, а затем перейдем к его подробному анализу. Остальные классы проекта Collision остаются неизменными, как и в предыдущих проектах игры.
//========================================================================= /// /// Листинг 10.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 10 /// Проект: Collision /// Класс: 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 Collision { 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 pauseKeyDown = false; public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5]; /// /// Конструктор /// public Game1() { … } /// /// Инициализация /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { … } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { … } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit(); Pause(); if (paused == false) { double elapsed = gameTime.ElapsedGameTime.TotalSeconds; for (int i = 0; sprite.Length > i; i++) { sprite.UpdateFrame(elapsed); } MoveSprite(); MovePlatform(); Collisions(); } base.Update(gameTime); } /// /// Движение спрайта по вертикали /// public void MoveSprite() { … } /// /// Движение ковра-самолета по экрану /// public void MovePlatform() { … } /// /// Пауза в игре /// public void Pause() { … } /// /// Столкновения /// public void Collisions() { bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 45, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 45 + platform.spriteTexture.Height, 0); for (int i = 0; bb.Length > i; i++) { bb.Min = new Vector3(sprite.spritePosition.X, sprite.spritePosition.Y, 0); bb.Max = new Vector3(sprite.spritePosition.X + sprite.spriteTexture.Width / 12, sprite.spritePosition.Y + sprite.spriteTexture.Height, 0); } if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[0].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[1].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[2].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[3].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[4].spriteTexture.Width / 12 - 50), -500); } } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { … }
Первое, что необходимо сделать в исходном коде класса Game1, – это создать переменные структуры BoundingBox.
public BoundingBox bbplatform; public BoundingBox[] bb = new BoundingBox[5];
В первой строке этого кода создается переменная bbplatform структуры BoundingBox, отвечающая за создание ограничивающего прямоугольника вокруг ковра-самолета. Во второй строке блока исходного кода происходит создание массива переменных структуры BoundingBox, состоящей из пяти элементов. Как вы уже догадались, эти пять ограничивающих прямоугольников, или пять структурных переменных bb[0]-bb[4], назначены для имеющихся в игре пяти спрайтов. После создания переменных структуры BoundingBox необходимо определить размеры ограничивающих прямоугольников для каждого спрайта игры. То есть нужно наложить один конкретный ограничивающий прямоугольник на один конкретный спрайт, четко подогнав его под прямоугольный размер самого спрайта. Более того, необходимо также неотступно следовать за спрайтом, а значит, перемещать ограничивающий прямоугольник в соответствии с изменившимися координатами спрайта, поскольку у нас все спрайты динамичны и двигаются по экрану. Для этих целей создается отдельный метод Collisions(), который мы будем вызывать непосредственно в игровом цикле. Метод большой и несколько запутанный, поэтому я сейчас его прокомментирую и дополнительно разверну некоторые строки исходного кода с использованием простого языка. Смотрим, что получилось.
public void Collisions() { /* Первая часть */ // Создаем ограничивающий прямоугольник для платформы bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 45, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 45 + platform.spriteTexture.Height, 0); // Создаем ограничивающий прямоугольник для sprite[0] bb[0].Min = new Vector3(sprite[0].spritePosition.X, sprite[0].spritePosition.Y, 0); bb[0].Max = new Vector3(sprite[0].spritePosition.X + ширина одного фрейма, sprite[0].spritePosition.Y + высота одного фрейма, 0); // Создаем ограничивающий прямоугольник для sprite[1] bb[1].Min = new Vector3(sprite[1].spritePosition.X, sprite[1].spritePosition.Y, 0); bb[1].Max = new Vector3(sprite[1].spritePosition.X + ширина одного фрейма, sprite[1].spritePosition.Y + высота одного фрейма, 0); // Создаем ограничивающий прямоугольник для sprite[2] bb[2].Min = new Vector3(sprite[2].spritePosition.X, sprite[2].spritePosition.Y, 0); bb[2].Max = new Vector3(sprite[2].spritePosition.X + ширина одного фрейма, sprite[2].spritePosition.Y + высота одного фрейма, 0); // Создаем ограничивающий прямоугольник для sprite[3] bb[3].Min = new Vector3(sprite[3].spritePosition.X, sprite[3].spritePosition.Y, 0); bb[3].Max = new Vector3(sprite[3].spritePosition.X + ширина одного фрейма, sprite[3].spritePosition.Y + высота одного фрейма, 0); // Создаем ограничивающий прямоугольник для sprite[4] bb[4].Min = new Vector3(sprite[4].spritePosition.X, sprite[4].spritePosition.Y, 0); bb[4].Max = new Vector3(sprite[4].spritePosition.X + ширина одного фрейма, sprite[4].spritePosition.Y + высота одного фрейма, 0); /* Вторая часть */ // Проверяем пересечение всех назначенных прямоугольников if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[0].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[1])) { sprite[1].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[1].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[2])) { sprite[2].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[2].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[3])) { sprite[3].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[3].spriteTexture.Width / 12 - 50), -500); } if (bbplatform.Intersects(bb[4])) { sprite[4].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[4].spriteTexture.Width / 12 - 50), -500); } }
Исходный код метода Collisions() делится на два блока, или две разные по своим функциям части. В первой части мы для каждого объекта (коверамолет и пять спрайтов) создаем ограничивающие прямоугольники. Каждый прямоугольник соответствует размеру конкретно взятого спрайта, возьмем, например, ковер-самолет и переменную Min.
bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 45, 0);
В этих строках мы задаем начальную точку отсчета для платформы, что соответствует у нас левому верхнему углу изображения платформы, а затем помещаем это значение в переменную Min. Дополнительно прибавляются еще 45 пикселей к значению по оси Y, чтобы уменьшить ограничивающий прямоугольник, а точнее опустить границу столкновения с ковром. Сделано это потому, что ковер-самолет в своей центральной части имеет углубление, и чтобы столкновение было реалистичным, мы уменьшаем прямоугольник (рис. 10.2).
Рис. 10.2. Ограничивающий прямоугольник для ковра/самолета
Затем необходимо дорисовать оставшуюся часть ограничивающего прямоугольника, и для этого достаточно задать координату для нижнего правого угла прямоугольника и сохранить ее значение в переменной Max.
Вторая координата ограничивающего прямоугольника соответствует правому нижнему углу изображения ковра. Все оставшиеся стороны прямоугольника дорисовываются автоматически на основе этих двух точек. В итоге получается, что мы надели на спрайт ограничивающий прямоугольник размером в сам спрайт, а поскольку метод Collisions() постоянно вызывается в итерациях игрового цикла, то и координаты прямоугольника будут постоянно изменяться и соответствовать текущему положению платформы на экране. Такой подход позволяет нам не только создавать ограничивающие прямоугольники, но и отслеживать положение спрайтов на экране телевизора. Создание ограничивающих прямоугольников для падающих объектов проиходит аналогичным образом, но уже применительно к каждому элементу массива данных.
// Создаем ограничивающий прямоугольник для sprite[0] bb[0].Min = new Vector3(sprite[0].spritePosition.X, sprite[0].spritePosition.Y, 0); bb[0].Max = new Vector3(sprite[0].spritePosition.X + ширина одного фрейма, sprite[0].spritePosition.Y + высота одного фрейма, 0);
При задании значений для переменных Max каждого из прямоугольников спрайта используются не числовые значения, а размер текстуры. Единственное, что нужно помнить, – это то, что при получении ширины изображения вам будет выдан весь фактический размер анимационной последовательности спрайта (все 12 фреймов), поэтому полученное значение необходимо поделить на количество имеющихся фреймов анимации. Вторая часть метода Collisions() направлена на определение столкновений между ковром-самолетом и каждым спрайтом, падающим с неба. Для этого используется метод Intersects() структуры BoundingBox. В переводе название этого метода обозначает пересечение. То есть этот метод определяет, пересеклись между собой прямоугольники или нет, что равносильно определению столкновения между двумя спрайтами.
if (bbplatform.Intersects(bb[0])) { sprite[0].spritePosition = new Vector2(rand.Next(50, screenWidth - sprite[0].spriteTexture.Width / 12 - 50), -500); }
Дословно на русском языке следующая строка исходного кода if(bbplatform. Intersects(bb[0])) обозначает следующее: если ограничивающий прямоугольник платформы будет пересекаться с ограничивающим прямоугольником спрайта под номером ноль, то необходимо выполнить заданные действия. В качестве заданных действий в столкновении мы определяем, что нужно убрать прямоугольник с экрана и поставить его на новую игровую позицию в верхней части дисплея. После чего метод MoveSprite() заставит перемещаться этот спрайт в направлении сверху вниз или падать с неба. Если столкновение или пересечение ограничивающих прямоугольников не происходит, то спрайт уходит за пределы экрана и затем опять устанавливается на новую игровую позицию. В приведенном выше исходном коде для каждого спрайта происходит вызов метода Intersects() и соответственно идет слежение за пересечением всех прямоугольников между собой. Но поскольку мы имеем дело с массивом данных, то весь этот исходный код можно поместить в цикл, как первую часть, так и вторую часть метода Collisions(). В итоге мы значительно уменьшим и упростим исходный код данного метода. Помещаем все в цикл и смотрим, что получилось.
public void Collisions() { /* Первая часть */ // Создаем ограничивающий прямоугольник для ковра bbplatform.Min = new Vector3(platform.spritePosition.X, platform.spritePosition.Y + 45, 0); bbplatform.Max = new Vector3(platform.spritePosition.X + platform.spriteTexture.Width, platform.spritePosition.Y + 45 + platform.spriteTexture.Height, 0); // Создаем ограничивающие прямоугольники для спрайтов for (int i = 0; bb.Length > i; i++) { bb.Min = new Vector3(sprite.spritePosition.X, sprite.spritePosition.Y, 0); bb.Max = new Vector3(sprite.spritePosition.X + sprite.spriteTexture.Width / 12, sprite.spritePosition.Y+ sprite.spriteTexture.Height, 0); } /* Вторая часть */ // Проверяем столкновения между ковром и спрайтами for (int i = 0; bb.Length > i; i++) { if (bbplatform.Intersects(bb)) { sprite.spritePosition = new Vector2(rand.Next(50, screenWidth - sprite.spriteTexture.Width/12 -50), -500); } } }
И напоследок один небольшой секрет. Когда вы работаете с циклом for, то, как правило, вызов этого цикла происходит следующим образом:
for(int i = 0; 5 > i; i++) {…}
Но есть и другой, более интересный способ создания цикла for:
for(int i = 5; —i >= 0;) {…}
Главным в этой записи является то, что такая формулировка цикла позволяет работать циклу примерно на 10–13% быстрее, чем его стандартная запись. В компьютерных играх (мощные системные ресурсы) это, конечно, не столь актуально, но вот в мобильных играх я постоянно использую только такую запись. Теперь вы можете запустить рассматриваемый в этой главе проект и с помощью ковра-самолета ловить падающие с неба объекты. Единственное, что еще можно несколько видоизменить в коде проекта, – так это уменьшить ограничивающий прямоугольник для ковра. В частности, сделать его по высоте не в размер всей текстуры, а, скажем, всего в пару пикселей (рис. 10.3). Полный размер высоты ковра-самолета кажется несколько большим, и иногда достаточно коснуться падающего объекта боком – и считается, что вы его поймали. Если уменьшить площадь ограничивающего прямоугольника, а именно его боковую часть, то игровой процесс станет более реалистичным.