В этой лабораторной работе мы поговорим о реализации физических законов в компьютерных играх.
Цель работы
Научиться добавлять в игры механизмы, имитирующие физические законы
Задачи работы
Создать шаблон платформенной игры
Немного теории
Строго говоря, наше знакомство с игровой физикой началось тогда, когда мы начали перемещать объекты по игровому полю, используя для их перемещения понятие скорости. В нашем случае это – скорость измерения координат. Столкновения объектов – это так же пример реализации физических законов, когда мы имитируем столкновение твердых тел в пространстве. Любые другие взаимодействия объектов так же являются примерами применения физических законов в компьютерных играх.
Для реализации более сложных взаимодействий требуется построение физической модели игрового объекта, которая учитывает значимые воздействия на этот объект.
Шаблон платформенной игры
Платформенные игры или платформеры получили такое название потому, что игрок управляет персонажем, который перемещается по площадкам – платформам. В ходе прохождения игры он переходит или перепрыгивает с платформы на платформу, собирает так называемые бонусы, сражается с компьютерными «врагами» и т.д. Действие подобных игр происходит в двумерной среде. Разработаем шаблон такой игры.
Для начала подумаем о том, как нам конструировать игровые экраны. Экран типичной платформенной игры состоит из элементов, которые по-разному взаимодействуют с объектом пользователя. Некоторые из них (стены) непроницаемы для объекта, некоторые (бонусы) при столкновении с объектом исчезают, а игроку начисляются очки или даются какие-то другие улучшения игрового персонажа. Объекты-лестницы позволяют перемещаться между отдельными «этажами» игрового экрана, которые недоступны при других способах перемещения, объекты-враги отнимают некоторое количество очков (или «жизней») у персонажа. Как правило, объекты, которыми заполнен экран, имеют определенный размер, обычно – одинаковый. То есть при их расстановке по экрану можно представить, что экран разбит на клетки, равные размерам объекта. Перемещение игрового объекта, контролируемого пользователем, совсем необязательно должно быть прерывистым, поклеточным, однако сам игровой экран удобнее всего конструировать именно таким способом.
Прежде чем начинать разработку, создадим несколько изображений, которые будем использовать в качестве графических образов игровых объектов. На рис. 5.1. вы можете видеть эти объекты.
Рис. 5.1. Изображения для визуализации игровых объектов и фона
Перечислим объекты, которые понадобятся нам для создания прототипа платформенной игры.
Объект игрока (me.png) – им управляет играющий.
Фон (background.png) – фоновое изображение.
Бонус №1 (bonus1.png) – бонус первого вида. Если объект игрока соприкасается с этим бонусом, игроку начисляется 100 очков.
Бонус №2 (bonus2.png) – бонус второго вида. Если объект игрока соприкасается с этим бонусом, игроку добавляется 1 «жизнь».
Враг (enemy.png) – объект «врага». Если игрок соприкасается с объектом врага – количество «жизней» уменьшается на 1. При количестве жизней меньше 0 игра заканчивается. Если игрок сможет прыгнуть и приземлиться на объект врага – враг будет уничтожен, а игрок получит 50 очков.
Лестница (ladder.png) – игрок может подниматься и опускаться по лестнице там, где другим способом ему не пройти.
Стена (wall.png) – стена. Стена непроницаема для объекта игрока. Объект игрока может подпрыгнуть только тогда, когда снизу находится стена.
Создадим новый стандартный объект, назовем его P5_1. Добавим в папку Content нужные ресурсы. Все объекты (кроме объекта игрока), которыми мы будем пользоваться, имеют размеры 64х64 пикселя, объект игрока имеет размер 32х32 пикселя. Для начала нам нужно разработать систему конструирования игровых уровней, в данном случае – систему конструирования игровых экранов. Мы пользуемся разрешением игрового окна 640х512 пикселей, то есть – 8х10 элементов размера 64х64 пикселя. Игровой экран разбит на 80 ячеек, в каждой из которых может быть один из игровых объектов.
Для хранения информации о содержимом каждой из этих ячеек используем двумерный массив. Каждая ячейка массива соответствует ячейке игрового экрана. Для того, чтобы получить координаты ячейки, соответствующие ячейке массива с индексом (i, j), достаточно умножить каждый из элементов индекса на 64. Верхней левой ячейке экрана соответствует элемент массива с индексом (0, 0), ячейке в правом нижнем углу – элемент (7,9).
Мы назвали этот массив Layer, в листинге 5.1. приведен код его инициализации.
Для удобства мы расположили код инициализации массива с переходом на новую строки для каждой из его строк. В результате этот код отражает состояние игрового экрана в момент начала игры. Как видите, в ячейках массива расположены целые числа. Каждое из этих чисел символизирует определенный вид объекта, который следует поместить в позицию на экране, соответствующую координатам ячейки.
В табл. 5.1. приведено соответствие чисел, используемых в таблице-конструкторе уровня и объектов.
Таблица 5.1. Номера объектов в массиве Layer
Номер
Название объекта
0
Объект отсутствует
1
Стена
2
Лестница
3
Бонус №1
4
Бонус №2
5
Враг
6
Игрок
На рис. 5.2. приведено изображение игрового экрана, сконструированного в соответствии с таблицей и содержимым массива.
Рис. 5.2. Игровой экран, сконструированный в соответствии с массивом Layer
На рис. 5.3. приведено окно проекта.
Рис. 5.3. Окно проекта
В табл. 5.2. приведено описание классов, которые мы используем в данном проекте.
Таблица 5.2. Классы проекта
Имя файла
Описание
Game1.cs
Класс стандартного проекта
gBaseClass.cs
Базовый класс – родитель классов игровых объектов. Он содержит процедуры визуализации объектов и их расстановки по игровому полю. Этот класс наследует класс DrawableGameComponent
Bonus1.cs
Класс для создания объектов бонусов вида №1
Bonus2.cs
Класс для создания объектов бонусов вида №2
Enemy.cs
Класс для создания объектов-врагов
Ladder.cs
Класс для создания лестниц
Me.cs
Класс для создания объекта игрока. Именно он содержит основную часть игровой логики, в том числе – реализацию игровой физики для объекта-игрока
Основная задача этого компонента – перевести координаты объекта, выраженные в индексе массива в координаты объекта на игровом экране и вывести объект на экран. Это достигается умножением индексов на 64. Так же здесь объявляются переменные для хранения координат объекта, информации о прямоугольнике, ограничивающем объект и о текстуре объекта.
Классы Bonus1, Bonus2, Ladder, Wall, Enemy полностью наследуют код класса gBaseComponent, фактически, являясь его точными копиями. Можно было бы отказаться от разработки этих классов, создав соответствующие объекты путем создания объекта класса gBaseComponent. Однако, подход с созданием отдельного класса для каждого компонента предпочтительнее по нескольким причинам. Во-первых – наличие самостоятельных классов для отдельных компонентов позволяет удобно осуществлять различные проверки при работе с игровым объектом. Во-вторых – если понадобится расширить функциональность одного из объектов (например, перемещать объект Enemy и т.д.) – отдельные классы готовы для модификаций. В листинге 5.4. приведен код класса Bonus1. Остальные классы, как уже было сказано, идентичны ему.
usingSystem;usingSystem.Collections.Generic;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Audio;usingMicrosoft.Xna.Framework.Graphics;usingMicrosoft.Xna.Framework.Input;usingMicrosoft.Xna.Framework.Content;namespace P5_1.GameObj{/// /// This is a game component that implements IUpdateable./// publicclass Me : gBaseClass{//Прямоугольник, представляющий игровое окно Rectangle scrBounds;//Скорость, с которой будет перемещаться спрайтfloat sprSpeed=2;//"Сила тяжести"float sprGravity = 0.4f;//"Ускорение свободного падения"float sprAcceleration = 0.03f;//Скорость при паденииfloat sprGrAcc =0;//Скорость, с которой объект будет подпрыгиватьfloat sprJump =70;//Переменная для хранения количества "жизней" объектаint sprLives =2;//Переменная для хранения набранных очковint sprScores =0;public Me(Game game, ref Texture2D _sprTexture, Vector2 _sprPosition, Rectangle _sprRectangle):base(game, ref _sprTexture, _sprPosition, _sprRectangle){ scrBounds =new Rectangle(0, 0, game.Window.ClientBounds.Width, game.Window.ClientBounds.Height);}/// /// Allows the game component to perform any initialization it needs to before starting/// to run. This is where it can query for any required services and load content./// publicoverridevoid Initialize(){base.Initialize();}//Проверка на столкновение с границами экранаvoid Check(){if(sprPosition.X< scrBounds.Left){ sprPosition.X= scrBounds.Left;}if(sprPosition.X> scrBounds.Width- sprRectangle.Width){ sprPosition.X= scrBounds.Width- sprRectangle.Width;}if(sprPosition.Y< scrBounds.Top){ sprPosition.Y= scrBounds.Top;}if(sprPosition.Y> scrBounds.Height- sprRectangle.Height){ sprPosition.Y= scrBounds.Height- sprRectangle.Height;}}//Процедуры, которые используются для перемещения объекта в одном из указанных направленийvoid MoveUp(float speed){this.sprPosition.Y-= speed;}void MoveDown(float speed){this.sprPosition.Y+= speed;}void MoveLeft(float speed){this.sprPosition.X-= speed;}void MoveRight(float speed){this.sprPosition.X+= speed;}//Функция, которая проверяет, находится ли непосредственно под нашим объектом//объект типа Wall - то есть стена. Наш алгоритм обработки столкновений удерживает//объект на небольшом расстоянии от стены, не давая ему пройти сквозь нее.//Поэтому при проверке к координате Y объекта добавляется 1. Эта функция используется//при проверке возможности совершения объектом прыжка - он может подпрыгнуть//только в том случае, если по ним есть стена.bool IsWallIsInTheBottom(){int Collision =0;foreach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Wall))){if(this.sprPosition.X+this.sprRectangle.Width> spr.sprPosition.X&&this.sprPosition.X< spr.sprPosition.X+ spr.sprRectangle.Width&&this.sprPosition.Y+1+this.sprRectangle.Height+1> spr.sprPosition.Y&&this.sprPosition.Y+1< spr.sprPosition.Y+ spr.sprRectangle.Height) Collision++;}}if(Collision >0)return true;elsereturn false;}//Функция используется как вспомогательная//Она проверяет, сталкивается ли наш объект с объектом//класса gBaseClass и возвращает True если столкновение естьbool IsCollideWithObject(gBaseClass spr){return(this.sprPosition.X+this.sprRectangle.Width> spr.sprPosition.X&&this.sprPosition.X< spr.sprPosition.X+ spr.sprRectangle.Width&&this.sprPosition.Y+this.sprRectangle.Height> spr.sprPosition.Y&&this.sprPosition.Y< spr.sprPosition.Y+ spr.sprRectangle.Height);}//Функция проверяет, находится ли объект в пределах лестницы//Если объект находится на лестнице - его поведение меняется//Он может взбираться и спускаться по лестнице, но не может подпрыгиватьbool IsCollideWithLadder(){foreach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Ladder))){if(this.sprPosition.X+this.sprRectangle.Width+1> spr.sprPosition.X&&this.sprPosition.X+1< spr.sprPosition.X+ spr.sprRectangle.Width&&this.sprPosition.Y+this.sprRectangle.Height+1> spr.sprPosition.Y&&this.sprPosition.Y+1< spr.sprPosition.Y+ spr.sprRectangle.Height)return true;}}return false;}//Процедура, отвечающая за перемещение игрового объектаvoid Move(){ KeyboardState kbState = Keyboard.GetState();//При нажатии кнопки "Вверх"if(kbState.IsKeyDown(Keys.Up)){//Если под объектом находится стена//и он не соприкасается с лестницей//Объект подпрыгиваетif(IsWallIsInTheBottom()==true&& IsCollideWithLadder ()==false){ MoveUp(sprJump);//При прыжке проводится проверка на контакт со стеной//Которая может быть расположена над объектом//при необходимости его координаты корректируютсяwhile(IsCollideWithWall()){ MoveDown((sprSpeed /10));}}//Если объект находится на лестнице//Он перемещается вверх с обычной скоростью без прыжковif(IsCollideWithLadder()==true){ MoveUp(sprSpeed);while(IsCollideWithWall()){ MoveDown((sprSpeed /10));}}}//При нажатии кнопки "Вниз"//Происходит обычная процедура перемещения объекта с проверкойif(kbState.IsKeyDown(Keys.Down)){ MoveDown(sprSpeed);while(IsCollideWithWall()){ MoveUp((sprSpeed /10));}}//Точно так же обрабатывается нажатие кнопки "Влево"if(kbState.IsKeyDown(Keys.Left)){ MoveLeft(sprSpeed);while(IsCollideWithWall()){ MoveRight((sprSpeed /10));}}//Аналогично - перемещение вправоif(kbState.IsKeyDown(Keys.Right)){ MoveRight(sprSpeed);while(IsCollideWithWall()){ MoveLeft((sprSpeed /10));}}}//Проверяем столкновение объекта со стеной//Результат проверки нужен для обработки перемещений bool IsCollideWithWall(){foreach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Wall))){if(IsCollideWithObject(spr))return true;}}returnfalse;}//В этой процедуре мы проверяем столкновения с объектами-бонусами//и объектами-врагами, причем, наш объект может уничтожать врагов,//прыгая на них сверху. С точки зрения данной процедуры такой прыжок//ничем не отличается от обычного контакта с объектом-врагом и приводит к//проигрышу. Поэтому проверка на прыжок нашего объекта на вражеский объект//вынесена в отдельную процедуруvoid IsCollideWithAny(){//Заводим переменную для временного хранения//ссылки на объект, с которым столкнулся игровой объект gBaseClass FindObj = null;//Проверка на столкновение с объектами Bonus1foreach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Bonus1))){if(IsCollideWithObject(spr)){ FindObj = spr;}}}//Если было столкновение - уничтожаем объект, с которым было столкновение//и увеличиваем число очковif(FindObj !=null){ sprScores +=100; FindObj.Dispose(); FindObj = null;}//Проверка на столкновение с объектами Bonus2foreach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Bonus2))){if(IsCollideWithObject(spr)){ FindObj = spr;}}}//Если столкновение было уничтожим объект//Увеличим число "жизней"if(FindObj !=null){ sprLives +=1; FindObj.Dispose(); FindObj = null;}//Проверка на столкновение с объектами Enemyforeach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Enemy))){if(IsCollideWithObject(spr)){ FindObj = spr;}}}if(FindObj !=null){ FindObj.Dispose(); sprLives--;}}//Этот метод реализует игровую "силу тяжести"//Объект, который находится в свободном пространстве падает внизvoid GoToDown(){// перемещается вниз//при перемещении проверяется столкновение объекта со стенойif(IsCollideWithLadder()==false){if(sprGrAcc ==0) sprGrAcc = sprAcceleration; MoveDown(sprGrAcc);while(IsCollideWithWall()){ MoveUp((sprSpeed /10));} sprGrAcc += sprAcceleration;if(IsWallIsInTheBottom()) sprGrAcc =0;if(IsCollideWithLadder()) sprGrAcc =0;}}//Метод, проверяющий, не "прыгнул" ли игровой объект на "голову" объекту-врагу//Обратите внимание на то, что процедура проверки условия на первый взгляд кажется похожей//на обычные проверки, однако таковой не являетсяvoid IsKillEnemy(){ gBaseClass FindObj = null;foreach(gBaseClass spr in Game.Components){if(spr.GetType()==(typeof(Enemy))){//Если игровой объект находится в пределах координат X объекта-врага//Если при этом игровой объект находится выше, чем объект-враг//менее чем на 35 пикселей (если считать по верхней стороне прямоугольника//описанного около объектов - это значит, что игровой объект//"прыгнул" на объект-врага и уничтожил его.if(this.sprPosition.X+this.sprRectangle.Width> spr.sprPosition.X&&this.sprPosition.X< spr.sprPosition.X+ spr.sprRectangle.Width&&this.sprPosition.Y+this.sprRectangle.Height< spr.sprPosition.Y&&this.sprPosition.Y< spr.sprPosition.Y+ spr.sprRectangle.Height&&(spr.sprPosition .Y-this.sprPosition .Y)<35){//Если условие выполняется - сохраним ссылку на объект врага FindObj = spr;}}}//Если был удачный прыжок на врага//добавим игроку очков и уничтожим объект класса Enemyif(FindObj !=null){ sprScores +=50; FindObj.Dispose();}}/// /// Allows the game component to update itself./// /// Provides a snapshot of timing values.publicoverridevoid Update(GameTime gameTime){//Вызовем процедуру перемещения объекта по клавиатурным командам Move();//Проверим, не вышел ли он за границы экрана, если надо исправим его позицию Check();//Применим к объекту "силу тяжести" GoToDown();//Проверим, не прыгнул ли наш объект на объект класса Enemy//Вызов этого метода предшествует вызову метода IsCollideWithAny()//Иначе прыжок на врага с целью уничтожить его может обернуться//Печальными последствиями для нашего объекта IsKillEnemy();//Проверим на другие столкновения IsCollideWithAny(); Game.Window.Title="У вашего персонажа "+ sprLives.ToString()+" жизни(ей) и "+ sprScores +" очков";if(sprLives <0){ Game.Window.Title="Вы проиграли";this.Dispose();}base.Update(gameTime);}}}
Назначение свойств и методов этого класса подробно описано внутри кода. Отметим ключевые методы, которые реализуют игровую физику.
Метод GoToDown() перемещает объект вниз до тех пор, пока он не коснется объекта-стены. Если объект «стоит» на стене (метод IsWallIsInTheBottom()) – он может подпрыгнуть. Если объект касается лестницы (IsCollideWithLadder()) – «сила тяжести» на него не действует, он может перемещаться по лестнице в любом направлении, однако не может подпрыгивать. Если объект сталкивается с бонусом №1 (IsCollideWithAny())– ему начисляются очки, если с бонусом №2 – жизни. Если объект столкнулся с врагом – одна жизнь теряется, если объект «прыгнул» на врага (IsKillEnemy()) – объект врага уничтожается, игроку начисляются очки.
Задание
Разработайте собственные спрайты для визуализации объектов.
Реализуйте самостоятельно игру, которая описана выше.
Доработайте объект Enemy таким образом, чтобы он мог автоматически (по параметрам, переданным при его конструировании) перемещаться по экрану в горизонтальной или вертикальной плоскости, отталкиваясь от других объектов (кроме лестницы).