| Россия, г. Санкт-Петербург |
Курсовые работы
Курсовая работа №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 :
