7 января 2013

Разработка игры под Windows Phone

Разработка под Windows Phone


В этой статье я хочу рассказать о своем опыте написания игры под платформу Windows Phone. Несмотря на кажущуюся простоту, путь от идеи до принятия игры в Windows Phone Store занял практически год и был полон неожиданных подводных камней — как с технической, так и с организационной сторон. Статья рассчитана на начинающих разработчиков, которые имеют представление о .NET / C#, но не пробовали делать полноценных игр.

Идея


Сложно вспомнить, как именно пришла идея написать игру. В школьной и институтской юности я развлекался написанием игрушек на конструкторах игр типа Multimedia Fusion, однако система «событие-действие» довольно неудобна для описания сложной логики. Выбор в пользу Windows Phone пал по следующим причинам:

  • На тот момент (год назад) в маркете было очень мало приложений, моя игра не затеряется.
  • Игры можно писать на C#, который мне хорошо знаком.
  • Мой коллега DiverOfDark, с помощью которого я потом публиковал игру, купил себе Windows Phone и расхваливал его во всех красках, пророча платформе феерический успех.

Я написал другу и поведал ему, что хочу написать игру для телефона: очередной ремейк классической игровой механики тридцатипятилетней давности, порядка 30 уровней с несколькими боссами. Друг согласился заняться графикой, а я сел изучать инструментарий.



Выводы:
  • Не обязательно писать игру под платформу, которой пользуешься сам.
  • Люди любят классические игры. Не обязательно открывать новый жанр и изобретать радикально новые неизведанные игровые механики, однако вылизывать ее до мелочей все же необходимо.


На чем писать?


Актуальной на тот момент версией платформы была WP 7.5 Mango, позволявшая использовать и Silverlight, и 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


На носу было католическое рождество. Я хотел выпустить игру поскорее, в результате чего пожадничал времени на тестирование, и очень зря. Исправив несколько найденных ошибок, я решил, что игра готова, и отправил ее в Store. Первоначальная сертификация заняла неделю, и буквально за несколько часов до того, как игру приняли, мне написал друг и сказал, что нашел новый существенный баг, из-за которого игра зависает намертво. Пришлось собирать новую версию и ждать еще 5 дней, когда ее проверят.

При принятии приложения в Windows Phone Store никто не проверяет его фактическую значимость. Microsoft в этом вопросе руководствуется идеей о том, что заведомо никудышные приложения сами отфильтруются низкими оценками. На практике же неописуемого говна в маркетe очень много.

Для увеличения шансов того, что игра пройдет сертификацию с первого раза, следует уделить особое внимание следующим вещам:

  • Приложение не имеет права самовольно запускать свою музыку, если уже играет пользовательская. Лучше всего показать сообщение с запросом «включить саундтрек или нет?», но можно и просто оставить пользователя с той музыкой, которая играет у него в плеере. Этот случай проверяют в 100% случаев и нарушение этого правила гарантирует отказ.
  • Приложение должно быть стабильным. Если оно упадет при каких-то базовых действиях, скорее всего, сертификацию оно не пройдет.


Выводы:
  • Время, первоначально выделенное на тестирование, нужно умножать на два. Дважды.


Полезные службы и сервисы


Для облегчения работы с падением приложения есть удобный сервис BugSense. Исключения автоматически классифицируются по callstack'у и присылаются вам по почте. Хорошим тоном является создание специальной страницы, переход на которую осуществляется при возникновении необработанного исключения: на ней можно написать нечто вроде "Что-то сломалось, но не волнуйся, милый пользователь, стектрейс уже на полпути, а мы в поте лица работаем над проблемой!". Мелочь, а приятно ©.

Для сбора статистики отлично подходит Flurry. Количество различных статистических срезов впечатляет:

  • Количество новых, уникальных, постоянных пользователей
  • Количество и средняя продолжительность сессии
  • География и системная локаль
  • Модель телефона
  • Пол, возраст пользователя
  • Масса других показателей

Оба сервиса могут быть использованы бесплатно (правда, BugSense c некоторыми ограничениями). Однако у подключения статистики есть и неожиданное негативное свойство: список требований приложения в маркете пополняется сразу четыремя довольно страшно звучащими пунктами:

  • Удостоверение телефона — требуется для сбора информации о моделях телефонов.
  • Удостоверение владельца — требуется для сбора информации о количестве уникальных пользователей и сессий.
  • Службы определения местоположения — требуется для сбора географических сведений.
  • Службы данных — требуется для отправки статистики и crash reports.

Кроме того, если ваша игра проигрывает музыку через MediaPlayer.Play(), в списке требований также появится пункт "библиотеки фото, музыки и видео".

Выводы:
  • В комментариях может завестись какой-нибудь параноик, но не стоит придавать его словам слишком много внимания — статистика важнее.


Реклама


Как привлечь пользователей в свою игру или приложение? Есть несколько способов:

  1. Купить рекламу на каком-нибудь тематическом сайте или в приложениях.
  2. Если у вас \ ваших друзей есть другие популярные приложения, поместить рекламу в них.
  3. Воспользоваться сервисом AdDuplex: вы показываете у себя рекламу других приложений, а они — вашу.
  4. Размещать информацию на тематических форумах, группах в соцсетях, реддитах и верещать в твиторе.

В моем случае самым эффективным способом оказался последний: разместив на форуме WPCentral небольшое сообщение со скриншотами, видео на youtube и ссылкой, на следующее утро я обнаружил красующийся на главной странице обзор, выросшую в пять раз статистику скачиваний и упоминание в официальном аккаунте Nokia USA.

Выводы:
  • Чудеса случаются :)


Подводя итог


Пока сложно сказать, насколько коммерчески успешной получилась игра, но то, что доведение продукта от идеи до готовности дает массу полезного жизненного опыта — бесспорно. Надеюсь, кому-то мои заметки и мысли вслух сэкономят пару набитых шишек на лбу.



P.S. Прямых ссылок на свою игру сознательно не даю, но любопытный пользователь, внимательно читавший статью, без труда сможет ее найти.
Теги:windows phonexnasilverlightomg aliens
Хабы: Разработка под Windows Phone
+45
36,9k 216
Комментарии 28
Лучшие публикации за сутки