Программируем “морской бой” на XNA для Windows Phone 7

Как вы, возможно, слышали – очень скоро выйдет новое поколение смартфонов на платформе Microsoft Windows Phone 7. Под этот телефон можно будет разрабатывать программы на двух технологиях: Silverlight и XNA. Сегодня мы посмотрим, как можно создавать приложения, используя XNA, на примере простой компьютерной игры – морского боя. Если вам лень читать эту статью, и вы предпочитаете посмотреть процесс на видео – вам сюда.

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

Для начала – несколько слов об XNA. XNA – это набор библиотек, работающий поверх Microsoft .NET, причем как на телефоне, так и на других устройствах: полноценном персональном компьютере, XBox 360 и Zune. Причем код игры для всех этих устройств может отличаться весьма незначительно – учитывая различие способов управления игрой и разное разрешение экранов. Остальные аспекты – вывод графики, звука, сохранение игры и т.д. – максимально унифицированы для всех устройств. Кроме того, XNA работает поверх доступной платформы .NET (это полноценная .NET 4.0 на компьютере, и .NET Compact Framework на других устройствах), поэтому вы можете использовать и другие возможности платформы (например, средства сетевого взаимодействия). Теоретически, XNA может использоваться не только для создания игр, но и для более широкого круга динамичных богатых графических приложений – например, для научной визуализации.

Архитектура XNA-приложения весьма отличается от классического Windows или Web-приложения. В нем не используется модель событий, поскольку она не очень подходит для решения задач реального времени – а ведь нам хочется, чтобы игра разворачивалась перед нашими глазами именно в реальном времени, со скоростью не менее, чем 25 кадров в секунду! Поэтому игра имеет весьма простую программную модель – цикл игры. В начале игры (или игрового уровня) вызывается специальная функция LoadContent для загрузки основных ресурсов (графических и звуковых элементов), затем в цикле попеременно вызываются методы Update (для обновления состояния игры в зависимости от действия пользователя с устройствами ввода) и Draw (для отрисовки состояния игры на экране). Таким образом, для написания игры надо совершить несколько основных действий:

  1. Создать графические и звуковые элементы оформления игры и поместить их в проект. Для создания графических элементов хорошо подойдут инструменты Microsoft Expression. Графические элементы могут быть как двумерными спрайтами, так и трёхмерными моделями.
  2. Описать переменные для хранения всех необходимых элементов и переопределить метод LoadContent для загрузки их в память.
  3. Понять, как будет выглядеть состояние игры – т.е. набор переменных и структур данных, которые будут описывать игру в каждый момент времени. Состояние может быть весьма простое (как в нашем примере - координаты корабля и снаряда), или состоять из множества независимых взаимодействующих объектов или агентов, обладающих своим интеллектом и логикой.
  4. Обработать действия пользователя с устройствами ввода и запрограммировать логику изменения состояния игры в методе Update.
  5. Запрограммировать отображение состояния на экран в методе Draw. Здесь опять же может использоваться 2D или 3D-графика, в зависимости от стиля игры.
  6. Если игра более сложная, содержит несколько уровней и т.д. – возможно будет полезно усовершенствовать объектную модель, чтобы отделить каждый уровень в отдельный класс – тогда для каждого уровня придётся частично повторять описанные выше действия
  7. Играть, наслаждаться, делиться игрой с другими (сюда входят такие шаги, как создание инсталлятора, распространение игры через Windows Mobile Marketplace, XBox Indie Games и т.д.).

imageДля нашей разработки нам потребуется Visual Studio 2010 (напоминаю, что студенты могут получить её по программе DreamSpark бесплатно) и XNA Game Studio 4.0, который входит в состав Windows Phone Developer Tools. Установив всё это, вы должны быть в состоянии создать новый проект типа Windows Phone Game (4.0) , который будет представлять собой каркас игры, содержащий описанные выше методы, и выдающий при запуске игру с пустым фиолетовым экраном. Чтобы наполнить игру содержанием, пройдёмся по описанным выше шагам (1-5, поскольку шаги 6 и 7 для простой игры не имеют смысла).

Предварительно, однако, надо сделать одно важное действие, чтобы правильно ориентировать телефон. По умолчанию экран игры может быть повёрнут вертикально или горизонтально. Если нам нужна какая-то конкретная ориентация, то в конструкторе класса Game1 можно задать желаемое разрешение экрана следующим образом:

 graphics.PreferredBackBufferHeight = 800;
graphics.PreferredBackBufferWidth = 480;

Обратите внимание – в созданном решении два проекта: собственно игра, и ресурсы игры (Content) – изображения, звуки и т.д. В самой игре есть два класса – Program.cs нужен для запуска игры, а Game1.cs содержит основную логику игры (функции LoadContent/Update/Draw), и именно его мы будем модифицировать.

Графическое содержимое в нашем случае будет состоять из трёх элементов – изображения корабля и взрыва, которые мы возьмём из коллекции clipart Microsoft Office, и изображения снаряда – красной чёрточки, которую можно нарисовать в Paint. Полученные графические файлы мы добавим в Content-проект нашей игры (используем меню Add Existing Item) – результат можно наблюдать на рисунке выше.

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

 Texture2D ship, rocket, explosion;
Vector2 ship_pos = new Vector2(100, 100);
Vector2 ship_dir = new Vector2(3, 0);
Vector2 rock_pos = Vector2.Zero;

int explode = 0;

Метод LoadContent для загрузки содержимого игры будет выглядеть совсем просто (описание функции и первая строчка были сгенерированы автоматически при создании проекта, нам необходимо было добавить лишь три строчки для загрузки описанных нами ресурсов):

 protected override void LoadContent()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    ship = Content.Load<Texture2D>("Ship");
    rocket = Content.Load<Texture2D>("Rocket");
    explosion = Content.Load<Texture2D>("Explode");
    
}

Метод отрисовки также достаточно прост:

 protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.White);
    spriteBatch.Begin();
    if (explode > 0) spriteBatch.Draw(explosion, ship_pos, Color.White);
    else spriteBatch.Draw(ship, ship_pos,Color.White);
    if (rock_pos != Vector2.Zero){ spriteBatch.Draw(rocket, rock_pos, Color.Red); }
    spriteBatch.End();
    base.Draw(gameTime);
}

Здесь следует отметить одну тонкость – при рисовании спрайтов на экране мы рисуем картинки “порциями”, называемыми SpriteBatch. Соответственно, мы открываем такую “рисовательную транзакцию” вызовом spriteBatch.Begin(), и заканчиваем вызовом spriteBatch.End(), между которыми расположены вызовы spriteBatch.Draw() или DrawString(..). В нашем случае мы рисуем либо корабль, либо картинку взрыва – в зависимости от переменной explode, а также отображаем ракету в том случае, если она выпущена и летит к кораблю – это задаётся ненулевым значением вектора координат ракеты rock_pos.

Теперь перейдём к рассмотрению метода Update. Его будем рассматривать по частям. Первая часть отвечает за отрисовку взрыва: когда переменная-флаг explode ненулевая, единственная задача нашей игры – отрисовать взрыв. Поэтому мы просто уменьшаем счетчик кадров, в течение которых показывается взрыв, а когда он достигает нулевой отметки – возвращаем корабль в исходное положение, чтобы игра началась снова:

 protected override void Update(GameTime gameTime)
{

    if (explode > 0)
    {
        explode--;
        if (explode == 0)
        {
            ship_pos.X = 0;
            ship_dir.X = 3;
        }
        base.Update(gameTime);
        return;
    }
    ....
     base.Update(gameTime);
}

Обратите внимание, что в конце метода Update вызывается метод Update базового класса.

Следующий фрагмент кода отвечает за движение корабля влево-вправо:

 ship_pos += ship_dir;
if (ship_pos.X + ship.Width >= 480 || ship_pos.X <= 0)
{
    ship_dir = -ship_dir;
}

Здесь мы в явном виде используем разрешение экрана – на практике конечно так делать не стоит, лучше задать константы вначале программы, или же использовать переменные, которые инициализируются в процессе работы игры в зависимости от конкретного устройства.

Далее будем обрабатывать действия пользователя – в нашем случае прикосновения к экрану. TouchPanel.GetState() позволяет получить состояние панели телефона, которое в свою очередь может содержать информацию о нескольких одновременных касаниях (поддержка MultiTouch). Мы будем обрабатывать лишь одно (первое) касание, и в случае, если такое касание есть – будем запускать ракету из точки, X-координата которой совпадает с касанием, а координата по вертикали – фиксирована где-то внизу экрана:

 var tc = TouchPanel.GetState();
if (tc.Count>0)
{
    rock_pos.X = tc[0].Position.X;
    rock_pos.Y = 750;
}

Далее следует код, отвечающий за движение ракеты и отработку столкновения ракеты с кораблём:

 if (rock_pos != Vector2.Zero)
{
    rock_pos += new Vector2(0, -7);
    if (rock_pos.Y >= 0 && rock_pos.Y <= ship_pos.Y + ship.Height &&
        rock_pos.X >= ship_pos.X && rock_pos.X <= ship_pos.X + ship.Width)
    {
        explode = 20;
        ship_pos.X = rock_pos.X - explosion.Width / 2;
        rock_pos = Vector2.Zero;
    }
    if (rock_pos.Y == 0) rock_pos = Vector2.Zero;
}

Для движения ракеты мы просто прибавляем на каждом цикле игры значение скорости в виде двумерного вектора. Столкновение определяется “вручную” по координатам (для более сложных фигур имеет смысл использовать другие функции распознавания пересечений из XNA) – в случае поражения устанавливается переменная explode, что означает, что следующие несколько кадров вместо корабля будет показан взрыв. Если же ракета достигает верхней границы экрана – её движение прекращается (устанавливаются нулевые координаты).

Вот и всё, что необходимо для написания простейшей игры. Чтобы сделать её более привлекательной, стоит нарисовать красивую графическую подложку, поверх которой будет разворачиваться стрельба – для этого достаточно рисовать соответствующее изображение в начале spriteBatch, чтобы все все дальнейшие объекты отрисовывались поверх картинки. Также можно добавить счётчик попаданий – при этом для отрисовки строки надо будет использовать метод spriteBatch.DrawString, а шрифт, которым будет выводиться строка, надо будет поместить в ресурсы проекта и загрузить в методе LoadContent.

Чтобы более детально разобраться в программировании игр на XNA, в том числе для телефона Windows Phone 7, хочу рекомендовать следующие ресурсы:

Также процесс создания описанной выше игры показан на видео. Презентацию можно посмотреть на SlideShare, а задать вопросы можно в твиттере.

WPMB.zip