Разработка компьютерных игр для Windows Phone 7 с использованием технологий Silverlight и XNA

Игровая физика

Разбить на страницы
Показывать лекцию целиком

Цель работы: Ознакомиться с методологией разработки платформеных игр и реализации в играх физических законов

16.1. Шаблон платформенной игры

Наше знакомство с игровой физикой началось тогда, когда мы начали перемещать объекты по игровому полю, используя для их перемещения понятие скорости. В нашем случае это – скорость измерения координат. Столкновения объектов – это так же пример реализации физических законов, когда мы имитируем столкновение твердых тел в пространстве. Любые другие взаимодействия объектов так же являются примерами применения физических законов в компьютерных играх.

Для реализации более сложных взаимодействий требуется построение физической модели игрового объекта, которая учитывает значимые воздействия на этот объект.

Платформенные игры или платформеры получили такое название потому, что игрок управляет персонажем, который перемещается по площадкам – платформам. В ходе прохождения игры он переходит или перепрыгивает с платформы на платформу, собирает так называемые бонусы, сражается с компьютерными "врагами" и т.д. Действие подобных игр происходит в двумерной среде. Разработаем шаблон такой игры.

Для начала подумаем о том, как нам конструировать игровые экраны. Экран типичной платформенной игры состоит из элементов, которые по-разному взаимодействуют с объектом пользователя. Некоторые из них (стены) непроницаемы для объекта, некоторые (бонусы) при столкновении с объектом исчезают, а игроку начисляются очки или даются какие-то другие улучшения игрового персонажа.

Объекты-лестницы позволяют перемещаться между отдельными "этажами" игрового экрана, которые недоступны при других способах перемещения, объекты-враги отнимают некоторое количество очков (или "жизней") у персонажа. Как правило, объекты, которыми заполнен экран, имеют определенный размер, обычно – одинаковый.

То есть при их расстановке по экрану можно представить, что экран разбит на клетки, равные размерам объекта. Перемещение игрового объекта, контролируемого пользователем, совсем необязательно должно быть прерывистым, поклеточным, однако сам игровой экран удобнее всего конструировать именно таким способом.

Прежде чем начинать разработку, создадим несколько изображений, которые будем использовать в качестве графических образов игровых объектов. На рис. 16.1 вы можете видеть эти объекты.

(рис 16.1) Изображения для визуализации игровых объектов

Перечислим объекты, которые понадобятся нам для создания прототипа платформенной игры.

  • Объект игрока (me.png) – им управляет играющий.
  • Фон (background.png) – фоновое изображение.
  • Бонус №1 (bonus1.png) – бонус первого вида. Если объект игрока соприкасается с этим бонусом, игроку начисляется 100 очков.
  • Бонус №2 (bonus2.png) – бонус второго вида. Если объект игрока соприкасается с этим бонусом, игроку добавляется 1 "жизнь".
  • Враг (enemy.png) – объект "врага". Если игрок соприкасается с объектом врага – количество "жизней" уменьшается на 1. При количестве жизней меньше 0 игра заканчивается. Если игрок сможет прыгнуть и приземлиться на объект врага – враг будет уничтожен, а игрок получит 50 очков.
  • Лестница (ladder.png) – игрок может подниматься и опускаться по лестнице там, где другим способом ему не пройти.
  • Стена (wall.png) – стена. Стена непроницаема для объекта игрока. Объект игрока может подпрыгнуть только тогда, когда снизу находится стена.
  • Кроме того, мы, для организации управления игрой, используем изображение стрелок (рис. 16.2), аналогичное тому, которое использовали в ранее рассмотренных проектах. Разница заключается лишь в том, что это изображение выполнено полупрозрачным – сквозь него можно видеть ту часть игрового поля, над которой оно расположено.

    (рис 16.2) Изображение стрелок, используемых для управления игрой

    Создадим новый стандартный игровой проект XNA, назовем его P10_1. Добавим в проект Content нужные ресурсы. Все объекты (кроме объекта игрока), которыми мы будем пользоваться, имеют размеры 64х64 пикселя, объект игрока имеет размер 32х32 пикселя. Для начала нам нужно разработать систему конструирования игровых уровней, в данном случае – систему конструирования игровых экранов. Мы пользуемся разрешением игрового окна 768х448 пикселей, то есть – 7х12 элементов размера 64х64 пикселя. Игровой экран разбит на 84 ячейки, в каждой из которых может быть один из игровых объектов.

    Для хранения информации о содержимом каждой из этих ячеек используем двумерный массив. Каждая ячейка массива соответствует ячейке игрового экрана. Для того, чтобы получить координаты ячейки, соответствующие ячейке массива с индексом (i, j), достаточно умножить каждый из элементов индекса на 64. Верхней левой ячейке экрана соответствует элемент массива с индексом (0, 0), ячейке в правом нижнем углу – элемент (6,11).

    Мы назвали этот массив 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 приведено соответствие чисел, используемых в таблице-конструкторе уровня и объектов.

    Номера объектов для размещения в массиве Layer
    НомерНазвание объекта
    0Объект отсутствует
    1Стена
    2Лестница
    3Бонус №1
    4Бонус №2
    5Враг
    6Игрок

    На рис. 16.3 приведено изображение игрового экрана, сконструированного в соответствии с таблицей и содержимым массива.

    (рис 16.3) Игровой экран проекта

    Кроме того, на экране можно видеть экранные элементы управления объектом и сообщение для пользователя.

    На рис. 16.4 вы можете видеть окно Обозреватель решений нашего проекта.

    (рис 16.4) Обозреватель решений, проект P10_1

    Втабл. 16.2 приведено описание классов, которые мы используем в данном проекте.

    Классы проекта P10_1
    Имя файлаОписание
    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);
            }
    
        }
    }

    Основная задача этого компонента – перевести координаты объекта, выраженные в индексе массива в координаты объекта на игровом экране и вывести объект на экран. Это достигается умножением индексов на 64. Так же здесь объявляются переменные для хранения координат объекта, информации о прямоугольнике, ограничивающем объект и о текстуре объекта.

    Классы 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.2. Выводы

    В этой лабораторной работе была рассмотрена методика разработки платформенной игры с использованием элементов моделирования физических явлений.

    16.3. Задание

  • Разработайте собственные спрайты для визуализации объектов.
  • Реализуйте самостоятельно игру, которая описана выше.
  • Доработайте объект Enemy таким образом, чтобы он мог автоматически (по параметрам, переданным при его конструировании) перемещаться по экрану в горизонтальной или вертикальной плоскости, отталкиваясь от других объектов (кроме лестницы).
  • 16.4. Дополнительные материалы

    К данной лабораторной работе прилагается видеоурок

    и демонстрационный проект.

    Страницы:

    Цель работы: Ознакомиться с методологией разработки платформеных игр и реализации в играх физических законов

    16.1. Шаблон платформенной игры

    Наше знакомство с игровой физикой началось тогда, когда мы начали перемещать объекты по игровому полю, используя для их перемещения понятие скорости. В нашем случае это – скорость измерения координат. Столкновения объектов – это так же пример реализации физических законов, когда мы имитируем столкновение твердых тел в пространстве. Любые другие взаимодействия объектов так же являются примерами применения физических законов в компьютерных играх.

    Для реализации более сложных взаимодействий требуется построение физической модели игрового объекта, которая учитывает значимые воздействия на этот объект.

    Платформенные игры или платформеры получили такое название потому, что игрок управляет персонажем, который перемещается по площадкам – платформам. В ходе прохождения игры он переходит или перепрыгивает с платформы на платформу, собирает так называемые бонусы, сражается с компьютерными "врагами" и т.д. Действие подобных игр происходит в двумерной среде. Разработаем шаблон такой игры.

    Для начала подумаем о том, как нам конструировать игровые экраны. Экран типичной платформенной игры состоит из элементов, которые по-разному взаимодействуют с объектом пользователя. Некоторые из них (стены) непроницаемы для объекта, некоторые (бонусы) при столкновении с объектом исчезают, а игроку начисляются очки или даются какие-то другие улучшения игрового персонажа.

    Объекты-лестницы позволяют перемещаться между отдельными "этажами" игрового экрана, которые недоступны при других способах перемещения, объекты-враги отнимают некоторое количество очков (или "жизней") у персонажа. Как правило, объекты, которыми заполнен экран, имеют определенный размер, обычно – одинаковый.

    То есть при их расстановке по экрану можно представить, что экран разбит на клетки, равные размерам объекта. Перемещение игрового объекта, контролируемого пользователем, совсем необязательно должно быть прерывистым, поклеточным, однако сам игровой экран удобнее всего конструировать именно таким способом.

    Прежде чем начинать разработку, создадим несколько изображений, которые будем использовать в качестве графических образов игровых объектов. На рис. 16.1 вы можете видеть эти объекты.

    (рис 16.1) Изображения для визуализации игровых объектов

    Перечислим объекты, которые понадобятся нам для создания прототипа платформенной игры.

  • Объект игрока (me.png) – им управляет играющий.
  • Фон (background.png) – фоновое изображение.
  • Бонус №1 (bonus1.png) – бонус первого вида. Если объект игрока соприкасается с этим бонусом, игроку начисляется 100 очков.
  • Бонус №2 (bonus2.png) – бонус второго вида. Если объект игрока соприкасается с этим бонусом, игроку добавляется 1 "жизнь".
  • Враг (enemy.png) – объект "врага". Если игрок соприкасается с объектом врага – количество "жизней" уменьшается на 1. При количестве жизней меньше 0 игра заканчивается. Если игрок сможет прыгнуть и приземлиться на объект врага – враг будет уничтожен, а игрок получит 50 очков.
  • Лестница (ladder.png) – игрок может подниматься и опускаться по лестнице там, где другим способом ему не пройти.
  • Стена (wall.png) – стена. Стена непроницаема для объекта игрока. Объект игрока может подпрыгнуть только тогда, когда снизу находится стена.
  • Кроме того, мы, для организации управления игрой, используем изображение стрелок (рис. 16.2), аналогичное тому, которое использовали в ранее рассмотренных проектах. Разница заключается лишь в том, что это изображение выполнено полупрозрачным – сквозь него можно видеть ту часть игрового поля, над которой оно расположено.

    (рис 16.2) Изображение стрелок, используемых для управления игрой

    Создадим новый стандартный игровой проект XNA, назовем его P10_1. Добавим в проект Content нужные ресурсы. Все объекты (кроме объекта игрока), которыми мы будем пользоваться, имеют размеры 64х64 пикселя, объект игрока имеет размер 32х32 пикселя. Для начала нам нужно разработать систему конструирования игровых уровней, в данном случае – систему конструирования игровых экранов. Мы пользуемся разрешением игрового окна 768х448 пикселей, то есть – 7х12 элементов размера 64х64 пикселя. Игровой экран разбит на 84 ячейки, в каждой из которых может быть один из игровых объектов.

    Для хранения информации о содержимом каждой из этих ячеек используем двумерный массив. Каждая ячейка массива соответствует ячейке игрового экрана. Для того, чтобы получить координаты ячейки, соответствующие ячейке массива с индексом (i, j), достаточно умножить каждый из элементов индекса на 64. Верхней левой ячейке экрана соответствует элемент массива с индексом (0, 0), ячейке в правом нижнем углу – элемент (6,11).

    Мы назвали этот массив 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 приведено соответствие чисел, используемых в таблице-конструкторе уровня и объектов.

    Номера объектов для размещения в массиве Layer
    НомерНазвание объекта
    0Объект отсутствует
    1Стена
    2Лестница
    3Бонус №1
    4Бонус №2
    5Враг
    6Игрок

    На рис. 16.3 приведено изображение игрового экрана, сконструированного в соответствии с таблицей и содержимым массива.

    (рис 16.3) Игровой экран проекта

    Кроме того, на экране можно видеть экранные элементы управления объектом и сообщение для пользователя.

    На рис. 16.4 вы можете видеть окно Обозреватель решений нашего проекта.

    (рис 16.4) Обозреватель решений, проект P10_1

    Втабл. 16.2 приведено описание классов, которые мы используем в данном проекте.

    Классы проекта P10_1
    Имя файлаОписание
    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);
            }
    
        }
    }

    Основная задача этого компонента – перевести координаты объекта, выраженные в индексе массива в координаты объекта на игровом экране и вывести объект на экран. Это достигается умножением индексов на 64. Так же здесь объявляются переменные для хранения координат объекта, информации о прямоугольнике, ограничивающем объект и о текстуре объекта.

    Классы 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.2. Выводы

    В этой лабораторной работе была рассмотрена методика разработки платформенной игры с использованием элементов моделирования физических явлений.

    16.3. Задание

  • Разработайте собственные спрайты для визуализации объектов.
  • Реализуйте самостоятельно игру, которая описана выше.
  • Доработайте объект Enemy таким образом, чтобы он мог автоматически (по параметрам, переданным при его конструировании) перемещаться по экрану в горизонтальной или вертикальной плоскости, отталкиваясь от других объектов (кроме лестницы).
  • 16.4. Дополнительные материалы

    К данной лабораторной работе прилагается видеоурок

    и демонстрационный проект.

    Вернуться к учебному плану