Учимся программировать игры на XNA для Windows Phone 7 «Mango» — начало
В свете недавнего анонса HTC о скором появлении телефонов на Windows Phone 7 «Mango» на российском рынке, особую актуальность приобретает разработка приложений для Windows Phone — ведь именно сейчас есть возможность насытить Windows Phone Marketplace приложениями, близкими нашему русскому сердцу. Это одна из причин, по которой мы сегодня (5 сентября) проводим Windоws Phone 7 Camp, и призываем вас приходить, смотреть онлайн-трансляцию и браться за Visual Studio прямо сейчас.
На этом мероприятии я только что рассказал про программирование игр на XNA для телефона, и в этой связи хотел бы посвятить этому несколько статей на хабре. В сегодняшней статье я покажу вам, как написать простую игру «морской бой» — а в следующих статьях мы добавим в неё звук, интеграцию XNA с Silverlight, использование сенсоров (акселерометра) и т.д. Для любителей смотреть видео — процесс написания игры также описан в этом скринкасте.
Морской бой, который мы будем писать — это не та игра, в которую многие играли в детстве на бумаге в клеточку. Это игра, которая была доступна в игровых автоматах, где надо было попасть в плывущий корабль несколькими выстрелами. В нашей игре также будет корабль, плавающий в верхней части экрана, и снаряды, которые можно будет выпускать из нижней части экрана путём прикосновения к нему.
Для начала – несколько слов об XNA. XNA – это набор библиотек, работающий поверх Microsoft .NET, причем как на телефоне, так и на других устройствах: полноценном персональном компьютере, XBox 360 и Zune. Причем код игры для всех этих устройств может отличаться весьма незначительно – учитывая различие способов управления игрой и разное разрешение экранов. Остальные аспекты – вывод графики, звука, сохранение игры и т.д. – максимально унифицированы для всех устройств. Кроме того, XNA работает поверх доступной платформы .NET (это полноценная .NET 4.0 на компьютере, и .NET Compact Framework на других устройствах), поэтому вы можете использовать и другие возможности платформы (например, средства сетевого взаимодействия). Теоретически, XNA может использоваться не только для создания игр, но и для более широкого круга динамичных богатых графических приложений – например, для научной визуализации.
Архитектура XNA-приложения весьма отличается от классического Windows или Web-приложения. В нем не используется модель событий, поскольку она не очень подходит для решения задач реального времени – а ведь нам хочется, чтобы игра разворачивалась перед нашими глазами именно в реальном времени, со скоростью не менее, чем 25 кадров в секунду! Поэтому игра имеет весьма простую программную модель – цикл игры. В начале игры (или игрового уровня) вызывается специальная функция LoadContent для загрузки основных ресурсов (графических и звуковых элементов), затем в цикле попеременно вызываются методы Update (для обновления состояния игры в зависимости от действия пользователя с устройствами ввода) и Draw (для отрисовки состояния игры на экране). Таким образом, для написания игры надо совершить несколько основных действий:
- Создать графические и звуковые элементы оформления игры и поместить их в проект. Для создания графических элементов хорошо подойдут инструменты Microsoft Expression. Графические элементы могут быть как двумерными спрайтами, так и трёхмерными моделями.
- Описать переменные для хранения всех необходимых элементов и переопределить метод LoadContent для загрузки их в память.
- Понять, как будет выглядеть состояние игры – т.е. набор переменных и структур данных, которые будут описывать игру в каждый момент времени. Состояние может быть весьма простое (как в нашем примере — координаты корабля и снаряда), или состоять из множества независимых взаимодействующих объектов или агентов, обладающих своим интеллектом и логикой.
- Обработать действия пользователя с устройствами ввода и запрограммировать логику изменения состояния игры в методе Update.
- Запрограммировать отображение состояния на экран в методе Draw. Здесь опять же может использоваться 2D или 3D-графика, в зависимости от стиля игры.
- Если игра более сложная, содержит несколько уровней и т.д. – возможно будет полезно усовершенствовать объектную модель, чтобы отделить каждый уровень в отдельный класс – тогда для каждого уровня придётся частично повторять описанные выше действия
- Играть, наслаждаться, делиться игрой с другими (сюда входят такие шаги, как создание инсталлятора, распространение игры через Windows Mobile Marketplace, XBox Indie Games и т.д.).
Для нашей разработки нам потребуется Visual Studio 2010 (напоминаю, что студенты могут получить её по программе DreamSpark бесплатно) и XNA Game Studio, которая входит в состав Windows Phone Developer Tools. Установив всё это, вы должны быть в состоянии создать новый проект типа Windows Phone Game (4.0), который будет представлять собой каркас игры, содержащий описанные выше методы, и выдающий при запуске игру с пустым фиолетовым экраном. Чтобы наполнить игру содержанием, пройдёмся по описанным выше шагам (1-5, поскольку шаги 6 и 7 для простой игры не имеют смысла). Когда Visual Studio спросит вас, под какую версию телефона — 7.0 или 7.1 Mango — следует разрабатывать игру, смело выбирайте 7.1 — так вам будут доступны новые возможности, такие, как дополнительные сенсоры, быстрое переключение приложений и т.д.
Начнем с того, что сделаем одно важное действие, чтобы правильно ориентировать телефон. По умолчанию экран игры может быть повёрнут вертикально или горизонтально. Если нам нужна какая-то конкретная ориентация, то в конструкторе класса Game1 можно задать желаемое разрешение экрана следующим образом:
graphics.PreferredBackBufferWidth = 480;
graphics.PreferredBackBufferHeight = 800;
Обратите внимание – в созданном решении два проекта: собственно игра, и ресурсы игры (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, что означает, что следующие несколько кадров вместо корабля будет показан взрыв. Если же ракета достигает верхней границы экрана – её движение прекращается (устанавливаются нулевые координаты).
Для придания игре окончательно правдоподобности, имеет смысл зеркально отображать изображение корабля в том случае, когда он плывет влево (чтобы он не плыл «задом»). Для этого слегка изменим код для отображения корабля, добавив возможность зеркального отражения:
if (explode == 0)
{
spriteBatch.Draw(ship, ship_pos, null, Color.White,0f,
Vector2.Zero,1.0f,
ship_dir.X>0 ? SpriteEffects.FlipHorizontally : SpriteEffects.None,0f);
}
else spriteBatch.Draw(explosion, ship_pos, Color.White);
Вот и всё, что необходимо для написания простейшей игры. Чтобы сделать её более привлекательной, стоит нарисовать красивую графическую подложку, поверх которой будет разворачиваться стрельба – для этого достаточно рисовать соответствующее изображение в начале spriteBatch, чтобы все все дальнейшие объекты отрисовывались поверх картинки. Также можно добавить счётчик попаданий – при этом для отрисовки строки надо будет использовать метод spriteBatch.DrawString, а шрифт, которым будет выводиться строка, надо будет поместить в ресурсы проекта и загрузить в методе LoadContent.
Вы можете скачать исходный текст всего проекта, чтобы на досуге разобраться с ним, или использовать как отправную точку для создания своих игр. Конечно, данный пример был очень простым, и не являет собой хороший пример того, как надо писать игры в действительности. Многие более реалистичные примеры вы можете найти:
В следующих выпусках я расскажу, как можно улучшить игру, добавив звук и управление кораблем при помощи акселерометра. Буду рад услышать ваши комментарии, пожелания и вопросы ниже, а также в твиттере.