Опубликован: 11.01.2013 | Доступ: свободный | Студентов: 623 / 124 | Длительность: 12:06:00
Лекция 6:

Курсовые работы

< Лекция 5 || Лекция 6: 12 || Лекция 7 >

Курсовая работа №2. Игровое приложение средствами XNA

Задание

Используя технологию XNA, написать игру "Тетрис" для Windows Phone 7.

Допускается использование технологии Silverlight + XNA, если основная логика игры будет написана на XNA Framework.

Требования к работе:

  • уровни сложности (изменение скорости игры)
  • подсчет очков и сохранение лучшего результата
  • меню игры
  • звуковые эффекты
  • вывод на экран текущего состояния игры (набранные очки, уровень сложности)

Описание

Создадим новый проект XNA Game Studio 4.0 – Windows Phone Silverlight and XNA Application.

Создадим абстрактный класс Figure, которые будет содержать основные методы для будущих классов фигур. Ниже представлен данный класс с перечнем функций без их реализации. Реализацию функций рекомендуется написать самостоятельно:

 public class Figure
    {
        protected List<Vector2> Elements;
        protected int Position;

        public Figure();
        public List<Vector2> GetElements();
        public List<Vector2> GetAfterLeft();
        public List<Vector2> GetAfterRight();
        public List<Vector2> GetAfterDown();
        public List<Vector2> GetAfterRotate();
        public virtual void Rotate();
        public void MoveLeft();
        public void MoveRight();
        public void MoveDown();
    }

Функции GetAfterLeft() и аналогичные возвращают координаты фигуры после перемещения/поворота. Данные функции необходимы для того, чтобы определить, возможно ли перемещение/поворот – не выйдут ли части фигуры за границы и т.п. Поле Position хранит текущее положение фигуры. При повороте значение Position увеличивается на 1.

Создадим классы всех фигур, унаследуем их от класса Figure. Получится 7 фигур. Их условные названия: I, J, L, O, S, T, Z. Ниже представлена реализация фигуры "I". Реализацию остальных классов рекомендуется написать самостоятельно:

public class I : Figure
    {
        public I() : base()
        {
            Position1(4, -1);
        }
 
        public void Position1(float x, float y)
        {
            Elements.Clear();
            Elements.Add(new Vector2(x, y + 1));
            Elements.Add(new Vector2(x + 1, y + 1));
            Elements.Add(new Vector2(x + 2, y + 1));
            Elements.Add(new Vector2(x + 3, y + 1));
        }
 
        public void Position2(float x, float y)
        {
            Elements.Clear();
            Elements.Add(new Vector2(x + 1, y));
            Elements.Add(new Vector2(x + 1, y + 1));
            Elements.Add(new Vector2(x + 1, y + 2));
            Elements.Add(new Vector2(x + 1, y + 3));
        }
 
        public override void Rotate()
        {
            base.Rotate();
 
            if (Position > 2) Position = 1;
 
            switch (Position)
            {
                case 1:
                    Position1(Elements[0].X - 1f, Elements[0].Y);
                    break;
                case 2:
                    Position2(Elements[0].X, Elements[0].Y - 1f);
                    break;
            }
        }
    }

Создадим класс поля. Определим в нем размеры, массив кирпичиков на поле и функции проверки перемещений, контакта фигуры с кирпичиками на поле, передачи фигуры массиву Bricks и удаления линии:

 public class Field
    {
        private const int FIELD_LEFT = 90;
        private const int FIELD_TOP = 180;
        private const int FIELD_WIDTH = 12;
        private const int FIELD_HEIGHT = 24;
        private const int CELL_SIZE = 25;
 
        private bool[,] Bricks;
 
        public Field();

        //свойства
        public int Left;
        public int Top;
        public int Width;
        public int Height;
        public int CellSize;
 
        public bool[,] GetBricks();
        public bool CanLeft(List<Vector2> figure);
        public bool CanRight(List<Vector2> figure);
        public bool CanUp(List<Vector2> figure);
        public bool CanDown(List<Vector2> figure);
        public bool ContactWithBricks(List<Vector2> figure);
        public void FigureToBricks(List<Vector2> figure);
        public void RemoveLine(int index);
    }

На странице GamePage.xaml будем реализовывать основную логику игры, построенную на фреймворке XNA. В классе игры определим глобальные переменные типа Texture2D, SpriteFont и SoundEffect (для этого нужно использовать пространство имен Microsoft.Xna.Framework.Audio). Внутри метода OnNavigatedTo() будем загружать изображения, шрифты и звуковые эффекты из ресурсов (ресурсы добавляются в проект TetrisLibContent: Add – Existing Items). Также там будем получать параметр от страницы Main.xaml с номером уровня сложности (скорости игры):

 protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            ...

            imgPause = contentManager.Load<Texture2D>("pause");
            fontText = contentManager.Load<SpriteFont>("textfont");
            audioDong = contentManager.Load<SoundEffect>("dong");

            int level = 1;
            if (NavigationContext.QueryString.ContainsKey("level"))
            {
                level = int.Parse(NavigationContext.QueryString["level"].ToString());
            }
 
            StartGame(level);
            base.OnNavigatedTo(e);
        }

В функции StartGame() будем инициализировать все переменные, необходимые для игры. Также будем там генерировать первую фигуру и определять, какие типы жестов будем считывать сенсором экрана (для этого нужно использовать пространство имен Microsoft.Xna.Framework.Input.Touch):

 TouchPanel.EnabledGestures = GestureType.Tap | GestureType.Hold;

В методе OnUpdate() происходит обновление объектов, пересчет положений и обработка нажатий.

Внутри данного метода будем перехватывать нажатия. При одиночном нажатии будем перемещать фигуру просто на 1 клетку. При задержанном – будем осуществлять быстрое перемещение. Поскольку в Windows Phone 7 нельзя перехватить событие отпускания нажатия, будем использовать следующий ход. Если они одиночные (Tap) –просто перемещаем фигуру. Если нажатие с задержкой (Hold) – задаем переменной dtTouchSpeed (типа DateTime) значение текущего времени. Таким образом мы сохраняем момент, когда было перехвачено задержанное нажатие. В течение определенного времени после этого будем перехватывать обычные касания:

//обработка задержанных нажатий (по таймеру)
            if (dtTouchSpeed.AddMilliseconds(TOUCH_SPEED) > DateTime.Now)
            {
                foreach (TouchLocation location in TouchPanel.GetState())
                {
                    TouchButtons(location.Position);
                }
            }
 
            //обработка одиночных нажатий
            while (TouchPanel.IsGestureAvailable)
            {
                GestureSample gesture = TouchPanel.ReadGesture();
 
                if (gesture.GestureType == GestureType.Tap)
                {
                    TouchButtons(gesture.Position);
                }
                else
                {
                    if (gesture.GestureType == GestureType.Hold)
                    {
                        dtTouchSpeed = DateTime.Now;
                    }
                }
            }

Если, например, не использовать обработчик жестов, а только обработчик касаний – то игроку будет трудно перемещать фигуру на 1 клетку.

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

Таким же способом, как и с обработчиком нажатий, будем обрабатывать игровые такты. Переменная gameSpeed будет хранить время (в миллисекундах), через которое будет опускаться фигура. Чем выше уровень игры, тем меньше значение переменной gameSpeed:

if (dtGameSpeed.AddMilliseconds(gameSpeed) < DateTime.Now)
{
    //обработка падения
}

В обработчике падения будем проверять, может ли фигура опуститься. Если нет – значит, фигура коснулась кирпичиков поля. В этом случае ее нужно передать полю, сгенерировать новую фигуру и выполнить все необходимые проверки (на окончание игры, на заполнение линии, на увеличение уровня сложности и т.п.).

Полностью метод OnUpdate() будет выглядеть примерно так:

        private void OnUpdate(object sender, GameTimerEventArgs e)
        {
            // TODO: Add your update logic here
 
            //обработка задержанных нажатий (по таймеру)
            if (dtTouchSpeed.AddMilliseconds(TOUCH_SPEED) > DateTime.Now)
            {
                foreach (TouchLocation location in TouchPanel.GetState())
                {
                    TouchButtons(location.Position);
                }
            }
 
            //обработка одиночных нажатий
            while (TouchPanel.IsGestureAvailable)
            {
                GestureSample gesture = TouchPanel.ReadGesture();
 
                if (gesture.GestureType == GestureType.Tap)
                {
                    TouchButtons(gesture.Position);
                }
                else
                {
                    if (gesture.GestureType == GestureType.Hold)
                    {
                        dtTouchSpeed = DateTime.Now;
                    }
                }
            }
 
            if (!gamePaused)
            {
                if (dtGameSpeed.AddMilliseconds(gameSpeed) < DateTime.Now)
                {
                    if (field.CanDown(figure.GetAfterDown()))
                    {
                        figure.MoveDown();
                    }
                    else
                    {
                        field.FigureToBricks(figure.GetElements());
 
                        //убираем линии
                        int lines = 0;
                        int line = field.CheckForLine(); ;
                        while (-1 != line)
                        {
                            audioTinTinTin.Play();
                            lines++;
                            field.RemoveLine(line);
                            line = field.CheckForLine();
                        }
 
                        if (lines > 0)
                        {
                            //начислить очки
                            switch (lines)
                            {
                                case 1:
                                    scoreCurr += SCORE_LINES_1;
                                    break;
                                case 2:
                                    scoreCurr += SCORE_LINES_2;
                                    break;
                                case 3:
                                    scoreCurr += SCORE_LINES_3;
                                    break;
                                case 4:
                                    scoreCurr += SCORE_LINES_4;
                                    break;
                            }
 
                            //смена уровня сложности
                            gameLevel = gameStartLevel + (int)(scoreCurr / LEVEL_CHANGE);
 
                            if (gameLevel <= LEVEL_MAX)
                            {
                                gameSpeed = GAME_SPEED_START - (gameLevel - 1) * GAME_SPEED_DELTA;
                            }
                            else
                            {
                                //победа
                                GameOver();
                            }
                        }
 
                        //новая фигура
                        audioDong.Play();
                        figure = figureNext;
                        figureNext = GenerateRandomFigure();
 
                        if (field.ContactWithBricks(figure.GetElements()))
                        {
                            //поражение
                            GameOver();
                        }
                    }
 
                    dtGameSpeed = DateTime.Now;
                }
            }
        }

Метод OnDraw() отвечает за перерисовку игрового пространства. Метод вызывается каждый игровой такт. Внутри него с помощью уже определенной переменной spriteBatch будем рисовать фон, кнопки, игровое поле, кирпичики на поле, фигуры и строки. Всю вырисовку необходимо заключать в функциональные "скобки" spriteBatch.Begin() и spriteBatch.End().

Чтобы не загромождать метод, вынесем вырисовку отдельных частей в разные функции. Так, например, рисование фигур на поле и строк будет выглядеть следующим образом:

private void DrawFigures()
        {
            Microsoft.Xna.Framework.Rectangle rect;
            List<Vector2> elmts = figure.GetElements();
 
            for (int i = 0; i < elmts.Count; i++)
            {
                rect = new Microsoft.Xna.Framework.Rectangle();
                rect.X = field.Left + (int)elmts[i].X * field.CellSize;
                rect.Y = field.Top + (int)elmts[i].Y * field.CellSize;
                rect.Width = field.CellSize;
                rect.Height = field.CellSize;
 
                spriteBatch.Draw(imgBrick, rect, Color.White);
            }
        }

        private void DrawStrings()
        {
            float x1 = 30f;
            float y1 = 20f;
 
            Microsoft.Xna.Framework.Rectangle rect = new Microsoft.Xna.Framework.Rectangle();
            rect.X = (int)x1 - 20;
            rect.Y = (int)y1 - 10;
            rect.Width = 460;
            rect.Height = 45;
            spriteBatch.Draw(imgField, rect, Color.White);
 
            spriteBatch.DrawString(fontText, "Level: " + gameLevel.ToString(), new Vector2(x1, y1), Color.DarkBlue);
            spriteBatch.DrawString(fontText, "Score: " + scoreCurr.ToString(), new Vector2(x1 + 120f, y1), Color.DarkBlue);
            spriteBatch.DrawString(fontText, "High score: " + scoreHigh.ToString(), new Vector2(x1 + 250f, y1), Color.DarkBlue);
 
            //статус игры
            if (gamePaused)
            {
                spriteBatch.DrawString(fontText, "PAUSED", new Vector2(200, 390), Color.DarkRed);
            }
        }

Метод OnDraw() будет иметь следующий вид:

private void OnDraw(object sender, GameTimerEventArgs e)
{
    SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
 
    // TODO: Add your drawing code here
    spriteBatch.Begin();
 
    //фон
    spriteBatch.Draw(imgBack, new Vector2(0f), Color.White);
    spriteBatch.Draw(imgTitle, new Vector2(0f, 60f), Color.White);
 
    //кнопки
    DrawButtons();
 
    //поле
    DrawField();
    
    //строки
    DrawStrings();
 
    //следующая фигура
    DrawNextFigure();
 
    //мусор
    DrawBricks();
 
    //фигуры
    DrawFigures();
 
    spriteBatch.End();
}

В итоге, на эмуляторе игра будет выглядеть примерно следующим образом Рис. 6.3 :

Внешний вид конечного приложения

Рис. 6.3. Внешний вид конечного приложения

< Лекция 5 || Лекция 6: 12 || Лекция 7 >