Курсовые работы
Курсовая работа №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 :