В этой статье я хочу рассказать о своем опыте написания игры под платформу Windows Phone. Несмотря на кажущуюся простоту, путь от идеи до принятия игры в Windows Phone Store занял практически год и был полон неожиданных подводных камней — как с технической, так и с организационной сторон. Статья рассчитана на начинающих разработчиков, которые имеют представление о .NET / C#, но не пробовали делать полноценных игр.
Идея
Сложно вспомнить, как именно пришла идея написать игру. В школьной и институтской юности я развлекался написанием игрушек на конструкторах игр типа Multimedia Fusion, однако система «событие-действие» довольно неудобна для описания сложной логики. Выбор в пользу Windows Phone пал по следующим причинам:
- На тот момент (год назад) в маркете было очень мало приложений, моя игра не затеряется.
- Игры можно писать на C#, который мне хорошо знаком.
- Мой коллега DiverOfDark, с помощью которого я потом публиковал игру, купил себе Windows Phone и расхваливал его во всех красках, пророча платформе феерический успех.
Я написал другу и поведал ему, что хочу написать игру для телефона: очередной ремейк классической игровой механики тридцатипятилетней давности, порядка 30 уровней с несколькими боссами. Друг согласился заняться графикой, а я сел изучать инструментарий.
Выводы:
- Не обязательно писать игру под платформу, которой пользуешься сам.
- Люди любят классические игры. Не обязательно открывать новый жанр и изобретать радикально новые неизведанные игровые механики, однако вылизывать ее до мелочей все же необходимо.
На чем писать?
Актуальной на тот момент версией платформы была WP 7.5 Mango, позволявшая использовать и Silverligh, и XNA в одном приложении. Это оказалось очень кстати, поскольку XNA является довольно куцым фреймворком, предоставляющим только спартанский минимум функционала. Silverlight можно использовать для меню и прочих «спокойных» страниц с текстом, кнопками и полями ввода, а саму игру отрисовывать на специальной XNA-странице.
Примеры игр, которые можно скачать с сайта Microsoft и поковырять, показывали слабо подходящие для разработки нормальной игры практики. Все переменные объявлялись в качестве свойств прямо в классе сцены, и если для игры из одного задника и двух объектов это еще простительно, то при создании сколько-нибудь сложных сцен код превратится в неподдерживаемое месиво. Поиск подходящих игровых движков тоже не принес желаемых успехов: почти все движки ориентированы на 3D игры, а наша игра исключительно 2D. Так было принято решение потешить жажду велосипедостроения и написать свой небольшой движок для внутреннего пользования.
Когда движок уже подавал сознательные признаки жизни, анонс Windows Phone 8 стал для меня приятной неожиданностью, которая, однако, быстро переросла в неприятную: XNA поддерживается теперь только в режиме совместимости, а официального способа писать игры для WinPhone на C# Microsoft больше не предлагает! Однако начинать изучать новую технологию и переписывать все под нее было абсолютно нереально, и пришлось довольствоваться режимом совместимости, который, к счастью, никаких неожиданных подводных камней не приготовил.
Выводы:
- Microsoft так часто меняет свои приоритеты, что кладет как на разработчиков, так и на пользователей.
Свой 2D движок
Основной задачей движка была организация кода и предоставление ООП-каркаса, на котором можно было бы строить схему классов предметной области. Для тех, кому хочется посмотреть на код или использовать движок для своей игры — на здоровье, он доступен под лицензией MIT на гитхабе.
Базовый класс VisualObjectBase
обеспечивал наличие двух абстрактных методов Update
и Draw
, повсеместно используемых в XNA-играх, а также хранил положение объекта и позволял вычислять его размеры (bounding box).
От VisualObjectBase
наследовался DynamicObject
, добавлявший объектам такие свойства, как прозрачность, угол поворота, масштаб и их производные, а также линейную скорость. Объект наделялся списком анимированных свойств (animated property) и поведений (behaviour), о которых чуть ниже. Дальше по иерархии стоял InteractiveObject
, обеспечивающий проверку столкновений, положения объекта и нажатий на него пальцем (tap), а за ним — GameObject
, в котором появлялись спрайты. Большинство пользовательских объектов в игре являются наследниками GameObject
.
Для хранения заранее неизвестного множества однотипных объектов существует класс ObjectGroup
: он наследуется от DynamicObject
и по сути представляет собой обертку над List<VisualObjectBase>
.
На картинке приведена примерная схема классов в движке. Сплошная стрелка — «наследует», пунктирная — «использует».
Наиболее значимые проблемы, решаемые с помощью движка, рассмотрим более подробно.
Проверка столкновений
Даже такой важной вещи, как проверка столкновений, в XNA по умолчанию не оказалось. Пришлось искать компромисс между скоростью работы и точностью, который нашел отражение в следующем коде (несколько упрощен для статьи):
public override bool IsOverlappedWith(InteractableObject obj)
{
var box1 = GetBoundingBox(true);
var box2 = obj.GetBoundingBox(true);
var isect = Rectangle.Intersect(box1, box2);
if (isect.IsEmpty)
return false;
var gameObject = obj as GameObject;
// Check whether both objects are GameObjects and are neither rotated nor scaled
if (gameObject == null
|| !Scale.IsAlmost(1)
|| !obj.Scale.IsAlmost(1)
|| !Angle.IsAlmostNull()
|| !gameObject.Angle.IsAlmostNull()
)
return true;
// Convert it from screen coordinates to texture coordinates
Rectangle textureRect1 = isect, textureRect2 = isect;
textureRect1.X -= box1.X;
textureRect1.Y -= box1.Y;
textureRect2.X -= box2.X;
textureRect2.Y -= box2.Y;
var colorData1 = GetCurrentAnimation().GetTextureRegion(textureRect1);
var colorData2 = gameObject.GetCurrentAnimation().GetTextureRegion(textureRect2);
// check every 2nd pixel for the sake of speed
for (var idx = 0; idx < colorData1.Length; idx += 2)
if (colorData1[idx].A != 0 && colorData2[idx].A != 0)
return true;
return false;
}
Суть примера довольно проста: сначала проверяется пересечение прямоугольников, ограничивающих объекты. Если они не пересекаются, то объекты заведомо не могут столкнуться, в противном же случае производится попиксельное сравнение участка текстур, находящегося в месте пересечения прямоугольников. После того, как владелец HTC Mozart пожаловался на заметные лаги при проверке столкновений у многих объектов, пришлось пожертвовать точностью механизма и проверять только каждый второй пиксель.
Анимированные свойства
В реальном мире равномерных движений практически не существует: когда объект начинает двигаться, он постепенно ускоряется, а перед остановкой также постепенно тормозит. Чтобы движения объектов в игре выглядели более естественно и привлекательно, были использованы немного переработанные easing-формулы Роберта Пеннера. Универсальный механизм позволяет применять неравномерное постепенное изменение к любому float
-свойству объекта, а через него косвенно к значениям типа Vector2
или Color
.
Поведения
По сути, это шаблон проектирования «Стратегия»: у каждого объекта типа DynamicObject есть список объектов типа IBehaviour, каждый из которых имеет ссылку на родительский объект и имеющий возможность управлять его свойствами, выполняя произвольный код.
Такой подход позволяет очень сильно упростить описание игровой логики, сведя его к комбинированию нескольких готовых «рецептов». Например, на всех врагов можно одним махом навесить «поведение», заставляюшее их мерцать после попадания в них пули игрока, рассыпаться снопом искр после смерти, дребезжать, отскакивать от стен и двигаться по сложным тракториям.
Взаимодействие с touchscreen
Для получения информации о нажатиях на экран используется класс TouchPanel
и его метод GetState
. В документации по этому методу и примерах использования ничего не было написано, однако состояние TouchCollection
обновляется при каждом вызове. Таким образом, если в дереве объектов несколько из них вызывают GetState
, то только первый из них увидит нажатия с состояниями Pressed
и Released
! У остальных объектов Pressed
превратится в Moved
, а Released
будет вообще исключен из коллекции. Движок обернул эту шероховатость, кешируя у себя однократно получаемый TouchCollection
, которая доступна всем объектам в дереве.
Отложенные действия
Представим себе типичную игровую ситуацию: если некий объект улетел за пределы экрана, его нужно уничтожить. Это можно представить в виде следующего псевдокода:
foreach(var bullet in Bullets)
if(bullet.LeavesPlayfield())
Bullets.Remove(bullet);
Однако такой код выдаст исключение, поскольку изменение коллекции во время ее обхода в цикле foreach
запрещено. Что же делать? Самым элегантным решением проблемы было создание глобального списка продолжений, и код стал работать примерно вот так:
var cont = new List<Action>();
foreach(var bullet in Bullets)
if(bullet.LeavesPlayfield())
cont.Add(() => Bullets.Remove(bullet));
foreach(var act in cont)
cont();
Выводы:
- Велосипедостроение — не всегда плохо, особенно если оно сводится к написанию helper-методов.
- Предварительное создание списка всего требуемого функционала в виде заданий на каком-нибудь task tracker'е очень хорошо помогает бороться с разрастанием требований.
Поиск художников и музыкантов
К концу мая проект был готов более чем наполовину, и вдруг случилось непредвиденное: мой друг, рисовавший графику для игры, получил повышение на работе, в связи с чем свободного времени для нашего проекта у него не стало. Проект застопорился на всё лето, и хотя за это время я и написал немного кода, мотивации для работы в одиночку явно не хватало.
Ближе к августу я думал, что игра заглохла и доделывать ее не имеет смысла. Тут на помощь пришли волшебные пендали от коллег; я отказался сократил требования к игре (отказался от боссов и story mode) и отправился на поиски художников-фрилансеров.
Самым эффективным местом, где можно найти pixel artist'а, оказался форум Job Offerings на сайте PixelJoint: за ночь мне написало больше десяти человек, предложив свою помощь и дав ссылки на портфолио. С одним из них я договорился и работа закипела вновь.
Разброс цен был довольно существенным. Американцы и европейцы просили за свои услуги почти четырехкратную стоимость по сравнению с коллегами из стран СНГ, хотя разницы в качестве практически не было. Бывают и очень странные личности: один американец, хваставшийся участием в «выпущенных под Game Boy Advance проектах», до сих пор время от времени пишет в скайп и просит одолжить ему $100 в счет работы над будущими проектами со мной.
Один из посетителей форума, узнав, что я уже нашел художника, предложил свою помощь в качестве музыканта. Первоначально написанные им мелодии мне не очень понравились, однако после некоторой конструктивной критики мне удалось убедить его переписать музыку так, чтобы она больше подходила к игре.
Выводы:
- Даже за маленькие, но фиксированные деньги художники находятся и работают куда активнее, нежели за идею или процент от прибыли. Если вы правда хотите доделать и выпустить проект — следует учесть первоначальные инвестиции.
- Лучше выпустить какую-то часть игры и дорабатывать ее после, нежели пытаться сделать сразу всё и рисковать не выпустить ничего.
Тестирование и отправка в Windows Phone Store
На носу было католическое рождество и я хотел выпустить игру поскорее, в результате чего пожадничал времени на тестирование, и очень зря. Исправив несколько найденных ошибок, я решил, что игра готова и отправил ее на сертификацию. Первоначальная сертификация заняла неделю, и буквально за несколько часов до того, как игру приняли, мне написал друг и сказал, что нашел новый существенный баг, из-за которого игра зависает намертво. Пришлось собирать новую версию и ждать 5 дней, когда ее проверят.
При принятии приложения в Windows Phone Store никто не проверяет его фактическую значимость. Microsoft в этом вопросе руководствуется идеей о том, что заведомо никудышные приложения сами отфильтруются низкими оценками. На практике же неописуемого говна в маркетe очень много.
Для увеличения шансов того, что игра пройдет сертификацию с первого раза, следует уделить особое внимание следующим вещам:
- Приложение не имеет права самовольно запускать свою музыку, если уже играет пользовательская. Лучше всего показать сообщение с запросом «включить саундтрек или нет?», но можно и просто оставить пользователя с той музыкой, которая играет у него в плеере. Этот случай проверяют в 100% случаев и нарушение этого правила гарантирует отказ.
- Приложение должно быть стабильным. Если оно упадет при каких-то базовых действиях, скорее всего, сертификацию оно не пройдет.
Выводы:
- Время, первоначально выделенное на тестирование, нужно умножать на два. Дважды.
Полезные службы и сервисы
Для облегчения работы с падением приложения есть удобный сервис BugSense. Исключения автоматически классифицируются по callstack'у и присылаются вам по почте. Хорошим тоном является создание специальной страницы, переход на которую осуществляется при возникновении необработанного исключения: на ней можно написать нечто вроде "Что-то сломалось, но не волнуйся, милый пользователь, стектрейс уже на полпути, а мы в поте лица работаем над проблемой!". Мелочь, а приятно ©.
Для сбора статистики отлично подходит Flurry. Количество различных статистических срезов впечатляет:
- Количество новых, уникальных, постоянных пользователей
- Количество и средняя продолжительность сессии
- География и системная локаль
- Модель телефона
- Пол, возраст пользователя
- Масса других показателей
Оба сервиса могут быть использованы бесплатно (правда, BugSense c некоторыми ограничениями). Однако у подключения статистики есть и неожиданное негативное свойство: список требований приложения в маркете пополняется сразу четыремя довольно страшно звучащими пунктами:
- Удостоверение телефона — требуется для сбора информации о моделях телефонов.
- Удостоверение владельца — требуется для сбора информации о количестве уникальных пользователей и сессий.
- Службы определения местоположения — требуется для сбора географических сведений.
- Службы данных — требуется для отправки статистики и crash reports.
Кроме того, если ваша игра проигрывает музыку через MediaPlayer.Play()
, в списке требований также появится пункт "библиотеки фото, музыки и видео".
Выводы:
- В комментариях может завестись какой-нибудь параноик, но не стоит придавать его словам слишком много внимания — статистика важнее.
Реклама
Как привлечь пользователей в свою игру или приложение? Есть несколько способов:
- Купить рекламу на каком-нибудь тематическом сайте или в приложениях.
- Если у вас ваших друзей есть другие популярные приложения, поместить рекламу в них.
- Воспользоваться сервисом AdDuplex: вы показываете у себя рекламу других приложений, а они — вашу.
- Размещать информацию на тематических форумах, группах в соцсетях, реддитах и верещать в твиторе.
В моем случае самым эффективным способом оказался последний: разместив на форуме WPCentral небольшое сообщение со скриншотами, видео на youtube и ссылкой, на следующее утро я обнаружил красующийся на главной странице обзор, выросшую в пять раз статистику скачиваний и упоминание в официальном аккаунте Nokia USA.
Выводы:
- Чудеса случаются :)
Подводя итог
Пока сложно сказать, насколько коммерчески успешной получилась игра, но то, что доведение продукта от идеи до готовности дает массу полезного жизненного опыта — бесспорно. Надеюсь, кому-то мои заметки и мысли вслух сэкономят пару набитых шишек на лбу.
P.S. Прямых ссылок на свою игру сознательно не даю, но любопытный пользователь, внимательно читавший статью, без труда сможет ее найти.
Автор: impwx