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

в 17:06, , рубрики: silverlight, windows phone, xna, разработка под windows phone, метки: , ,

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

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

Идея

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

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

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

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

Выводы:

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

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

Актуальной на тот момент версией платформы была 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>.

На картинке приведена примерная схема классов в движке. Сплошная стрелка — «наследует», пунктирная — «использует».
Разработка игры под Windows Phone

Наиболее значимые проблемы, решаемые с помощью движка, рассмотрим более подробно.

Проверка столкновений

Даже такой важной вещи, как проверка столкновений, в 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(), в списке требований также появится пункт "библиотеки фото, музыки и видео".

Выводы:

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

Реклама

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

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

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

Выводы:

  • Чудеса случаются :)

Подводя итог

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

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

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

Автор: impwx

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js