- PVSM.RU - https://www.pvsm.ru -
В этой статье я хочу рассказать о своем опыте написания игры под платформу Windows Phone. Несмотря на кажущуюся простоту, путь от идеи до принятия игры в Windows Phone Store занял практически год и был полон неожиданных подводных камней — как с технической, так и с организационной сторон. Статья рассчитана на начинающих разработчиков, которые имеют представление о .NET / C#, но не пробовали делать полноценных игр.
Сложно вспомнить, как именно пришла идея написать игру. В школьной и институтской юности я развлекался написанием игрушек на конструкторах игр типа Multimedia Fusion [1], однако система «событие-действие» довольно неудобна для описания сложной логики. Выбор в пользу Windows Phone пал по следующим причинам:
Я написал другу и поведал ему, что хочу написать игру для телефона: очередной ремейк классической игровой механики тридцатипятилетней давности [3], порядка 30 уровней с несколькими боссами. Друг согласился заняться графикой, а я сел изучать инструментарий.
Выводы:
Актуальной на тот момент версией платформы была WP 7.5 Mango, позволявшая использовать и Silverligh, и XNA в одном приложении. Это оказалось очень кстати, поскольку XNA является довольно куцым фреймворком, предоставляющим только спартанский минимум функционала. Silverlight можно использовать для меню и прочих «спокойных» страниц с текстом, кнопками и полями ввода, а саму игру отрисовывать на специальной XNA-странице.
Примеры игр, которые можно скачать с сайта Microsoft и поковырять, показывали слабо подходящие для разработки нормальной игры практики. Все переменные объявлялись в качестве свойств прямо в классе сцены, и если для игры из одного задника и двух объектов это еще простительно, то при создании сколько-нибудь сложных сцен код превратится в неподдерживаемое месиво. Поиск подходящих игровых движков тоже не принес желаемых успехов: почти все движки ориентированы на 3D игры, а наша игра исключительно 2D. Так было принято решение потешить жажду велосипедостроения и написать свой небольшой движок для внутреннего пользования.
Когда движок уже подавал сознательные признаки жизни, анонс Windows Phone 8 стал для меня приятной неожиданностью, которая, однако, быстро переросла в неприятную: XNA поддерживается теперь только в режиме совместимости, а официального способа писать игры для WinPhone на C# Microsoft больше не предлагает! Однако начинать изучать новую технологию и переписывать все под нее было абсолютно нереально, и пришлось довольствоваться режимом совместимости, который, к счастью, никаких неожиданных подводных камней не приготовил.
Выводы:
Основной задачей движка была организация кода и предоставление ООП-каркаса, на котором можно было бы строить схему классов предметной области. Для тех, кому хочется посмотреть на код или использовать движок для своей игры — на здоровье, он доступен под лицензией MIT на гитхабе [4].
Базовый класс 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-формулы Роберта Пеннера [5]. Универсальный механизм позволяет применять неравномерное постепенное изменение к любому float
-свойству объекта, а через него косвенно к значениям типа Vector2
или Color
.
По сути, это шаблон проектирования «Стратегия» [6]: у каждого объекта типа DynamicObject есть список объектов типа IBehaviour, каждый из которых имеет ссылку на родительский объект и имеющий возможность управлять его свойствами, выполняя произвольный код.
Такой подход позволяет очень сильно упростить описание игровой логики, сведя его к комбинированию нескольких готовых «рецептов». Например, на всех врагов можно одним махом навесить «поведение», заставляюшее их мерцать после попадания в них пули игрока, рассыпаться снопом искр после смерти, дребезжать, отскакивать от стен и двигаться по сложным тракториям.
Для получения информации о нажатиях на экран используется класс TouchPanel
и его метод GetState
. В документации по этому методу [7] и примерах использования [8] ничего не было написано, однако состояние TouchCollection
обновляется при каждом вызове. Таким образом, если в дереве объектов несколько из них вызывают GetState
, то только первый из них увидит нажатия с состояниями Pressed
и Released
! У остальных объектов Pressed
превратится в Moved
, а Released
будет вообще исключен из коллекции. Движок обернул эту шероховатость, кешируя у себя однократно получаемый TouchCollection
, которая доступна всем объектам в дереве.
Представим себе типичную игровую ситуацию: если некий объект улетел за пределы экрана, его нужно уничтожить. Это можно представить в виде следующего псевдокода:
foreach(var bullet in Bullets)
if(bullet.LeavesPlayfield())
Bullets.Remove(bullet);
Однако такой код выдаст исключение, поскольку изменение коллекции во время ее обхода в цикле foreach
запрещено. Что же делать? Самым элегантным решением проблемы было создание глобального списка продолжений [9], и код стал работать примерно вот так:
var cont = new List<Action>();
foreach(var bullet in Bullets)
if(bullet.LeavesPlayfield())
cont.Add(() => Bullets.Remove(bullet));
foreach(var act in cont)
cont();
Выводы:
К концу мая проект был готов более чем наполовину, и вдруг случилось непредвиденное: мой друг, рисовавший графику для игры, получил повышение на работе, в связи с чем свободного времени для нашего проекта у него не стало. Проект застопорился на всё лето, и хотя за это время я и написал немного кода, мотивации для работы в одиночку явно не хватало.
Ближе к августу я думал, что игра заглохла и доделывать ее не имеет смысла. Тут на помощь пришли волшебные пендали от коллег; я отказался сократил требования к игре (отказался от боссов и story mode) и отправился на поиски художников-фрилансеров.
Самым эффективным местом, где можно найти pixel artist'а, оказался форум Job Offerings на сайте PixelJoint [11]: за ночь мне написало больше десяти человек, предложив свою помощь и дав ссылки на портфолио. С одним из них я договорился и работа закипела вновь.
Разброс цен был довольно существенным. Американцы и европейцы просили за свои услуги почти четырехкратную стоимость по сравнению с коллегами из стран СНГ, хотя разницы в качестве практически не было. Бывают и очень странные личности: один американец, хваставшийся участием в «выпущенных под Game Boy Advance проектах», до сих пор время от времени пишет в скайп и просит одолжить ему $100 в счет работы над будущими проектами со мной.
Один из посетителей форума, узнав, что я уже нашел художника, предложил свою помощь в качестве музыканта. Первоначально написанные им мелодии мне не очень понравились, однако после некоторой конструктивной критики мне удалось убедить его переписать музыку так, чтобы она больше подходила к игре.
Выводы:
На носу было католическое рождество и я хотел выпустить игру поскорее, в результате чего пожадничал времени на тестирование, и очень зря. Исправив несколько найденных ошибок, я решил, что игра готова и отправил ее на сертификацию. Первоначальная сертификация заняла неделю, и буквально за несколько часов до того, как игру приняли, мне написал друг и сказал, что нашел новый существенный баг, из-за которого игра зависает намертво. Пришлось собирать новую версию и ждать 5 дней, когда ее проверят.
При принятии приложения в Windows Phone Store никто не проверяет его фактическую значимость. Microsoft в этом вопросе руководствуется идеей о том, что заведомо никудышные приложения сами отфильтруются низкими оценками. На практике же неописуемого [12] говна [13] в [14] маркетe [15] очень [16] много [17].
Для увеличения шансов того, что игра пройдет сертификацию с первого раза, следует уделить особое внимание следующим вещам:
Выводы:
Для облегчения работы с падением приложения есть удобный сервис BugSense [18]. Исключения автоматически классифицируются по callstack'у и присылаются вам по почте. Хорошим тоном является создание специальной страницы, переход на которую осуществляется при возникновении необработанного исключения: на ней можно написать нечто вроде "Что-то сломалось, но не волнуйся, милый пользователь, стектрейс уже на полпути, а мы в поте лица работаем над проблемой!". Мелочь, а приятно ©.
Для сбора статистики отлично подходит Flurry [19]. Количество различных статистических срезов впечатляет:
Оба сервиса могут быть использованы бесплатно (правда, BugSense c некоторыми ограничениями). Однако у подключения статистики есть и неожиданное негативное свойство: список требований приложения в маркете пополняется сразу четыремя довольно страшно звучащими пунктами:
Кроме того, если ваша игра проигрывает музыку через MediaPlayer.Play()
, в списке требований также появится пункт "библиотеки фото, музыки и видео".
Выводы:
Как привлечь пользователей в свою игру или приложение? Есть несколько способов:
В моем случае самым эффективным способом оказался последний: разместив на форуме WPCentral [21] небольшое сообщение со скриншотами, видео на youtube и ссылкой, на следующее утро я обнаружил красующийся на главной странице обзор, выросшую в пять раз статистику скачиваний и упоминание в официальном аккаунте Nokia USA [22].
Выводы:
Пока сложно сказать, насколько коммерчески успешной получилась игра, но то, что доведение продукта от идеи до готовности дает массу полезного жизненного опыта — бесспорно. Надеюсь, кому-то мои заметки и мысли вслух сэкономят пару набитых шишек на лбу.
P.S. Прямых ссылок на свою игру сознательно не даю, но любопытный пользователь, внимательно читавший статью, без труда сможет ее найти.
Автор: impwx
Источник [23]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/windows-phone/24222
Ссылки в тексте:
[1] Multimedia Fusion: http://www.clickteam.com/website/world/multimedia-fusion-2
[2] DiverOfDark: http://habrahabr.ru/users/diverofdark/
[3] классической игровой механики тридцатипятилетней давности: http://en.wikipedia.org/wiki/Space_Invaders
[4] на гитхабе: https://github.com/impworks/xna.geek.engine
[5] easing-формулы Роберта Пеннера: http://www.robertpenner.com/easing/easing_demo.html
[6] шаблон проектирования «Стратегия»: http://ru.wikipedia.org/wiki/%D0%A1%D1%82%D1%80%D0%B0%D1%82%D0%B5%D0%B3%D0%B8%D1%8F_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)
[7] документации по этому методу: http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.input.touch.touchpanel.getstate.aspx
[8] примерах использования: http://msdn.microsoft.com/en-us/library/ff434208.aspx
[9] продолжений: http://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B4%D0%BE%D0%BB%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5
[10] разрастанием требований: http://en.wikipedia.org/wiki/Feature_creep
[11] Job Offerings на сайте PixelJoint: http://www.pixeljoint.com/forum/forum_topics.asp?FID=11
[12] неописуемого: http://www.windowsphone.com/en-us/store/app/xnapage1/16926456-2c84-4f6d-ac74-6674755de0fd
[13] говна: http://www.windowsphone.com/en-us/store/app/killtheduck_v1/0c5fbe6f-be1b-4c1a-bf2c-fb3d49dd0b6c
[14] в: http://www.windowsphone.com/en-us/store/app/z-archer/a65b8878-3c1b-425a-be32-21c91c5dc7b9
[15] маркетe: http://www.windowsphone.com/en-us/store/app/love-message-extender/e22856c7-24cc-46ee-bf96-17d36adc016a
[16] очень: http://www.windowsphone.com/en-us/store/app/laser-tag/51aca357-1aad-4d67-b1d3-f730e9804467
[17] много: http://www.windowsphone.com/en-us/store/app/silly-eye/8bae4eec-10f8-4b52-954c-d7b6b5e8297b
[18] BugSense: https://www.bugsense.com/
[19] Flurry: http://www.flurry.com/
[20] AdDuplex: http://www.adduplex.com/
[21] WPCentral: http://www.wpcentral.com/
[22] Nokia USA: https://twitter.com/NokiaUS
[23] Источник: http://habrahabr.ru/post/164969/
Нажмите здесь для печати.