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

Многоуровневые игры

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

Цель работы: Освоить идеи разработки многоуровневых игр и конструкторов уровней

21.1. Организация многоуровневой игры

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

В рамках этой лабораторной работы мы создадим игру – клон популярной игры Arkanoid. Наша игра будет обладать следующим набором характеристик.

  • Имеется 2 вида блоков – один из них уничтожается при попадании в него мяча, причем, мяч отскакивает, второй – не уничтожается.
  • При уничтожении всех блоков текущего уровня осуществляется переход на следующий уровень.
  • После того, как мяч выйдет за пределы поля – игра окончится и произойдет загрузка первого уровня.
  • Игрок может играть в стандартном режиме и в режиме пользовательского уровня. Пользовательский уровень создается игроком самостоятельно с помощью конструктора уровней.
  • Здесь мы создадим каркас игры и конструктор уровней для создания пользовательских уровней.

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

    (рис 21.1) Обозреватель решений для проекта P15_1

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

    Классы Ball, Bat, Brick1, Brick2 – это игровые компоненты, соответственно, для мяча (Ball), биты (Bat), и двух видов блоков – обычного, который уничтожается при попадании в него мяча (Brick1), и неуничтожимого (Brick2).

    Класс BrickBuilder используется для создания указателя, который применяется в конструкторе уровней.

    На рис. 21.2 вы можете видеть изображения графических ресурсов, примененных в игре.

    (рис 21.2) Игровые ресурсы

    Изображения BrickBuild используются в конструкторе уровней. Указатель с изображением BrickBuild0.png очищает ячейку игрового поля, расположенную под ней. Указатель с BrickBuild1.png устанавливает в текущую позицию игрового поля объект Brick1, указатель с изображением BrickBuild2.png устанавливает в текущую ячейку объект Brick2.

    Изображение Arrows используется для управления конструированием игры. Оно выполнено полупрозрачным и в режиме конструктора выводится на экран. Изображения ToCinstr и ToGame выводятся в верхней части экрана и служат для перехода из игрового режима в режим конструктора и наоборот.

    На рис. 21.3 вы можете видеть игровой экран.

    (рис 21.3) Игровой экран

    На рис. 21.4 представлен экран в режиме конструктора уровней.

    (рис 21.4) Режим конструктора уровней

    Рассмотрим коды классов, которые используются в игре.

    В листинге 21.1 приведен код класса Game1.

    using System;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    using Microsoft.Xna.Framework.Input.Touch;
    using Microsoft.Devices;
    
    namespace P15_1
    {
        /// <summary>
        /// Это главный тип игры
        /// </summary>
        public class Game1 : Microsoft.Xna.Framework.Game
        {
            GraphicsDeviceManager graphics;
            SpriteBatch spriteBatch;
            //Текстуры
            Texture2D txtBack, txtBrick1, txtBrick2, txtBat, txtBall;
            Texture2D txtBrBuild1, txtBrBuild2, txtBrBuild0;
            Texture2D txtArrows, txtToGame, txtToConstr;
            //Массивы для хранения данных об уровнях
            public byte[,] Layer1, Layer2, Layer3, UserLayer;
            //Прямоугольник для фона
            Rectangle recBack = new Rectangle(0, 0, 480, 800);
            //Прямоугольник для "блока"
            Rectangle recBrick = new Rectangle(0, 0, 64, 16);
            //Прямоугольник для "мяча"
            Rectangle recBall = new Rectangle(0, 0, 10, 10);
            //Прямоугольник для биты
            Rectangle recBat = new Rectangle(0, 0, 96, 16);
            //Номер текущего уровня
            //1 - первый
            //2 - второй
            //3 - третий
            //-1 - пользовательский
            int currentLayer;
            //Установлена в false, если программа находится в 
            //режиме игры, при установке в True включается
            //режим конструктора
            bool IsBuild;
            //Позиция указателя в режиме конструктора
            Vector2 PositionBuild;
            //Тип блока, который будет добавлен на игровое поле
            //при поступлении соответствующей команды в режиме 
            //конструктора
            byte typeOfBuildBlock;
            //Прямоугольники для элементов управления
            Rectangle keyGameOrBuild = new Rectangle(0, 0, 480, 100);
            Rectangle keyChange = new Rectangle(90, 600, 100, 100);
            Rectangle keyUp = new Rectangle(190, 600, 100, 100);
            Rectangle keyOK = new Rectangle(290, 600, 100, 100);
            Rectangle keyLeft = new Rectangle(90, 700, 100, 100);
            Rectangle keyDown = new Rectangle(190, 700, 100, 100);
            Rectangle keyRight = new Rectangle(290, 700, 100, 100);
    
            public Game1()
            {
                graphics = new GraphicsDeviceManager(this);
                Content.RootDirectory = "Content";
                graphics.IsFullScreen = true;
                graphics.SupportedOrientations = DisplayOrientation.Portrait;
                graphics.PreferredBackBufferHeight = 800;
                graphics.PreferredBackBufferWidth = 480;
                // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
                TargetElapsedTime = TimeSpan.FromTicks(333333);
                // Дополнительный заряд аккумулятора заблокирован.
                InactiveSleepTime = TimeSpan.FromSeconds(1);
            }
    
            /// <summary>
            /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
            /// Здесь можно запросить нужные службы и загрузить неграфический
            /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
            /// их инициализации.
            /// </summary>
            protected override void Initialize()
            {
                //Программа находится в игровом режиме
                IsBuild = false;
                //Позиция для указателя в режиме конструктора (0,0)
                PositionBuild = new Vector2(0, 0);
                //Тип блока по умолчанию в режиме конструктора - 1
                typeOfBuildBlock = 1;
                //пользовательский уровень по умолчанию пуст - ячейки
                //заполнены нулями
                UserLayer = new byte[50, 7];
                //Заполняем массивы для разных уровней
                //0 - пустая ячейка
                //1 - блок первого типа (уничтожимый)
                //2 - блок второго типа (неуничтожимый)
                Layer1 = new byte[50, 7]{
                {1,1,1,1,1,1,1},
                {0,1,0,1,0,1,0},
                {1,0,1,0,1,0,1},
                {0,1,0,1,0,1,0},
                {1,0,1,0,1,0,1},
                {0,1,0,1,0,1,0},
                {0,0,0,0,0,0,0},
                {2,0,0,2,0,0,2},
                {0,1,0,0,0,0,0},
                {0,0,1,0,0,0,1},
                {0,0,0,1,0,1,0},
                {0,0,0,0,1,0,0},
                {1,1,1,1,1,1,1},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                };
    
                Layer2 = new byte[50, 7]{
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,2,1,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                };
    
                Layer3 = new byte[50, 7]{
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,1,2,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,1,1,1,1,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,1,2,0,2,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,1,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                };
    
                //Текущий уровень - 1
                currentLayer = 1;
    
    
                base.Initialize();
            }
    
            /// <summary>
            /// LoadContent будет вызываться в игре один раз; здесь загружается
            /// весь контент.
            /// </summary>
            protected override void LoadContent()
            {
                // Загружаем ресурсы
                spriteBatch = new SpriteBatch(GraphicsDevice);
                Services.AddService(typeof(SpriteBatch), spriteBatch);
                txtBack = Content.Load<Texture2D>("Back");
                txtBrick1 = Content.Load<Texture2D>("Brick1");
                txtBrick2 = Content.Load<Texture2D>("Brick2");
                txtBall = Content.Load<Texture2D>("Ball");
                txtBat = Content.Load<Texture2D>("Bat");
                txtBrBuild0 = Content.Load<Texture2D>("BrickBuild0");
                txtBrBuild1 = Content.Load<Texture2D>("BrickBuild1");
                txtBrBuild2 = Content.Load<Texture2D>("BrickBuild2");
                txtArrows = Content.Load<Texture2D>("Arrows");
                txtToGame = Content.Load<Texture2D>("ToGame");
                txtToConstr = Content.Load<Texture2D>("ToConstr");
                //Создаем уровень
                AddSprites(Layer1);
                //Добавляем биту и мяч
                addBallAndBAts();
    
            }
    
            //Процедура создания уровня
            //расставляет элементы в соответствии с
            //массивом
            void AddSprites(byte[,] RenderLayer)
            {
                //Просматриваем массив
                for (int i = 0; i < 50; i++)
                {
                    for (int j = 0; j < 7; j++)
                    {
                        if (RenderLayer[i, j] == 1)
                            Components.Add(new Brick1(this, ref txtBrick1, new Vector2(j, i), recBrick, 64, 16));
                        if (RenderLayer[i, j] == 2)
                            Components.Add(new Brick2(this, ref txtBrick2, new Vector2(j, i), recBrick, 64, 16));
                    }
                }
            }
            //Процедура для установки биты и мяча
            void addBallAndBAts()
            {
                Components.Add(new Ball(this, ref txtBall, new Vector2(300, 750), recBall, 1, 1));
                Components.Add(new Bat(this, ref txtBat, new Vector2(300, (800 - 16)), recBat, 1, 1));
            }
    
    
    
            /// <summary>
            /// UnloadContent будет вызываться в игре один раз; здесь выгружается
            /// весь контент.
            /// </summary>
            protected override void UnloadContent()
            {
                // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
            }
            //Проверка попадания касания в область, занимаемую одним из элементов управления
            private bool MenuSelect(Rectangle m, Vector2 p)
            {
                bool res = false;
                if (p.X > m.X  p.X < m.X + m.Width  p.Y > m.Y  p.Y < m.Y + m.Height)
                {
                    res = true;
                }
    
                return res;
            }
    
            /// <summary>
            /// Позволяет игре запускать логику обновления мира,
            /// проверки столкновений, получения ввода и воспроизведения звуков.
            /// </summary>
            /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
            protected override void Update(GameTime gameTime)
            {
                // Позволяет выйти из игры
                if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                    this.Exit();
    
                TouchCollection touchLocations = TouchPanel.GetState();
                //Если находимся в игровом режиме
                if (!IsBuild)
                {
                    foreach (TouchLocation touchLocation in touchLocations)
                    {
                        if (touchLocation.State == TouchLocationState.Pressed)
                        {
                            //Если пользователь коснулся кнопки для перехода в режим конструктора
                            if (MenuSelect(keyGameOrBuild, touchLocation.Position))
                            {
                                //Включаем конструктор уровней
                                IsBuild = true;
                                builder();
                            }
                        }
                    }
    
                }
                //Если мы уже в конструкторе
                else
                {
                    foreach (TouchLocation touchLocation in touchLocations)
                    {
                        if (touchLocation.State == TouchLocationState.Pressed)
                        {
                            //Ищем попадание точки касания в один из элементов управления
                            //Стрелка вверх
                            if (MenuSelect(keyUp, touchLocation.Position))
                            {
                                if (PositionBuild.Y > 0) PositionBuild.Y--;
                            }
                            //Стрелка вниз
                            if (MenuSelect(keyDown, touchLocation.Position))
                            {
                                if (PositionBuild.Y < 49) PositionBuild.Y++;
                            }
                            //Стрелка влево
                            if (MenuSelect(keyLeft, touchLocation.Position))
                            {
                                if (PositionBuild.X > 0) PositionBuild.X--;
                            }
                            //Стрелка вправо
                            if (MenuSelect(keyRight, touchLocation.Position))
                            {
                                if (PositionBuild.X < 6) PositionBuild.X++;
                            }
                            //Кнопка изменения блока
                            if (MenuSelect(keyChange, touchLocation.Position))
                            {
                                //Всего три вида блоков
                                //Циклически изменяем вид блока при нажатии
                                typeOfBuildBlock++;
                                if (typeOfBuildBlock == 3) typeOfBuildBlock = 0;
                            }
                            //Кнопка подтверждения
                            if (MenuSelect(keyOK, touchLocation.Position))
                            {
                                //Записываем данные о позиции блока в соответствующую ячейку массива
                                //хранящего пользовательский уровень
                                UserLayer[(int)PositionBuild.Y, (int)PositionBuild.X] = typeOfBuildBlock;
                            }
                            //очищаем массив компонентов
                            Components.Clear();
                            //выводим пользовательский уровень
                            AddSprites(UserLayer);
                            //уничтожаем указатель и изменяем его в соответствии с типом выбранного блока
                            BrickBuilder br = null;
                            if (typeOfBuildBlock == 1)
                                br = new BrickBuilder(this, ref txtBrBuild1, PositionBuild, recBrick, 64, 16);
                            if (typeOfBuildBlock == 2)
                                br = new BrickBuilder(this, ref txtBrBuild2, PositionBuild, recBrick, 64, 16);
                            if (typeOfBuildBlock == 0)
                                br = new BrickBuilder(this, ref txtBrBuild0, PositionBuild, recBrick, 64, 16);
                            Components.Add(br);
                            //Если нажата кнопка перехода в игровой режим
                            if (MenuSelect(keyGameOrBuild, touchLocation.Position))
                            {
                                //укажем, что режим конструирования не активен
                                IsBuild = false;
                                Components.Clear();
                                AddSprites(UserLayer);
                                addBallAndBAts();
                                //запустим игру с уровнем -1
                                currentLayer = -1;
    
                            }
    
                        }
                    }
                }
    
                //Если находимся в режиме игры
                //проверим - завершен ли текущий уровень
                //признак завершения уровня
    
                if (!IsBuild)
                {
                    IsLayerCompleted();
                }
    
                base.Update(gameTime);
            }
            //Процедура старта конструктора уровней
            //очищаем набор компонентов
            //Создаем и выводим указатель
            public void builder()
            {
                Components.Clear();
                BrickBuilder br = new BrickBuilder(this, ref txtBrBuild1, PositionBuild, recBrick, 64, 16);
                Components.Add(br);
            }
    
            //Процедура проверки на завершенность
            //текущего уровня
            //Если на поле отсутствует мяч - игра начинается сначала
            //если на поле нет ни одного блока типа Brick1 - 
            //переход на следующий уровень или переход на первый уровень
            //с выводом надписи "Вы выиграли"
    
            public void IsLayerCompleted()
            {
                //Предположим, что на поле нет ни одного блока
                //типа Brick1
                bool f = false;
                //Предположим, что на поле нет мяча
                bool Ball = false;
                foreach (gBaseClass spr in this.Components)
                {
                    //Если найден хотя бы один блок типа 1
                    //установим F в True
                    if (spr.GetType() == (typeof(Brick1)))
                    {
                        f = true;
                    }
                    //Если найден мяч - установим Ball в True
                    if (spr.GetType() == (typeof(Ball)))
                    {
                        Ball = true;
                    }
                }
                //Если мяч не обнаружен
                //игра начинается сначала
                if (!Ball)
                {
                    Components.Clear();
                    currentLayer = 1;
                    AddSprites(Layer1);
                    addBallAndBAts();
                }
                //иначе
                else
                {
                    //Если не было найдено ни одного блока
                    //типа Brick1
                    if (!f)
                    {
                        //Очистим набор компонентов
                        Components.Clear();
                        //Увеличим номер уровня
                        currentLayer++;
                        //в зависимости от номера
                        //запустим соответствующий уровень
                        if (currentLayer == 2)
                        {
                            AddSprites(Layer2);
                            addBallAndBAts();
                        }
                        if (currentLayer == 3)
                        {
                            AddSprites(Layer3);
                            addBallAndBAts();
                        }
                        //4 - если был пройден 3-й уровень
                        //0 - при прохождении пользовательского уровня
                        if (currentLayer == 4 | currentLayer == 0)
                        {
                            currentLayer = 1;
                            AddSprites(Layer1);
                            addBallAndBAts();
                            //Включим вибраацию на 0,1 секунды
                            VibrateController.Default.Start(TimeSpan.FromMilliseconds(100));
    
                        }
                    }
                }
            }
    
            /// <summary>
            /// Вызывается, когда игра отрисовывается.
            /// </summary>
            /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
            protected override void Draw(GameTime gameTime)
            {
                spriteBatch.Begin();
                //выведем фоновое изображение
                spriteBatch.Draw(txtBack, recBack, Color.White);
                //Выведем игровые объекты
                base.Draw(gameTime);
                //Если в режиме конструктора - выведем экранные элементы управления
    
                if (IsBuild)
                {
                    //для управления конструированием
                    spriteBatch.Draw(txtArrows, new Rectangle(90, 600, 300, 200), Color.White);
                    //для перехода в игровой режим
                    spriteBatch.Draw(txtToGame, new Rectangle(0, 0, 480, 100), Color.White);
                }
                else
                {
                    //Если в игровом режиме - выведем элемент управления
                    //для перехода в конструктор
                    spriteBatch.Draw(txtToConstr, new Rectangle(0, 0, 480, 100), Color.White);
                }
                spriteBatch.End();
    
    
            }
        }
    }
    

    В листинге 21.2 приведен код класса gBaseClass.

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
    
        public class gBaseClass : Microsoft.Xna.Framework.DrawableGameComponent
        {
            Texture2D sprTexture;
            public Vector2 sprPosition;
            public Rectangle sprRectangle;
            public gBaseClass(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int X_Coord, int Y_Coord)
                : base(game)
            {
                sprTexture = _sprTexture;
                //Здесь производится перевод индекса элемента массива
                //в координаты на игровом экране
                //Так как спрайты имеют различные размеры, для их размещения на экране
                //используются коэффициенты X_Coord и Y_Coord
                sprPosition.X = _sprPosition.X * X_Coord+16;
                sprPosition.Y = _sprPosition.Y * Y_Coord;
                sprRectangle = _sprRectangle;
            }
            public override void Draw(GameTime gameTime)
            {
                SpriteBatch sprBatch =
                    (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
                sprBatch.Draw(sprTexture, sprPosition, Color.White);
                base.Draw(gameTime);
            }
        }
    }

    В листинге 21.3 приведен код класса Bat. Он занимается обработкой перемещений биты по горизонтали и контролем за пересечением битой левой и правой границ экрана.

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    using Microsoft.Xna.Framework.Input.Touch;
    namespace P15_1
    {
    
        public class Bat : gBaseClass
        {
            Rectangle scrBounds;
            //True для движения биты влево и False - вправо
            public bool direction;
    
            public Bat(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
                scrBounds = new Rectangle(16, 0,
                  game.Window.ClientBounds.Width-16,
                  game.Window.ClientBounds.Height);
                direction = true;
            }
    
    
            public override void Update(GameTime gameTime)
            {
                
                TouchCollection touchLocations = TouchPanel.GetState();
                foreach (TouchLocation touchLocation in touchLocations)
                {
                    //При касании экрана или при перемещении
                    if (touchLocation.State == TouchLocationState.Pressed||touchLocation.State == TouchLocationState.Moved)
                    {
                        //Узнаем направление движения объекта
                        if (sprPosition.X+48 < touchLocation.Position.X)
                        {
                            direction = false;
                        }
                        else
                        {
                            direction = true;
                        }
                        //Установим объект, по оси Х, таким образом, чтобы
                        //точка касания приходилась на середину объекта
                        sprPosition.X = touchLocation.Position.X-48;
    
                    }
                }
                //проверка на столкновение с границами экрана
                Check();
    
                base.Update(gameTime);
            }
            //Процедура проверки столкновения с левой
            //и правой границами экрана
            void Check()
            {
                if (sprPosition.X < scrBounds.Left)
                {
                    sprPosition.X = scrBounds.Left;
    
                }
                if (sprPosition.X > scrBounds.Width - sprRectangle.Width)
                {
                    sprPosition.X = scrBounds.Width - sprRectangle.Width;
    
                }
            }
        }
    }

    В листинге 21.4 вы можете видеть код класса Ball. Этот класс отвечает за перемещение мяча и за обработку столкновений мяча и других объектов.

    using System;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
    
        public class Ball : gBaseClass
        {
            //для хранения границ экрана
            Rectangle scrBounds;
            //скорость мяча
            public Vector2 speed;
            Random rand = new Random();
            public Ball(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
                speed = new Vector2(5, -5);
                scrBounds = new Rectangle(16, 0,
                  game.Window.ClientBounds.Width - 16,
                  game.Window.ClientBounds.Height);
    
            }
    
            //Проверка на столкновение с границами экрана
            void Check()
            {
                if (sprPosition.X < scrBounds.Left)
                {
                    sprPosition.X = scrBounds.Left;
                    RandomiseSpeed();
                    speed.X *= -1;
                }
                if (sprPosition.X > scrBounds.Width - sprRectangle.Width)
                {
                    sprPosition.X = scrBounds.Width - sprRectangle.Width;
                    RandomiseSpeed();
                    speed.X *= -1;
                }
                if (sprPosition.Y < scrBounds.Top)
                {
                    sprPosition.Y = scrBounds.Top;
                    RandomiseSpeed();
                    speed.Y *= -1;
                }
                if (sprPosition.Y > scrBounds.Height - sprRectangle.Height)
                {
                    this.Dispose();
                }
    
            }
    
            //Проверка на столкновение с объектом
            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);
    
    
            }
            //Процедуры случайного изменения скорости при контакте с объектами
            //Для всех объектов - скорость может либо возрасти, либо упасть
            void RandomiseSpeed()
            {
                speed.Y += (float)((rand.NextDouble() - rand.NextDouble()) * 0.4);
                speed.X += (float)((rand.NextDouble() - rand.NextDouble()) * 0.4);
            }
            //Для объектов типа Brick1 скорость лишь растет
            void RandomiseSpeedB1()
            {
                speed.Y += (float)(0.5 * Math.Sign(speed.Y) * Math.Abs(((rand.NextDouble() - rand.NextDouble()))));
                speed.X += (float)(0.5 * Math.Sign(speed.X) * Math.Abs(((rand.NextDouble() - rand.NextDouble()))));
            }
            //Проверка на столкновения
            public void IsCollide()
            {
                gBaseClass FindObj = null;
                Bat FindedBat;
                foreach (gBaseClass spr in Game.Components)
                {
                    if (IsCollideWithObject(spr))
                    {
                        //Если столкнулись с блоком Brick1
                        if (spr.GetType() == (typeof(Brick1)))
                        {
                            //сохраним найденный объект
                            FindObj = spr;
                            //Изменим скорость
                            RandomiseSpeedB1();
                            //Обратим скорость
                            speed *= -1;
                        }
                        //Если столкнулись с блоком 2
                        //обращаем скорость
                        if (spr.GetType() == (typeof(Brick2)))
                        {
                            speed *= -1;
                        }
                        //если столкнулись с битой
                        if (spr.GetType() == (typeof(Bat)))
                        {
                            FindedBat = (Bat)spr;
                            RandomiseSpeed();
                            //Если бита двигалась влево
                            //Мяч отразится от нее влево
                            if (FindedBat.direction)
                            {
                                speed.Y = Math.Abs(speed.Y);
                                speed.X = Math.Abs(speed.X);
                                speed *= -1;
                            }
                            //Если двигалась вправо
                            //отразится вправо
                            else
                            {
                                speed.Y = Math.Abs(speed.Y);
                                speed.X = Math.Abs(speed.X);
                                speed.Y *= -1;
                            }
    
    
                        }
    
                    }
                }
                //Если был найден блок Block1
                if (FindObj != null)
                {
                    //Уничтожить его
                    FindObj.Dispose();
                    FindObj = null;
                }
    
            }
    
            public override void Update(GameTime gameTime)
            {
    
                //Переместить мяч в соответствии со скоростью
                this.sprPosition += this.speed;
                //Ограничить скорость мяча 10 пикселями
                if (speed.Y > 10) speed.Y = (float)10;
                if (speed.Y < -10) speed.Y = (float)-10;
                if (speed.X > 10) speed.X = (float)10;
                if (speed.X < -10) speed.X = (float)-10;
                //Проверить столкновение с границами
                Check();
                //проверить столкновение с объектами
                IsCollide();
                base.Update(gameTime);
            }
        }
    }

    Листинг 21.5. содержит коды классов Brick1, Brick2 и BrickBuilder. Эти классы наследованы от gBaseClass и не вносят в его функциональность никаких дополнений.

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
        public class Brick1 : gBaseClass
        {
            public Brick1(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
    
            }
        }
    }
    
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
         public class Brick2 : gBaseClass
        {
            public Brick2(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
                
            }
    
        }
    }
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
        public class BrickBuilder : gBaseClass
        {
            public BrickBuilder(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
    
            }
        }
    }

    21.2. Выводы

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

    Здесь мы храним данные по уровням в массивах. Вполне возможны и другие подходы к хранению подобных данных. Более того, если массивы или другие структуры данных, хранящие информацию об уровнях, будут занимать слишком много системной памяти, это нельзя назвать рациональным её использованием. Поэтому на практике механизм хранения информации об уровнях хотя и может основываться на массивах, выглядеть может по-другому. Например, данные по уровням хранятся в файлах и подгружаются по мере необходимости.

    21.3. Задание

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

  • Переработайте процедуры обработки столкновений мяча с блоками. Мяч должен отскакивать от блоков точно так же, как он отскакивает от границ экрана.
  • Создайте интерфейс игры – меню, систему справки.
  • Предусмотрите сохранение пользовательского уровня в файл (с помощью сериализации специально созданного объекта) и загрузку его из файла.
  • Создайте систему подсчета очков, предусмотрите сохранение данных о набранных очках в файле.
  • Улучшите процесс прохождения игры – разработайте дополнительные уровни, сделайте игровой процесс разнообразнее, разработав бонусные объекты, которые появляются при уничтожении некоторых из блоков. Игрок должен "поймать" бонусный объект битой для того, чтобы активировать соответствующий бонус.
  • 21.4. Дополнительные материалы

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

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

    Страницы:

    Цель работы: Освоить идеи разработки многоуровневых игр и конструкторов уровней

    21.1. Организация многоуровневой игры

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

    В рамках этой лабораторной работы мы создадим игру – клон популярной игры Arkanoid. Наша игра будет обладать следующим набором характеристик.

  • Имеется 2 вида блоков – один из них уничтожается при попадании в него мяча, причем, мяч отскакивает, второй – не уничтожается.
  • При уничтожении всех блоков текущего уровня осуществляется переход на следующий уровень.
  • После того, как мяч выйдет за пределы поля – игра окончится и произойдет загрузка первого уровня.
  • Игрок может играть в стандартном режиме и в режиме пользовательского уровня. Пользовательский уровень создается игроком самостоятельно с помощью конструктора уровней.
  • Здесь мы создадим каркас игры и конструктор уровней для создания пользовательских уровней.

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

    (рис 21.1) Обозреватель решений для проекта P15_1

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

    Классы Ball, Bat, Brick1, Brick2 – это игровые компоненты, соответственно, для мяча (Ball), биты (Bat), и двух видов блоков – обычного, который уничтожается при попадании в него мяча (Brick1), и неуничтожимого (Brick2).

    Класс BrickBuilder используется для создания указателя, который применяется в конструкторе уровней.

    На рис. 21.2 вы можете видеть изображения графических ресурсов, примененных в игре.

    (рис 21.2) Игровые ресурсы

    Изображения BrickBuild используются в конструкторе уровней. Указатель с изображением BrickBuild0.png очищает ячейку игрового поля, расположенную под ней. Указатель с BrickBuild1.png устанавливает в текущую позицию игрового поля объект Brick1, указатель с изображением BrickBuild2.png устанавливает в текущую ячейку объект Brick2.

    Изображение Arrows используется для управления конструированием игры. Оно выполнено полупрозрачным и в режиме конструктора выводится на экран. Изображения ToCinstr и ToGame выводятся в верхней части экрана и служат для перехода из игрового режима в режим конструктора и наоборот.

    На рис. 21.3 вы можете видеть игровой экран.

    (рис 21.3) Игровой экран

    На рис. 21.4 представлен экран в режиме конструктора уровней.

    (рис 21.4) Режим конструктора уровней

    Рассмотрим коды классов, которые используются в игре.

    В листинге 21.1 приведен код класса Game1.

    using System;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    using Microsoft.Xna.Framework.Input.Touch;
    using Microsoft.Devices;
    
    namespace P15_1
    {
        /// <summary>
        /// Это главный тип игры
        /// </summary>
        public class Game1 : Microsoft.Xna.Framework.Game
        {
            GraphicsDeviceManager graphics;
            SpriteBatch spriteBatch;
            //Текстуры
            Texture2D txtBack, txtBrick1, txtBrick2, txtBat, txtBall;
            Texture2D txtBrBuild1, txtBrBuild2, txtBrBuild0;
            Texture2D txtArrows, txtToGame, txtToConstr;
            //Массивы для хранения данных об уровнях
            public byte[,] Layer1, Layer2, Layer3, UserLayer;
            //Прямоугольник для фона
            Rectangle recBack = new Rectangle(0, 0, 480, 800);
            //Прямоугольник для "блока"
            Rectangle recBrick = new Rectangle(0, 0, 64, 16);
            //Прямоугольник для "мяча"
            Rectangle recBall = new Rectangle(0, 0, 10, 10);
            //Прямоугольник для биты
            Rectangle recBat = new Rectangle(0, 0, 96, 16);
            //Номер текущего уровня
            //1 - первый
            //2 - второй
            //3 - третий
            //-1 - пользовательский
            int currentLayer;
            //Установлена в false, если программа находится в 
            //режиме игры, при установке в True включается
            //режим конструктора
            bool IsBuild;
            //Позиция указателя в режиме конструктора
            Vector2 PositionBuild;
            //Тип блока, который будет добавлен на игровое поле
            //при поступлении соответствующей команды в режиме 
            //конструктора
            byte typeOfBuildBlock;
            //Прямоугольники для элементов управления
            Rectangle keyGameOrBuild = new Rectangle(0, 0, 480, 100);
            Rectangle keyChange = new Rectangle(90, 600, 100, 100);
            Rectangle keyUp = new Rectangle(190, 600, 100, 100);
            Rectangle keyOK = new Rectangle(290, 600, 100, 100);
            Rectangle keyLeft = new Rectangle(90, 700, 100, 100);
            Rectangle keyDown = new Rectangle(190, 700, 100, 100);
            Rectangle keyRight = new Rectangle(290, 700, 100, 100);
    
            public Game1()
            {
                graphics = new GraphicsDeviceManager(this);
                Content.RootDirectory = "Content";
                graphics.IsFullScreen = true;
                graphics.SupportedOrientations = DisplayOrientation.Portrait;
                graphics.PreferredBackBufferHeight = 800;
                graphics.PreferredBackBufferWidth = 480;
                // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
                TargetElapsedTime = TimeSpan.FromTicks(333333);
                // Дополнительный заряд аккумулятора заблокирован.
                InactiveSleepTime = TimeSpan.FromSeconds(1);
            }
    
            /// <summary>
            /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
            /// Здесь можно запросить нужные службы и загрузить неграфический
            /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
            /// их инициализации.
            /// </summary>
            protected override void Initialize()
            {
                //Программа находится в игровом режиме
                IsBuild = false;
                //Позиция для указателя в режиме конструктора (0,0)
                PositionBuild = new Vector2(0, 0);
                //Тип блока по умолчанию в режиме конструктора - 1
                typeOfBuildBlock = 1;
                //пользовательский уровень по умолчанию пуст - ячейки
                //заполнены нулями
                UserLayer = new byte[50, 7];
                //Заполняем массивы для разных уровней
                //0 - пустая ячейка
                //1 - блок первого типа (уничтожимый)
                //2 - блок второго типа (неуничтожимый)
                Layer1 = new byte[50, 7]{
                {1,1,1,1,1,1,1},
                {0,1,0,1,0,1,0},
                {1,0,1,0,1,0,1},
                {0,1,0,1,0,1,0},
                {1,0,1,0,1,0,1},
                {0,1,0,1,0,1,0},
                {0,0,0,0,0,0,0},
                {2,0,0,2,0,0,2},
                {0,1,0,0,0,0,0},
                {0,0,1,0,0,0,1},
                {0,0,0,1,0,1,0},
                {0,0,0,0,1,0,0},
                {1,1,1,1,1,1,1},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                };
    
                Layer2 = new byte[50, 7]{
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,2,1,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                };
    
                Layer3 = new byte[50, 7]{
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,1,2,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,1,1,1,1,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,1,2,0,2,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,1,1,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                {0,0,0,0,0,0,0},
                };
    
                //Текущий уровень - 1
                currentLayer = 1;
    
    
                base.Initialize();
            }
    
            /// <summary>
            /// LoadContent будет вызываться в игре один раз; здесь загружается
            /// весь контент.
            /// </summary>
            protected override void LoadContent()
            {
                // Загружаем ресурсы
                spriteBatch = new SpriteBatch(GraphicsDevice);
                Services.AddService(typeof(SpriteBatch), spriteBatch);
                txtBack = Content.Load<Texture2D>("Back");
                txtBrick1 = Content.Load<Texture2D>("Brick1");
                txtBrick2 = Content.Load<Texture2D>("Brick2");
                txtBall = Content.Load<Texture2D>("Ball");
                txtBat = Content.Load<Texture2D>("Bat");
                txtBrBuild0 = Content.Load<Texture2D>("BrickBuild0");
                txtBrBuild1 = Content.Load<Texture2D>("BrickBuild1");
                txtBrBuild2 = Content.Load<Texture2D>("BrickBuild2");
                txtArrows = Content.Load<Texture2D>("Arrows");
                txtToGame = Content.Load<Texture2D>("ToGame");
                txtToConstr = Content.Load<Texture2D>("ToConstr");
                //Создаем уровень
                AddSprites(Layer1);
                //Добавляем биту и мяч
                addBallAndBAts();
    
            }
    
            //Процедура создания уровня
            //расставляет элементы в соответствии с
            //массивом
            void AddSprites(byte[,] RenderLayer)
            {
                //Просматриваем массив
                for (int i = 0; i < 50; i++)
                {
                    for (int j = 0; j < 7; j++)
                    {
                        if (RenderLayer[i, j] == 1)
                            Components.Add(new Brick1(this, ref txtBrick1, new Vector2(j, i), recBrick, 64, 16));
                        if (RenderLayer[i, j] == 2)
                            Components.Add(new Brick2(this, ref txtBrick2, new Vector2(j, i), recBrick, 64, 16));
                    }
                }
            }
            //Процедура для установки биты и мяча
            void addBallAndBAts()
            {
                Components.Add(new Ball(this, ref txtBall, new Vector2(300, 750), recBall, 1, 1));
                Components.Add(new Bat(this, ref txtBat, new Vector2(300, (800 - 16)), recBat, 1, 1));
            }
    
    
    
            /// <summary>
            /// UnloadContent будет вызываться в игре один раз; здесь выгружается
            /// весь контент.
            /// </summary>
            protected override void UnloadContent()
            {
                // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
            }
            //Проверка попадания касания в область, занимаемую одним из элементов управления
            private bool MenuSelect(Rectangle m, Vector2 p)
            {
                bool res = false;
                if (p.X > m.X  p.X < m.X + m.Width  p.Y > m.Y  p.Y < m.Y + m.Height)
                {
                    res = true;
                }
    
                return res;
            }
    
            /// <summary>
            /// Позволяет игре запускать логику обновления мира,
            /// проверки столкновений, получения ввода и воспроизведения звуков.
            /// </summary>
            /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
            protected override void Update(GameTime gameTime)
            {
                // Позволяет выйти из игры
                if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                    this.Exit();
    
                TouchCollection touchLocations = TouchPanel.GetState();
                //Если находимся в игровом режиме
                if (!IsBuild)
                {
                    foreach (TouchLocation touchLocation in touchLocations)
                    {
                        if (touchLocation.State == TouchLocationState.Pressed)
                        {
                            //Если пользователь коснулся кнопки для перехода в режим конструктора
                            if (MenuSelect(keyGameOrBuild, touchLocation.Position))
                            {
                                //Включаем конструктор уровней
                                IsBuild = true;
                                builder();
                            }
                        }
                    }
    
                }
                //Если мы уже в конструкторе
                else
                {
                    foreach (TouchLocation touchLocation in touchLocations)
                    {
                        if (touchLocation.State == TouchLocationState.Pressed)
                        {
                            //Ищем попадание точки касания в один из элементов управления
                            //Стрелка вверх
                            if (MenuSelect(keyUp, touchLocation.Position))
                            {
                                if (PositionBuild.Y > 0) PositionBuild.Y--;
                            }
                            //Стрелка вниз
                            if (MenuSelect(keyDown, touchLocation.Position))
                            {
                                if (PositionBuild.Y < 49) PositionBuild.Y++;
                            }
                            //Стрелка влево
                            if (MenuSelect(keyLeft, touchLocation.Position))
                            {
                                if (PositionBuild.X > 0) PositionBuild.X--;
                            }
                            //Стрелка вправо
                            if (MenuSelect(keyRight, touchLocation.Position))
                            {
                                if (PositionBuild.X < 6) PositionBuild.X++;
                            }
                            //Кнопка изменения блока
                            if (MenuSelect(keyChange, touchLocation.Position))
                            {
                                //Всего три вида блоков
                                //Циклически изменяем вид блока при нажатии
                                typeOfBuildBlock++;
                                if (typeOfBuildBlock == 3) typeOfBuildBlock = 0;
                            }
                            //Кнопка подтверждения
                            if (MenuSelect(keyOK, touchLocation.Position))
                            {
                                //Записываем данные о позиции блока в соответствующую ячейку массива
                                //хранящего пользовательский уровень
                                UserLayer[(int)PositionBuild.Y, (int)PositionBuild.X] = typeOfBuildBlock;
                            }
                            //очищаем массив компонентов
                            Components.Clear();
                            //выводим пользовательский уровень
                            AddSprites(UserLayer);
                            //уничтожаем указатель и изменяем его в соответствии с типом выбранного блока
                            BrickBuilder br = null;
                            if (typeOfBuildBlock == 1)
                                br = new BrickBuilder(this, ref txtBrBuild1, PositionBuild, recBrick, 64, 16);
                            if (typeOfBuildBlock == 2)
                                br = new BrickBuilder(this, ref txtBrBuild2, PositionBuild, recBrick, 64, 16);
                            if (typeOfBuildBlock == 0)
                                br = new BrickBuilder(this, ref txtBrBuild0, PositionBuild, recBrick, 64, 16);
                            Components.Add(br);
                            //Если нажата кнопка перехода в игровой режим
                            if (MenuSelect(keyGameOrBuild, touchLocation.Position))
                            {
                                //укажем, что режим конструирования не активен
                                IsBuild = false;
                                Components.Clear();
                                AddSprites(UserLayer);
                                addBallAndBAts();
                                //запустим игру с уровнем -1
                                currentLayer = -1;
    
                            }
    
                        }
                    }
                }
    
                //Если находимся в режиме игры
                //проверим - завершен ли текущий уровень
                //признак завершения уровня
    
                if (!IsBuild)
                {
                    IsLayerCompleted();
                }
    
                base.Update(gameTime);
            }
            //Процедура старта конструктора уровней
            //очищаем набор компонентов
            //Создаем и выводим указатель
            public void builder()
            {
                Components.Clear();
                BrickBuilder br = new BrickBuilder(this, ref txtBrBuild1, PositionBuild, recBrick, 64, 16);
                Components.Add(br);
            }
    
            //Процедура проверки на завершенность
            //текущего уровня
            //Если на поле отсутствует мяч - игра начинается сначала
            //если на поле нет ни одного блока типа Brick1 - 
            //переход на следующий уровень или переход на первый уровень
            //с выводом надписи "Вы выиграли"
    
            public void IsLayerCompleted()
            {
                //Предположим, что на поле нет ни одного блока
                //типа Brick1
                bool f = false;
                //Предположим, что на поле нет мяча
                bool Ball = false;
                foreach (gBaseClass spr in this.Components)
                {
                    //Если найден хотя бы один блок типа 1
                    //установим F в True
                    if (spr.GetType() == (typeof(Brick1)))
                    {
                        f = true;
                    }
                    //Если найден мяч - установим Ball в True
                    if (spr.GetType() == (typeof(Ball)))
                    {
                        Ball = true;
                    }
                }
                //Если мяч не обнаружен
                //игра начинается сначала
                if (!Ball)
                {
                    Components.Clear();
                    currentLayer = 1;
                    AddSprites(Layer1);
                    addBallAndBAts();
                }
                //иначе
                else
                {
                    //Если не было найдено ни одного блока
                    //типа Brick1
                    if (!f)
                    {
                        //Очистим набор компонентов
                        Components.Clear();
                        //Увеличим номер уровня
                        currentLayer++;
                        //в зависимости от номера
                        //запустим соответствующий уровень
                        if (currentLayer == 2)
                        {
                            AddSprites(Layer2);
                            addBallAndBAts();
                        }
                        if (currentLayer == 3)
                        {
                            AddSprites(Layer3);
                            addBallAndBAts();
                        }
                        //4 - если был пройден 3-й уровень
                        //0 - при прохождении пользовательского уровня
                        if (currentLayer == 4 | currentLayer == 0)
                        {
                            currentLayer = 1;
                            AddSprites(Layer1);
                            addBallAndBAts();
                            //Включим вибраацию на 0,1 секунды
                            VibrateController.Default.Start(TimeSpan.FromMilliseconds(100));
    
                        }
                    }
                }
            }
    
            /// <summary>
            /// Вызывается, когда игра отрисовывается.
            /// </summary>
            /// <param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
            protected override void Draw(GameTime gameTime)
            {
                spriteBatch.Begin();
                //выведем фоновое изображение
                spriteBatch.Draw(txtBack, recBack, Color.White);
                //Выведем игровые объекты
                base.Draw(gameTime);
                //Если в режиме конструктора - выведем экранные элементы управления
    
                if (IsBuild)
                {
                    //для управления конструированием
                    spriteBatch.Draw(txtArrows, new Rectangle(90, 600, 300, 200), Color.White);
                    //для перехода в игровой режим
                    spriteBatch.Draw(txtToGame, new Rectangle(0, 0, 480, 100), Color.White);
                }
                else
                {
                    //Если в игровом режиме - выведем элемент управления
                    //для перехода в конструктор
                    spriteBatch.Draw(txtToConstr, new Rectangle(0, 0, 480, 100), Color.White);
                }
                spriteBatch.End();
    
    
            }
        }
    }
    

    В листинге 21.2 приведен код класса gBaseClass.

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
    
        public class gBaseClass : Microsoft.Xna.Framework.DrawableGameComponent
        {
            Texture2D sprTexture;
            public Vector2 sprPosition;
            public Rectangle sprRectangle;
            public gBaseClass(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int X_Coord, int Y_Coord)
                : base(game)
            {
                sprTexture = _sprTexture;
                //Здесь производится перевод индекса элемента массива
                //в координаты на игровом экране
                //Так как спрайты имеют различные размеры, для их размещения на экране
                //используются коэффициенты X_Coord и Y_Coord
                sprPosition.X = _sprPosition.X * X_Coord+16;
                sprPosition.Y = _sprPosition.Y * Y_Coord;
                sprRectangle = _sprRectangle;
            }
            public override void Draw(GameTime gameTime)
            {
                SpriteBatch sprBatch =
                    (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));
                sprBatch.Draw(sprTexture, sprPosition, Color.White);
                base.Draw(gameTime);
            }
        }
    }

    В листинге 21.3 приведен код класса Bat. Он занимается обработкой перемещений биты по горизонтали и контролем за пересечением битой левой и правой границ экрана.

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    using Microsoft.Xna.Framework.Input;
    using Microsoft.Xna.Framework.Input.Touch;
    namespace P15_1
    {
    
        public class Bat : gBaseClass
        {
            Rectangle scrBounds;
            //True для движения биты влево и False - вправо
            public bool direction;
    
            public Bat(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
                scrBounds = new Rectangle(16, 0,
                  game.Window.ClientBounds.Width-16,
                  game.Window.ClientBounds.Height);
                direction = true;
            }
    
    
            public override void Update(GameTime gameTime)
            {
                
                TouchCollection touchLocations = TouchPanel.GetState();
                foreach (TouchLocation touchLocation in touchLocations)
                {
                    //При касании экрана или при перемещении
                    if (touchLocation.State == TouchLocationState.Pressed||touchLocation.State == TouchLocationState.Moved)
                    {
                        //Узнаем направление движения объекта
                        if (sprPosition.X+48 < touchLocation.Position.X)
                        {
                            direction = false;
                        }
                        else
                        {
                            direction = true;
                        }
                        //Установим объект, по оси Х, таким образом, чтобы
                        //точка касания приходилась на середину объекта
                        sprPosition.X = touchLocation.Position.X-48;
    
                    }
                }
                //проверка на столкновение с границами экрана
                Check();
    
                base.Update(gameTime);
            }
            //Процедура проверки столкновения с левой
            //и правой границами экрана
            void Check()
            {
                if (sprPosition.X < scrBounds.Left)
                {
                    sprPosition.X = scrBounds.Left;
    
                }
                if (sprPosition.X > scrBounds.Width - sprRectangle.Width)
                {
                    sprPosition.X = scrBounds.Width - sprRectangle.Width;
    
                }
            }
        }
    }

    В листинге 21.4 вы можете видеть код класса Ball. Этот класс отвечает за перемещение мяча и за обработку столкновений мяча и других объектов.

    using System;
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
    
        public class Ball : gBaseClass
        {
            //для хранения границ экрана
            Rectangle scrBounds;
            //скорость мяча
            public Vector2 speed;
            Random rand = new Random();
            public Ball(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
                speed = new Vector2(5, -5);
                scrBounds = new Rectangle(16, 0,
                  game.Window.ClientBounds.Width - 16,
                  game.Window.ClientBounds.Height);
    
            }
    
            //Проверка на столкновение с границами экрана
            void Check()
            {
                if (sprPosition.X < scrBounds.Left)
                {
                    sprPosition.X = scrBounds.Left;
                    RandomiseSpeed();
                    speed.X *= -1;
                }
                if (sprPosition.X > scrBounds.Width - sprRectangle.Width)
                {
                    sprPosition.X = scrBounds.Width - sprRectangle.Width;
                    RandomiseSpeed();
                    speed.X *= -1;
                }
                if (sprPosition.Y < scrBounds.Top)
                {
                    sprPosition.Y = scrBounds.Top;
                    RandomiseSpeed();
                    speed.Y *= -1;
                }
                if (sprPosition.Y > scrBounds.Height - sprRectangle.Height)
                {
                    this.Dispose();
                }
    
            }
    
            //Проверка на столкновение с объектом
            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);
    
    
            }
            //Процедуры случайного изменения скорости при контакте с объектами
            //Для всех объектов - скорость может либо возрасти, либо упасть
            void RandomiseSpeed()
            {
                speed.Y += (float)((rand.NextDouble() - rand.NextDouble()) * 0.4);
                speed.X += (float)((rand.NextDouble() - rand.NextDouble()) * 0.4);
            }
            //Для объектов типа Brick1 скорость лишь растет
            void RandomiseSpeedB1()
            {
                speed.Y += (float)(0.5 * Math.Sign(speed.Y) * Math.Abs(((rand.NextDouble() - rand.NextDouble()))));
                speed.X += (float)(0.5 * Math.Sign(speed.X) * Math.Abs(((rand.NextDouble() - rand.NextDouble()))));
            }
            //Проверка на столкновения
            public void IsCollide()
            {
                gBaseClass FindObj = null;
                Bat FindedBat;
                foreach (gBaseClass spr in Game.Components)
                {
                    if (IsCollideWithObject(spr))
                    {
                        //Если столкнулись с блоком Brick1
                        if (spr.GetType() == (typeof(Brick1)))
                        {
                            //сохраним найденный объект
                            FindObj = spr;
                            //Изменим скорость
                            RandomiseSpeedB1();
                            //Обратим скорость
                            speed *= -1;
                        }
                        //Если столкнулись с блоком 2
                        //обращаем скорость
                        if (spr.GetType() == (typeof(Brick2)))
                        {
                            speed *= -1;
                        }
                        //если столкнулись с битой
                        if (spr.GetType() == (typeof(Bat)))
                        {
                            FindedBat = (Bat)spr;
                            RandomiseSpeed();
                            //Если бита двигалась влево
                            //Мяч отразится от нее влево
                            if (FindedBat.direction)
                            {
                                speed.Y = Math.Abs(speed.Y);
                                speed.X = Math.Abs(speed.X);
                                speed *= -1;
                            }
                            //Если двигалась вправо
                            //отразится вправо
                            else
                            {
                                speed.Y = Math.Abs(speed.Y);
                                speed.X = Math.Abs(speed.X);
                                speed.Y *= -1;
                            }
    
    
                        }
    
                    }
                }
                //Если был найден блок Block1
                if (FindObj != null)
                {
                    //Уничтожить его
                    FindObj.Dispose();
                    FindObj = null;
                }
    
            }
    
            public override void Update(GameTime gameTime)
            {
    
                //Переместить мяч в соответствии со скоростью
                this.sprPosition += this.speed;
                //Ограничить скорость мяча 10 пикселями
                if (speed.Y > 10) speed.Y = (float)10;
                if (speed.Y < -10) speed.Y = (float)-10;
                if (speed.X > 10) speed.X = (float)10;
                if (speed.X < -10) speed.X = (float)-10;
                //Проверить столкновение с границами
                Check();
                //проверить столкновение с объектами
                IsCollide();
                base.Update(gameTime);
            }
        }
    }

    Листинг 21.5. содержит коды классов Brick1, Brick2 и BrickBuilder. Эти классы наследованы от gBaseClass и не вносят в его функциональность никаких дополнений.

    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
        public class Brick1 : gBaseClass
        {
            public Brick1(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
    
            }
        }
    }
    
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
         public class Brick2 : gBaseClass
        {
            public Brick2(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
                
            }
    
        }
    }
    using Microsoft.Xna.Framework;
    using Microsoft.Xna.Framework.Graphics;
    
    namespace P15_1
    {
        public class BrickBuilder : gBaseClass
        {
            public BrickBuilder(Game game, ref Texture2D _sprTexture,
                Vector2 _sprPosition, Rectangle _sprRectangle, int x_c, int y_c)
                : base(game, ref _sprTexture, _sprPosition, _sprRectangle, x_c, y_c)
            {
    
            }
        }
    }

    21.2. Выводы

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

    Здесь мы храним данные по уровням в массивах. Вполне возможны и другие подходы к хранению подобных данных. Более того, если массивы или другие структуры данных, хранящие информацию об уровнях, будут занимать слишком много системной памяти, это нельзя назвать рациональным её использованием. Поэтому на практике механизм хранения информации об уровнях хотя и может основываться на массивах, выглядеть может по-другому. Например, данные по уровням хранятся в файлах и подгружаются по мере необходимости.

    21.3. Задание

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

  • Переработайте процедуры обработки столкновений мяча с блоками. Мяч должен отскакивать от блоков точно так же, как он отскакивает от границ экрана.
  • Создайте интерфейс игры – меню, систему справки.
  • Предусмотрите сохранение пользовательского уровня в файл (с помощью сериализации специально созданного объекта) и загрузку его из файла.
  • Создайте систему подсчета очков, предусмотрите сохранение данных о набранных очках в файле.
  • Улучшите процесс прохождения игры – разработайте дополнительные уровни, сделайте игровой процесс разнообразнее, разработав бонусные объекты, которые появляются при уничтожении некоторых из блоков. Игрок должен "поймать" бонусный объект битой для того, чтобы активировать соответствующий бонус.
  • 21.4. Дополнительные материалы

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

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

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