Цель работы: Ознакомиться с
Наше знакомство с игровой физикой началось тогда, когда мы начали перемещать объекты по игровому полю, используя для их перемещения понятие скорости. В нашем случае это – скорость измерения координат. Столкновения объектов – это так же пример реализации физических законов, когда мы имитируем столкновение твердых тел в пространстве. Любые другие взаимодействия объектов так же являются примерами применения физических законов в компьютерных играх.
Для реализации более сложных взаимодействий требуется построение
Платформенные игры или платформеры получили такое название потому, что игрок управляет персонажем, который перемещается по площадкам – платформам. В ходе прохождения игры он переходит или перепрыгивает с платформы на платформу, собирает так называемые бонусы, сражается с компьютерными "врагами" и т.д. Действие подобных игр происходит в двумерной среде. Разработаем шаблон такой игры.
Для начала подумаем о том, как нам конструировать игровые экраны. Экран типичной платформенной игры состоит из элементов, которые по-разному взаимодействуют с объектом пользователя. Некоторые из них (стены) непроницаемы для объекта, некоторые (бонусы) при столкновении с объектом исчезают, а игроку начисляются очки или даются какие-то другие улучшения игрового персонажа.
Объекты-лестницы позволяют перемещаться между отдельными "этажами" игрового экрана, которые недоступны при других способах перемещения, объекты-враги отнимают некоторое количество очков (или "жизней") у персонажа. Как правило, объекты, которыми заполнен экран, имеют определенный размер, обычно – одинаковый.
То есть при их расстановке по экрану можно представить, что экран разбит на клетки, равные размерам объекта. Перемещение игрового объекта, контролируемого пользователем, совсем необязательно должно быть прерывистым, поклеточным, однако сам игровой экран удобнее всего конструировать именно таким способом.
Прежде чем начинать разработку, создадим несколько изображений, которые будем использовать в качестве графических образов игровых объектов. На рис. 16.1 вы можете видеть эти объекты.
(рис 16.1) Изображения для визуализации игровых объектов
Перечислим объекты, которые понадобятся нам для создания прототипа платформенной игры.
Кроме того, мы, для организации управления игрой, используем изображение стрелок (рис. 16.2), аналогичное тому, которое использовали в ранее рассмотренных проектах. Разница заключается лишь в том, что это изображение выполнено полупрозрачным – сквозь него можно видеть ту часть игрового поля, над которой оно расположено.
(рис 16.2) Изображение стрелок, используемых для управления игрой
Создадим новый стандартный игровой проект XNA, назовем его P10_1. Добавим в проект Content нужные ресурсы. Все объекты (кроме объекта игрока), которыми мы будем пользоваться, имеют размеры 64х64 пикселя, объект игрока имеет размер 32х32 пикселя. Для начала нам нужно разработать систему конструирования игровых уровней, в данном случае – систему конструирования игровых экранов. Мы пользуемся разрешением игрового окна 768х448 пикселей, то есть – 7х12 элементов размера 64х64 пикселя. Игровой экран разбит на 84 ячейки, в каждой из которых может быть один из игровых объектов.
Для хранения информации о содержимом каждой из этих ячеек используем
Мы назвали этот массив Layer, в листинге 16.1. приведен код его инициализации.
Layer = new int[7, 12] {
{ 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 5, 1, 3, 5, 1, 1, 2, 1, 1, 1, 5 },
{ 0, 0, 0, 1, 2, 1, 1, 2, 0, 0, 0, 4 },
{ 0, 1, 3, 0, 2, 0, 0, 2, 0, 0, 0, 3 },
{ 0, 5, 0, 0, 1, 1, 1, 2, 0, 1, 0, 1 },
{ 6, 0, 1, 3, 4, 0, 5, 2, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
Для удобства мы расположили код инициализации массива с переходом на новую строку для каждой из его строк. В результате этот код отражает состояние игрового экрана в момент начала игры. Как видите, в ячейках массива расположены целые числа. Каждое из этих чисел символизирует определенный вид объекта, который следует поместить в позицию на экране, соответствующую координатам ячейки.
В табл. 16.1 приведено соответствие чисел, используемых в таблице-конструкторе уровня и объектов.
| Номер | Название объекта |
| 0 | Объект отсутствует |
| 1 | Стена |
| 2 | Лестница |
| 3 | Бонус №1 |
| 4 | Бонус №2 |
| 5 | Враг |
| 6 | Игрок |
(рис 16.3) Игровой экран проекта
Кроме того, на экране можно видеть экранные элементы управления объектом и сообщение для пользователя.
На рис. 16.4 вы можете видеть окно Обозреватель решений нашего проекта.
(рис 16.4) Обозреватель решений, проект P10_1
Втабл. 16.2 приведено описание классов, которые мы используем в данном проекте.
| Имя файла | Описание |
| Game1.cs | Класс стандартного проекта |
| gBaseClass.cs | Базовый класс – родитель классов игровых объектов. Он содержит процедуры визуализации объектов и их расстановки по игровому полю. Этот класс наследует класс DrawableGameComponent |
| Bonus1.cs | Класс для создания объектов бонусов вида №1 |
| Bonus2.cs | Класс для создания объектов бонусов вида №2 |
| Enemy.cs | Класс для создания объектов-врагов |
| Ladder.cs | Класс для создания лестниц |
| Me.cs | Класс для создания объекта игрока. Именно он содержит основную часть игровой логики, в том числе – реализацию игровой физики для объекта-игрока |
| Wall.cs | Класс для создания стен |
В листинге 16.2 приведен код класса Game1.
>using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
namespace P10_1
{
/// <summary>
/// Это главный тип игры
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D txtBackground;
Texture2D txtBonus1;
Texture2D txtBonus2;
Texture2D txtEnemy;
Texture2D txtLadder;
Texture2D txtMe;
Texture2D txtWall;
Texture2D txtArrows;
SpriteFont myFont;
//Массив для конструирования уровня
public int[,] Layer;
Rectangle recBackround = new Rectangle(16, 0, 768, 448);
Rectangle recSprite = new Rectangle(0, 0, 64, 64);
Rectangle recArrows = new Rectangle(500, 280, 300, 200);
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.IsFullScreen = true;
// Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
TargetElapsedTime = TimeSpan.FromTicks(333333);
// Дополнительный заряд аккумулятора заблокирован.
InactiveSleepTime = TimeSpan.FromSeconds(1);
}
/// <summary>
/// Позволяет игре выполнить инициализацию, необходимую перед запуском.
/// Здесь можно запросить нужные службы и загрузить неграфический
/// контент. Вызов base.Initialize приведет к перебору всех компонентов и
/// их инициализации.
/// </summary>
protected override void Initialize()
{
Layer = new int[7, 12] {
{ 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 5, 1, 3, 5, 1, 1, 2, 1, 1, 1, 5 },
{ 0, 0, 0, 1, 2, 1, 1, 2, 0, 0, 0, 4 },
{ 0, 1, 3, 0, 2, 0, 0, 2, 0, 0, 0, 3 },
{ 0, 5, 0, 0, 1, 1, 1, 2, 0, 1, 0, 1 },
{ 6, 0, 1, 3, 4, 0, 5, 2, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
base.Initialize();
}
/// <summary>
/// LoadContent будет вызываться в игре один раз; здесь загружается
/// весь контент.
/// </summary>
protected override void LoadContent()
{
// Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
spriteBatch = new SpriteBatch(GraphicsDevice);
Services.AddService(typeof(SpriteBatch), spriteBatch);
txtBackground = Content.Load<Texture2D>("background");
txtBonus2 = Content.Load<Texture2D>("bonus2");
txtBonus1 = Content.Load<Texture2D>("bonus1");
txtEnemy = Content.Load<Texture2D>("enemy");
txtLadder = Content.Load<Texture2D>("ladder");
txtMe = Content.Load<Texture2D>("me");
txtWall = Content.Load<Texture2D>("wall");
txtArrows = Content.Load<Texture2D>("arrows");
myFont = Content.Load<SpriteFont>("MyFont");
//Вызываем процедуру расстановки объектов в игровом окне
AddSprites ();
}
void AddSprites()
{
//Переменные для временного хранения адреса
//объекта-игрока
int a=0,b=0;
//Просматриваем массив Layer
for (int i = 0; i < 7; i++)
{
for (int j = 0; j < 12; j++)
{
//Если элемент с индексом (i,j) равен 1 -
//устанавливаем в соответствующую позицию элемент с
//номером 1, то есть - стену
if (Layer[i,j]==1)
Components.Add(new GameObjects.Wall(this, ref txtWall, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 2)
Components.Add(new GameObjects.Ladder(this, ref txtLadder, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 3)
Components.Add(new GameObjects.Bonus1(this, ref txtBonus1, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 4)
Components.Add(new GameObjects.Bonus2(this, ref txtBonus2, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 5)
Components.Add(new GameObjects.Enemy(this, ref txtEnemy, new Vector2(j, i), recSprite, myFont));
//Если обнаружен объект игрока - запишем его координаты
if (Layer[i, j] == 6)
{
a = i;
b = j;
}
}
}
//Последним установим объект игрока - так он гарантированно
//расположен поверх всех остальных объектов
Components.Add(new GameObjects.Me(this, ref txtMe, new Vector2(b, a), new Rectangle (0,0,32,32), myFont));
}
/// <summary>
/// UnloadContent будет вызываться в игре один раз; здесь выгружается
/// весь контент.
/// </summary>
protected override void UnloadContent()
{
txtBackground.Dispose();
txtBonus1.Dispose();
txtBonus2.Dispose();
txtBonus1.Dispose();
txtEnemy.Dispose();
txtLadder.Dispose();
txtMe.Dispose ();
txtWall.Dispose();
spriteBatch.Dispose();
}
/// <summary>
/// Позволяет игре запускать логику обновления мира,
/// проверки столкновений, получения ввода и воспроизведения звуков.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
protected override void Update(GameTime gameTime)
{
// Позволяет выйти из игры
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// ЗАДАЧА: добавьте здесь логику обновления
base.Update(gameTime);
}
/// <summary>
/// Вызывается, когда игра отрисовывается.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// ЗАДАЧА: добавьте здесь код отрисовки
spriteBatch.Begin();
//выведем фоновое изображение
spriteBatch.Draw(txtBackground, recBackround ,Color.White);
//Выведем игровые объекты
base.Draw(gameTime);
//Стрелки
spriteBatch.Draw(txtArrows, recArrows, Color.White);
spriteBatch.End();
}
}
}
Здесь мы, во-первых, загружаем ресурсы, во-вторых, создаем игровые объекты, и, в-третьих, выводим на экран фоновое изображение (оно находится под всеми остальными изображениями
Теперь рассмотрим код класса gBaseClass (листинг 16.3.).
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace P10_1.GameObjects
{
/// <summary>
/// Это игровой компонент, реализующий интерфейс IUpdateable.
/// </summary>
public class gBaseClass : Microsoft.Xna.Framework.DrawableGameComponent
{
Texture2D sprTexture;
public Vector2 sprPosition;
public Rectangle sprRectangle;
public SpriteFont myFont;
public string message="";
public gBaseClass(Game game, ref Texture2D _sprTexture,
Vector2 _sprPosition, Rectangle _sprRectangle, SpriteFont _myFont)
: base(game)
{
sprTexture = _sprTexture;
//Именно здесь производится перевод индекса элемента массива
//в координаты на игровом экране
sprPosition = _sprPosition * 64;
sprPosition.X += 16;
sprRectangle = _sprRectangle;
myFont = _myFont;
}
/// <summary>
/// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.
Здесь можно запросить нужные службы и загрузить контент.
///
/// </summary>
public override void Initialize()
{
// ЗАДАЧА: добавьте здесь код инициализации
base.Initialize();
}
/// <summary>
/// Позволяет игровому компоненту обновиться.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
public override void Update(GameTime gameTime)
{
// ЗАДАЧА: добавьте здесь код обновления
base.Update(gameTime);
}
public override void Draw(GameTime gameTime)
{
SpriteBatch sprBatch =
(SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
sprBatch.Draw(sprTexture, sprPosition, Color.White);
//Если строка сообщения не пуста
if (this.message != "")
{
sprBatch.DrawString(myFont, message, new Vector2(0, 455), Color.Red);
}
base.Draw(gameTime);
}
}
}
Основная задача этого компонента – перевести координаты объекта, выраженные в
Классы Bonus1, Bonus2, Ladder, Wall, Enemy полностью наследуют код класса gBaseComponent, фактически, являясь его точными копиями. Можно было бы отказаться от разработки этих классов, создав соответствующие объекты путем создания объекта класса gBaseComponent. Однако, подход с созданием отдельного класса для каждого компонента предпочтительнее по нескольким причинам.
Во-первых – наличие самостоятельных классов для отдельных компонентов позволяет удобно осуществлять различные проверки при работе с игровым объектом. Во-вторых – если понадобится расширить функциональность одного из объектов (например, перемещать объект Enemy и т.д.) – отдельные классы готовы для модификаций. В листинге 16.4 приведен код класса Bonus1. Остальные классы, как уже было сказано, идентичны ему.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace P10_1.GameObjects
{
/// <summary>
/// Это игровой компонент, реализующий интерфейс IUpdateable.
/// </summary>
public class Bonus1 : gBaseClass
{
public Bonus1(Game game, ref Texture2D _sprTexture,
Vector2 _sprPosition, Rectangle _sprRectangle, SpriteFont _myFont)
: base(game, ref _sprTexture, _sprPosition, _sprRectangle, _myFont)
{
// ЗАДАЧА: здесь создаются дочерние компоненты
}
/// <summary>
/// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.
Здесь можно запросить нужные службы и загрузить контент.
///
/// </summary>
public override void Initialize()
{
// ЗАДАЧА: добавьте здесь код инициализации
base.Initialize();
}
/// <summary>
/// Позволяет игровому компоненту обновиться.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
public override void Update(GameTime gameTime)
{
// ЗАДАЧА: добавьте здесь код обновления
base.Update(gameTime);
}
}
}
Рассмотрим код класса Me (листинге 16.5.) – это основной игровой компонент, который реализует игровой процесс.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
namespace P10_1.GameObjects
{
/// <summary>
/// Это игровой компонент, реализующий интерфейс IUpdateable.
/// </summary>
public class Me :gBaseClass
{
//Прямоугольник, представляющий игровое окно
Rectangle scrBounds;
//Скорость, с которой будет перемещаться спрайт
float sprSpeed=2;
//"Ускорение свободного падения"
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, SpriteFont _myFont)
: base(game, ref _sprTexture, _sprPosition, _sprRectangle, _myFont)
{
scrBounds = new Rectangle(16, 0,
768,
448);
}
//Проверка на столкновение с границами экрана
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;
else
return 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;
}
//Проверяем столкновение объекта со стеной
//Результат проверки нужен для обработки перемещений
bool IsCollideWithWall()
{
foreach (gBaseClass spr in Game.Components)
{
if (spr.GetType() == (typeof(Wall)))
{
if (IsCollideWithObject(spr)) return true;
}
}
return false ;
}
//В этой процедуре мы проверяем столкновения с объектами-бонусами
//и объектами-врагами, причем, наш объект может уничтожать врагов,
//прыгая на них сверху. С точки зрения данной процедуры такой прыжок
//ничем не отличается от обычного контакта с объектом-врагом и приводит к
//проигрышу. Поэтому проверка на прыжок нашего объекта на вражеский объект
//вынесена в отдельную процедуру
void IsCollideWithAny()
{
//Заводим переменную для временного хранения
//ссылки на объект, с которым столкнулся игровой объект
gBaseClass FindObj = null;
//Проверка на столкновение с объектами Bonus1
foreach (gBaseClass spr in Game.Components)
{
if (spr.GetType() == (typeof(Bonus1)))
{
if (IsCollideWithObject(spr))
{
FindObj = spr;
}
}
}
//Если было столкновение - уничтожаем объект,
//с которым было столкновение
//и увеличиваем число очков
if (FindObj != null)
{
sprScores += 100;
FindObj.Dispose();
FindObj = null;
}
//Проверка на столкновение с объектами Bonus2
foreach (gBaseClass spr in Game.Components)
{
if (spr.GetType() == (typeof(Bonus2)))
{
if (IsCollideWithObject(spr))
{
FindObj = spr;
}
}
}
//Если столкновение было
//уничтожим объект
//Увеличим число "жизней"
if (FindObj != null)
{
sprLives += 1;
FindObj.Dispose();
FindObj = null;
}
//Проверка на столкновение с объектами Enemy
foreach (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;
}
}
}
//Если был удачный прыжок на врага
//добавим игроку очков и уничтожим объект класса Enemy
if (FindObj != null)
{
sprScores += 50;
FindObj.Dispose();
}
}
/// <summary>
/// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.
Здесь можно запросить нужные службы и загрузить контент.
///
/// </summary>
public override void Initialize()
{
// ЗАДАЧА: добавьте здесь код инициализации
base.Initialize();
}
//Перемещение объекта
void Move(int Direction)
{
//Вверх
if (Direction==1)
{
//Если под объектом находится стена
//и он не соприкасается с лестницей
//Объект подпрыгивает
if (IsWallIsInTheBottom()==true IsCollideWithLadder ()==false )
{
MoveUp(sprJump);
//При прыжке проводится проверка на контакт со стеной
//Которая может быть расположена над объектом
//при необходимости его координаты корректируются
while (IsCollideWithWall())
{
MoveDown((sprSpeed / 10));
}
}
//Если объект находится на лестнице
//Он перемещается вверх с обычной скоростью
//без прыжков
if (IsCollideWithLadder() == true)
{
MoveUp(sprSpeed);
while (IsCollideWithWall())
{
MoveDown((sprSpeed / 10));
}
}
}
//Вниз
//Происходит обычная процедура перемещения
//объекта с проверкой
if (Direction==2)
{
MoveDown(sprSpeed);
while (IsCollideWithWall())
{
MoveUp((sprSpeed / 10));
}
}
//Точно так же обрабатывается
//движение Влево
if (Direction==3)
{
MoveLeft(sprSpeed);
while (IsCollideWithWall())
{
MoveRight((sprSpeed / 10));
}
}
//Аналогично - перемещение вправо
if (Direction==4)
{
MoveRight(sprSpeed);
while (IsCollideWithWall())
{
MoveLeft((sprSpeed / 10));
}
}
}
//Обработка сенсорного ввода
void TouchInput()
{
//Реализуем управление объектом
//Получаем коллекцию объектов, содержащих информацию о касаниях экрана
TouchCollection touchLocations = TouchPanel.GetState();
//Перебираем коллекцию, присваивая объекту координаты касания
foreach (TouchLocation touchLocation in touchLocations)
{
if (touchLocation.State == TouchLocationState.Pressed || touchLocation.State==TouchLocationState.Moved)
{
//Стрелка "Влево"
if (touchLocation.Position.X > 500 touchLocation.Position.X < 600
touchLocation.Position.Y > 380 touchLocation.Position.Y < 480)
{
Move(3);
}
//Стрелка "Вправо"
if (touchLocation.Position.X > 700 touchLocation.Position.X < 800
touchLocation.Position.Y > 380 touchLocation.Position.Y < 480)
{
Move(4);
}
//Стрелка "Вниз"
if (touchLocation.Position.X > 600 touchLocation.Position.X < 700
touchLocation.Position.Y > 380 touchLocation.Position.Y < 480)
{
Move(2);
}
//Стрелка "Вверх"
if (touchLocation.Position.X > 600 touchLocation.Position.X < 700
touchLocation.Position.Y > 280 touchLocation.Position.Y < 380)
{
Move(1);
}
}
}
}
/// <summary>
/// Позволяет игровому компоненту обновиться.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
public override void Update(GameTime gameTime)
{
//Обработаем касания экрана для реализации перемещения объекта
TouchInput();
//Проверим, не вышел ли он за границы экрана, если надо
//исправим его позицию
Check();
//Применим к объекту "силу тяжести"
GoToDown();
//Проверим, не прыгнул ли наш объект на объект класса Enemy
//Вызов этого метода предшествует вызову метода IsCollideWithAny()
//Иначе прыжок на врага с целью уничтожить его может обернуться
//Печальными последствиями для нашего объекта
IsKillEnemy();
//Проверим на другие столкновения
IsCollideWithAny();
message = "You have " + sprLives.ToString() +
" live(s) and " + sprScores + " points";
if (sprLives < 0)
{
message = "You lose";
this.Dispose();
}
base.Update(gameTime);
}
}
}
Назначение свойств и методов этого класса подробно описано внутри кода. Отметим ключевые методы, которые реализуют игровую физику и управление объектом.
Метод GoToDown() перемещает объект вниз до тех пор, пока он не коснется объекта-стены. Если объект "стоит" на стене (метод IsWallIsInTheBottom()) – он может подпрыгнуть. Если объект касается лестницы (IsCollideWithLadder()) – "сила тяжести" на него не действует, он может перемещаться по лестнице в любом направлении, однако не может подпрыгивать. Если объект сталкивается с бонусом №1 (IsCollideWithAny())– ему начисляются очки, если с бонусом №2 – жизни. Если объект столкнулся с врагом – одна жизнь теряется, если объект "прыгнул" на врага (IsKillEnemy()) – объект врага уничтожается, игроку начисляются очки.
Данную физическую модель мы реализовали самостоятельно, на практике возможно как использование собственных решений, так и применение библиотек сторонних разработчиков.
При управлении объектом мы проверяем попадание касаний экрана в пределы экранных элементов управления (TouchInput()). При этом, мы выполняем перемещение объектов как при состоянии касания Pressed, так и при состоянии касания Moved. Это означает, что объект начнет движение в избранном пользователем направлении при касании соответствующей стрелки и будет двигаться до тех пор, пока пользователь не прекратит касаться стрелки. Если пользователь переведет палец на другую стрелку, не прекращая касания, объект начнет двигаться в новом направлении.
В этой лабораторной работе была рассмотрена методика разработки платформенной игры с использованием элементов моделирования физических явлений.
К данной лабораторной работе прилагается видеоурок
и демонстрационный проект.
Цель работы: Ознакомиться с
Наше знакомство с игровой физикой началось тогда, когда мы начали перемещать объекты по игровому полю, используя для их перемещения понятие скорости. В нашем случае это – скорость измерения координат. Столкновения объектов – это так же пример реализации физических законов, когда мы имитируем столкновение твердых тел в пространстве. Любые другие взаимодействия объектов так же являются примерами применения физических законов в компьютерных играх.
Для реализации более сложных взаимодействий требуется построение
Платформенные игры или платформеры получили такое название потому, что игрок управляет персонажем, который перемещается по площадкам – платформам. В ходе прохождения игры он переходит или перепрыгивает с платформы на платформу, собирает так называемые бонусы, сражается с компьютерными "врагами" и т.д. Действие подобных игр происходит в двумерной среде. Разработаем шаблон такой игры.
Для начала подумаем о том, как нам конструировать игровые экраны. Экран типичной платформенной игры состоит из элементов, которые по-разному взаимодействуют с объектом пользователя. Некоторые из них (стены) непроницаемы для объекта, некоторые (бонусы) при столкновении с объектом исчезают, а игроку начисляются очки или даются какие-то другие улучшения игрового персонажа.
Объекты-лестницы позволяют перемещаться между отдельными "этажами" игрового экрана, которые недоступны при других способах перемещения, объекты-враги отнимают некоторое количество очков (или "жизней") у персонажа. Как правило, объекты, которыми заполнен экран, имеют определенный размер, обычно – одинаковый.
То есть при их расстановке по экрану можно представить, что экран разбит на клетки, равные размерам объекта. Перемещение игрового объекта, контролируемого пользователем, совсем необязательно должно быть прерывистым, поклеточным, однако сам игровой экран удобнее всего конструировать именно таким способом.
Прежде чем начинать разработку, создадим несколько изображений, которые будем использовать в качестве графических образов игровых объектов. На рис. 16.1 вы можете видеть эти объекты.
(рис 16.1) Изображения для визуализации игровых объектов
Перечислим объекты, которые понадобятся нам для создания прототипа платформенной игры.
Кроме того, мы, для организации управления игрой, используем изображение стрелок (рис. 16.2), аналогичное тому, которое использовали в ранее рассмотренных проектах. Разница заключается лишь в том, что это изображение выполнено полупрозрачным – сквозь него можно видеть ту часть игрового поля, над которой оно расположено.
(рис 16.2) Изображение стрелок, используемых для управления игрой
Создадим новый стандартный игровой проект XNA, назовем его P10_1. Добавим в проект Content нужные ресурсы. Все объекты (кроме объекта игрока), которыми мы будем пользоваться, имеют размеры 64х64 пикселя, объект игрока имеет размер 32х32 пикселя. Для начала нам нужно разработать систему конструирования игровых уровней, в данном случае – систему конструирования игровых экранов. Мы пользуемся разрешением игрового окна 768х448 пикселей, то есть – 7х12 элементов размера 64х64 пикселя. Игровой экран разбит на 84 ячейки, в каждой из которых может быть один из игровых объектов.
Для хранения информации о содержимом каждой из этих ячеек используем
Мы назвали этот массив Layer, в листинге 16.1. приведен код его инициализации.
Layer = new int[7, 12] {
{ 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 5, 1, 3, 5, 1, 1, 2, 1, 1, 1, 5 },
{ 0, 0, 0, 1, 2, 1, 1, 2, 0, 0, 0, 4 },
{ 0, 1, 3, 0, 2, 0, 0, 2, 0, 0, 0, 3 },
{ 0, 5, 0, 0, 1, 1, 1, 2, 0, 1, 0, 1 },
{ 6, 0, 1, 3, 4, 0, 5, 2, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
Для удобства мы расположили код инициализации массива с переходом на новую строку для каждой из его строк. В результате этот код отражает состояние игрового экрана в момент начала игры. Как видите, в ячейках массива расположены целые числа. Каждое из этих чисел символизирует определенный вид объекта, который следует поместить в позицию на экране, соответствующую координатам ячейки.
В табл. 16.1 приведено соответствие чисел, используемых в таблице-конструкторе уровня и объектов.
| Номер | Название объекта |
| 0 | Объект отсутствует |
| 1 | Стена |
| 2 | Лестница |
| 3 | Бонус №1 |
| 4 | Бонус №2 |
| 5 | Враг |
| 6 | Игрок |
(рис 16.3) Игровой экран проекта
Кроме того, на экране можно видеть экранные элементы управления объектом и сообщение для пользователя.
На рис. 16.4 вы можете видеть окно Обозреватель решений нашего проекта.
(рис 16.4) Обозреватель решений, проект P10_1
Втабл. 16.2 приведено описание классов, которые мы используем в данном проекте.
| Имя файла | Описание |
| Game1.cs | Класс стандартного проекта |
| gBaseClass.cs | Базовый класс – родитель классов игровых объектов. Он содержит процедуры визуализации объектов и их расстановки по игровому полю. Этот класс наследует класс DrawableGameComponent |
| Bonus1.cs | Класс для создания объектов бонусов вида №1 |
| Bonus2.cs | Класс для создания объектов бонусов вида №2 |
| Enemy.cs | Класс для создания объектов-врагов |
| Ladder.cs | Класс для создания лестниц |
| Me.cs | Класс для создания объекта игрока. Именно он содержит основную часть игровой логики, в том числе – реализацию игровой физики для объекта-игрока |
| Wall.cs | Класс для создания стен |
В листинге 16.2 приведен код класса Game1.
>using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
namespace P10_1
{
/// <summary>
/// Это главный тип игры
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D txtBackground;
Texture2D txtBonus1;
Texture2D txtBonus2;
Texture2D txtEnemy;
Texture2D txtLadder;
Texture2D txtMe;
Texture2D txtWall;
Texture2D txtArrows;
SpriteFont myFont;
//Массив для конструирования уровня
public int[,] Layer;
Rectangle recBackround = new Rectangle(16, 0, 768, 448);
Rectangle recSprite = new Rectangle(0, 0, 64, 64);
Rectangle recArrows = new Rectangle(500, 280, 300, 200);
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
graphics.IsFullScreen = true;
// Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
TargetElapsedTime = TimeSpan.FromTicks(333333);
// Дополнительный заряд аккумулятора заблокирован.
InactiveSleepTime = TimeSpan.FromSeconds(1);
}
/// <summary>
/// Позволяет игре выполнить инициализацию, необходимую перед запуском.
/// Здесь можно запросить нужные службы и загрузить неграфический
/// контент. Вызов base.Initialize приведет к перебору всех компонентов и
/// их инициализации.
/// </summary>
protected override void Initialize()
{
Layer = new int[7, 12] {
{ 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 5, 1, 3, 5, 1, 1, 2, 1, 1, 1, 5 },
{ 0, 0, 0, 1, 2, 1, 1, 2, 0, 0, 0, 4 },
{ 0, 1, 3, 0, 2, 0, 0, 2, 0, 0, 0, 3 },
{ 0, 5, 0, 0, 1, 1, 1, 2, 0, 1, 0, 1 },
{ 6, 0, 1, 3, 4, 0, 5, 2, 0, 0, 0, 1 },
{ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};
base.Initialize();
}
/// <summary>
/// LoadContent будет вызываться в игре один раз; здесь загружается
/// весь контент.
/// </summary>
protected override void LoadContent()
{
// Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
spriteBatch = new SpriteBatch(GraphicsDevice);
Services.AddService(typeof(SpriteBatch), spriteBatch);
txtBackground = Content.Load<Texture2D>("background");
txtBonus2 = Content.Load<Texture2D>("bonus2");
txtBonus1 = Content.Load<Texture2D>("bonus1");
txtEnemy = Content.Load<Texture2D>("enemy");
txtLadder = Content.Load<Texture2D>("ladder");
txtMe = Content.Load<Texture2D>("me");
txtWall = Content.Load<Texture2D>("wall");
txtArrows = Content.Load<Texture2D>("arrows");
myFont = Content.Load<SpriteFont>("MyFont");
//Вызываем процедуру расстановки объектов в игровом окне
AddSprites ();
}
void AddSprites()
{
//Переменные для временного хранения адреса
//объекта-игрока
int a=0,b=0;
//Просматриваем массив Layer
for (int i = 0; i < 7; i++)
{
for (int j = 0; j < 12; j++)
{
//Если элемент с индексом (i,j) равен 1 -
//устанавливаем в соответствующую позицию элемент с
//номером 1, то есть - стену
if (Layer[i,j]==1)
Components.Add(new GameObjects.Wall(this, ref txtWall, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 2)
Components.Add(new GameObjects.Ladder(this, ref txtLadder, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 3)
Components.Add(new GameObjects.Bonus1(this, ref txtBonus1, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 4)
Components.Add(new GameObjects.Bonus2(this, ref txtBonus2, new Vector2(j, i), recSprite, myFont));
if (Layer[i, j] == 5)
Components.Add(new GameObjects.Enemy(this, ref txtEnemy, new Vector2(j, i), recSprite, myFont));
//Если обнаружен объект игрока - запишем его координаты
if (Layer[i, j] == 6)
{
a = i;
b = j;
}
}
}
//Последним установим объект игрока - так он гарантированно
//расположен поверх всех остальных объектов
Components.Add(new GameObjects.Me(this, ref txtMe, new Vector2(b, a), new Rectangle (0,0,32,32), myFont));
}
/// <summary>
/// UnloadContent будет вызываться в игре один раз; здесь выгружается
/// весь контент.
/// </summary>
protected override void UnloadContent()
{
txtBackground.Dispose();
txtBonus1.Dispose();
txtBonus2.Dispose();
txtBonus1.Dispose();
txtEnemy.Dispose();
txtLadder.Dispose();
txtMe.Dispose ();
txtWall.Dispose();
spriteBatch.Dispose();
}
/// <summary>
/// Позволяет игре запускать логику обновления мира,
/// проверки столкновений, получения ввода и воспроизведения звуков.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
protected override void Update(GameTime gameTime)
{
// Позволяет выйти из игры
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
// ЗАДАЧА: добавьте здесь логику обновления
base.Update(gameTime);
}
/// <summary>
/// Вызывается, когда игра отрисовывается.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// ЗАДАЧА: добавьте здесь код отрисовки
spriteBatch.Begin();
//выведем фоновое изображение
spriteBatch.Draw(txtBackground, recBackround ,Color.White);
//Выведем игровые объекты
base.Draw(gameTime);
//Стрелки
spriteBatch.Draw(txtArrows, recArrows, Color.White);
spriteBatch.End();
}
}
}
Здесь мы, во-первых, загружаем ресурсы, во-вторых, создаем игровые объекты, и, в-третьих, выводим на экран фоновое изображение (оно находится под всеми остальными изображениями
Теперь рассмотрим код класса gBaseClass (листинг 16.3.).
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace P10_1.GameObjects
{
/// <summary>
/// Это игровой компонент, реализующий интерфейс IUpdateable.
/// </summary>
public class gBaseClass : Microsoft.Xna.Framework.DrawableGameComponent
{
Texture2D sprTexture;
public Vector2 sprPosition;
public Rectangle sprRectangle;
public SpriteFont myFont;
public string message="";
public gBaseClass(Game game, ref Texture2D _sprTexture,
Vector2 _sprPosition, Rectangle _sprRectangle, SpriteFont _myFont)
: base(game)
{
sprTexture = _sprTexture;
//Именно здесь производится перевод индекса элемента массива
//в координаты на игровом экране
sprPosition = _sprPosition * 64;
sprPosition.X += 16;
sprRectangle = _sprRectangle;
myFont = _myFont;
}
/// <summary>
/// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.
Здесь можно запросить нужные службы и загрузить контент.
///
/// </summary>
public override void Initialize()
{
// ЗАДАЧА: добавьте здесь код инициализации
base.Initialize();
}
/// <summary>
/// Позволяет игровому компоненту обновиться.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
public override void Update(GameTime gameTime)
{
// ЗАДАЧА: добавьте здесь код обновления
base.Update(gameTime);
}
public override void Draw(GameTime gameTime)
{
SpriteBatch sprBatch =
(SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
sprBatch.Draw(sprTexture, sprPosition, Color.White);
//Если строка сообщения не пуста
if (this.message != "")
{
sprBatch.DrawString(myFont, message, new Vector2(0, 455), Color.Red);
}
base.Draw(gameTime);
}
}
}
Основная задача этого компонента – перевести координаты объекта, выраженные в
Классы Bonus1, Bonus2, Ladder, Wall, Enemy полностью наследуют код класса gBaseComponent, фактически, являясь его точными копиями. Можно было бы отказаться от разработки этих классов, создав соответствующие объекты путем создания объекта класса gBaseComponent. Однако, подход с созданием отдельного класса для каждого компонента предпочтительнее по нескольким причинам.
Во-первых – наличие самостоятельных классов для отдельных компонентов позволяет удобно осуществлять различные проверки при работе с игровым объектом. Во-вторых – если понадобится расширить функциональность одного из объектов (например, перемещать объект Enemy и т.д.) – отдельные классы готовы для модификаций. В листинге 16.4 приведен код класса Bonus1. Остальные классы, как уже было сказано, идентичны ему.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace P10_1.GameObjects
{
/// <summary>
/// Это игровой компонент, реализующий интерфейс IUpdateable.
/// </summary>
public class Bonus1 : gBaseClass
{
public Bonus1(Game game, ref Texture2D _sprTexture,
Vector2 _sprPosition, Rectangle _sprRectangle, SpriteFont _myFont)
: base(game, ref _sprTexture, _sprPosition, _sprRectangle, _myFont)
{
// ЗАДАЧА: здесь создаются дочерние компоненты
}
/// <summary>
/// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.
Здесь можно запросить нужные службы и загрузить контент.
///
/// </summary>
public override void Initialize()
{
// ЗАДАЧА: добавьте здесь код инициализации
base.Initialize();
}
/// <summary>
/// Позволяет игровому компоненту обновиться.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
public override void Update(GameTime gameTime)
{
// ЗАДАЧА: добавьте здесь код обновления
base.Update(gameTime);
}
}
}
Рассмотрим код класса Me (листинге 16.5.) – это основной игровой компонент, который реализует игровой процесс.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;
namespace P10_1.GameObjects
{
/// <summary>
/// Это игровой компонент, реализующий интерфейс IUpdateable.
/// </summary>
public class Me :gBaseClass
{
//Прямоугольник, представляющий игровое окно
Rectangle scrBounds;
//Скорость, с которой будет перемещаться спрайт
float sprSpeed=2;
//"Ускорение свободного падения"
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, SpriteFont _myFont)
: base(game, ref _sprTexture, _sprPosition, _sprRectangle, _myFont)
{
scrBounds = new Rectangle(16, 0,
768,
448);
}
//Проверка на столкновение с границами экрана
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;
else
return 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;
}
//Проверяем столкновение объекта со стеной
//Результат проверки нужен для обработки перемещений
bool IsCollideWithWall()
{
foreach (gBaseClass spr in Game.Components)
{
if (spr.GetType() == (typeof(Wall)))
{
if (IsCollideWithObject(spr)) return true;
}
}
return false ;
}
//В этой процедуре мы проверяем столкновения с объектами-бонусами
//и объектами-врагами, причем, наш объект может уничтожать врагов,
//прыгая на них сверху. С точки зрения данной процедуры такой прыжок
//ничем не отличается от обычного контакта с объектом-врагом и приводит к
//проигрышу. Поэтому проверка на прыжок нашего объекта на вражеский объект
//вынесена в отдельную процедуру
void IsCollideWithAny()
{
//Заводим переменную для временного хранения
//ссылки на объект, с которым столкнулся игровой объект
gBaseClass FindObj = null;
//Проверка на столкновение с объектами Bonus1
foreach (gBaseClass spr in Game.Components)
{
if (spr.GetType() == (typeof(Bonus1)))
{
if (IsCollideWithObject(spr))
{
FindObj = spr;
}
}
}
//Если было столкновение - уничтожаем объект,
//с которым было столкновение
//и увеличиваем число очков
if (FindObj != null)
{
sprScores += 100;
FindObj.Dispose();
FindObj = null;
}
//Проверка на столкновение с объектами Bonus2
foreach (gBaseClass spr in Game.Components)
{
if (spr.GetType() == (typeof(Bonus2)))
{
if (IsCollideWithObject(spr))
{
FindObj = spr;
}
}
}
//Если столкновение было
//уничтожим объект
//Увеличим число "жизней"
if (FindObj != null)
{
sprLives += 1;
FindObj.Dispose();
FindObj = null;
}
//Проверка на столкновение с объектами Enemy
foreach (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;
}
}
}
//Если был удачный прыжок на врага
//добавим игроку очков и уничтожим объект класса Enemy
if (FindObj != null)
{
sprScores += 50;
FindObj.Dispose();
}
}
/// <summary>
/// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.
Здесь можно запросить нужные службы и загрузить контент.
///
/// </summary>
public override void Initialize()
{
// ЗАДАЧА: добавьте здесь код инициализации
base.Initialize();
}
//Перемещение объекта
void Move(int Direction)
{
//Вверх
if (Direction==1)
{
//Если под объектом находится стена
//и он не соприкасается с лестницей
//Объект подпрыгивает
if (IsWallIsInTheBottom()==true IsCollideWithLadder ()==false )
{
MoveUp(sprJump);
//При прыжке проводится проверка на контакт со стеной
//Которая может быть расположена над объектом
//при необходимости его координаты корректируются
while (IsCollideWithWall())
{
MoveDown((sprSpeed / 10));
}
}
//Если объект находится на лестнице
//Он перемещается вверх с обычной скоростью
//без прыжков
if (IsCollideWithLadder() == true)
{
MoveUp(sprSpeed);
while (IsCollideWithWall())
{
MoveDown((sprSpeed / 10));
}
}
}
//Вниз
//Происходит обычная процедура перемещения
//объекта с проверкой
if (Direction==2)
{
MoveDown(sprSpeed);
while (IsCollideWithWall())
{
MoveUp((sprSpeed / 10));
}
}
//Точно так же обрабатывается
//движение Влево
if (Direction==3)
{
MoveLeft(sprSpeed);
while (IsCollideWithWall())
{
MoveRight((sprSpeed / 10));
}
}
//Аналогично - перемещение вправо
if (Direction==4)
{
MoveRight(sprSpeed);
while (IsCollideWithWall())
{
MoveLeft((sprSpeed / 10));
}
}
}
//Обработка сенсорного ввода
void TouchInput()
{
//Реализуем управление объектом
//Получаем коллекцию объектов, содержащих информацию о касаниях экрана
TouchCollection touchLocations = TouchPanel.GetState();
//Перебираем коллекцию, присваивая объекту координаты касания
foreach (TouchLocation touchLocation in touchLocations)
{
if (touchLocation.State == TouchLocationState.Pressed || touchLocation.State==TouchLocationState.Moved)
{
//Стрелка "Влево"
if (touchLocation.Position.X > 500 touchLocation.Position.X < 600
touchLocation.Position.Y > 380 touchLocation.Position.Y < 480)
{
Move(3);
}
//Стрелка "Вправо"
if (touchLocation.Position.X > 700 touchLocation.Position.X < 800
touchLocation.Position.Y > 380 touchLocation.Position.Y < 480)
{
Move(4);
}
//Стрелка "Вниз"
if (touchLocation.Position.X > 600 touchLocation.Position.X < 700
touchLocation.Position.Y > 380 touchLocation.Position.Y < 480)
{
Move(2);
}
//Стрелка "Вверх"
if (touchLocation.Position.X > 600 touchLocation.Position.X < 700
touchLocation.Position.Y > 280 touchLocation.Position.Y < 380)
{
Move(1);
}
}
}
}
/// <summary>
/// Позволяет игровому компоненту обновиться.
/// </summary>
/// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
public override void Update(GameTime gameTime)
{
//Обработаем касания экрана для реализации перемещения объекта
TouchInput();
//Проверим, не вышел ли он за границы экрана, если надо
//исправим его позицию
Check();
//Применим к объекту "силу тяжести"
GoToDown();
//Проверим, не прыгнул ли наш объект на объект класса Enemy
//Вызов этого метода предшествует вызову метода IsCollideWithAny()
//Иначе прыжок на врага с целью уничтожить его может обернуться
//Печальными последствиями для нашего объекта
IsKillEnemy();
//Проверим на другие столкновения
IsCollideWithAny();
message = "You have " + sprLives.ToString() +
" live(s) and " + sprScores + " points";
if (sprLives < 0)
{
message = "You lose";
this.Dispose();
}
base.Update(gameTime);
}
}
}
Назначение свойств и методов этого класса подробно описано внутри кода. Отметим ключевые методы, которые реализуют игровую физику и управление объектом.
Метод GoToDown() перемещает объект вниз до тех пор, пока он не коснется объекта-стены. Если объект "стоит" на стене (метод IsWallIsInTheBottom()) – он может подпрыгнуть. Если объект касается лестницы (IsCollideWithLadder()) – "сила тяжести" на него не действует, он может перемещаться по лестнице в любом направлении, однако не может подпрыгивать. Если объект сталкивается с бонусом №1 (IsCollideWithAny())– ему начисляются очки, если с бонусом №2 – жизни. Если объект столкнулся с врагом – одна жизнь теряется, если объект "прыгнул" на врага (IsKillEnemy()) – объект врага уничтожается, игроку начисляются очки.
Данную физическую модель мы реализовали самостоятельно, на практике возможно как использование собственных решений, так и применение библиотек сторонних разработчиков.
При управлении объектом мы проверяем попадание касаний экрана в пределы экранных элементов управления (TouchInput()). При этом, мы выполняем перемещение объектов как при состоянии касания Pressed, так и при состоянии касания Moved. Это означает, что объект начнет движение в избранном пользователем направлении при касании соответствующей стрелки и будет двигаться до тех пор, пока пользователь не прекратит касаться стрелки. Если пользователь переведет палец на другую стрелку, не прекращая касания, объект начнет двигаться в новом направлении.
В этой лабораторной работе была рассмотрена методика разработки платформенной игры с использованием элементов моделирования физических явлений.
К данной лабораторной работе прилагается видеоурок
и демонстрационный проект.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.