Разработка игры под 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. Прямых ссылок на свою игру сознательно не даю, но любопытный пользователь, внимательно читавший статью, без труда сможет ее найти.

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 763 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 28

      –12
      Как-то безрадостно всё.
      • НЛО прилетело и опубликовало эту надпись здесь
        0
        Мне не понятно, а была ли причина в построении велосипеда для детекции столкновений или просто было интересно сделать «свой» алгоритм? Например, чем тут плох quadtree (я не знаю ответа, интересно).

        Сочувствую по поводу XNA. ;(
          0
          Если я правильно понял, Quadtree решает несколько иной вопрос, нежели алгоритм, приведенный в статье. Там из множества объектов отсекаются те, которые заведомо не могут столкнуться. Я же проверяю более точное столкновение у объектов, пересечение границ которых уже определено. К сожалению, мой алгоритм не позволяет проверять попиксельное столкновение с объектами, когда они повернуты или отмасштабированы — если кто-то осилит такое написать или даст ссылку на чужое творение, буду очень признателен :)
          +1
          Не смотрели в сторону MonoGame? Они даже нэймспэйсы от XNA оставили… =)
            0
            Как раз смотрел, на него вся надежда. Но MonoGame заменяет только XNA, и я не совсем представляю, как он будет работать вкупе с новым WinRT XAML. Там есть нечто, аналогичное UIElementRenderer?
              0
              Пока еще не смотрел. Для WP8 есть еще SharpDX, у которого есть сборка Toolkit, реализующая часть функционала XNA и у него в примерах было что-то по части связки XAML. Но даже MS уже на британском «Power Up» рекомендует MonoGame. Хотя обидно за XNA.
                0
                SharpDX, насколько я знаю, является базой для MonoGame, т.е. еще более низкоуровневой оберткой. Хотя есть надежда, что раз используется «полноценный» DirectX, на WP8 наконец-то можно будет использовать кастомные шейдеры и другие удобные вещи.
                  0
                  Мне кажется не надо так сильно переживать по поводу XNA. Майкрософт решила не поддерживать это направление (т.к. в случае нехватки ресурсов приходится чем-то жертвовать), при этом Mono подхватило флаг и выпускает почти тоже самое под названием MonoGame — конечно, уровень качества и SLA не совсем такие, как в Майкрософт, но для целевой аудитории пользователей XNA это не должно быть стоп-фактором! Я тоже раньше переживал по поводу XNA, но потом стал рассуждать описанным образом, и полегчало!
                    +1
                    Я про вот это www.sharpdx.org/documentation/api/n-sharpdx-toolkit/. (Линк текстом, так как доспорился=)))
                0
                MonoGame достаточно хорошо работает с Xaml. В стандартной поставке есть шаблон MonoGame+Xaml для Windows 8.
              +1
              &bg;Прямых ссылок на свою игру сознательно не даю, но любопытный пользователь, внимательно читавший статью, без труда сможет ее найти.

              Сэкономлю кому-нибудь время на гугление по запросу «omg aliens» (из тегов) и переход по первой же ссылке :)
              www.wpcentral.com/play-omg-aliens-windows-phone-and-blast-8-bit-aliens-oblivion

              Судя по скриншотам и видео, игрушка на твёрдую 5. Респект за то, что довели до конца несмотря ни на что!
                +1
                Объясните мне простую вещь с идеями для игр — можно ли делать полные ремейки чужих игр, естественно со своей графикой, но с тем же сюжетом или правилами игры?
                  0
                  спросите у Zynga :)
                    0
                    Тоже интересен этот вопрос, может кто то таки подскажет?
                      +1
                      В основном — да, можно. Вы должны заменить графику, музыку, звуки, тексты — все что может представлять собой объекты авторского права. Насчет например структуры уровней и задач (если это паззл) — сложнее. Автору изначальной игры надо будет доказать что его структура и содержание его уровней или загадок защищены авторским правом, но это мутно и я не могу вам точно сказать по этому поводу. Лучше конечно придумать свои уровни. Правда если вы сделаете свою графику, музыку, тексты, звуки, напишите код и сделаете для него свои уровни, то у игры есть шанс отклеиться от ярлыка «клон»)
                      Все вышесказанное не распространяется на ряд игр, где запатентована игровая механика (например, Тетрис или Пакман). Я не знаю, как ее запатентовали, но если вы сделаете прибыльный клон Тетриса, у вас точно будут проблемы с TTC. Не факт что они выиграют в суде, но они попробуют.
                    0
                    Пара технических вопросов:
                    1. Зачем делать проверку столкновений попиксельно? Это же мягко говоря, не очень быстро. Тем более что судя по видео геймплея, с головой хватит проверки только прямоугольников.
                    2. И про «отложенные действия». Разве нельзя просто обычным for'ом коллекцию обходить?

                    А игра красивая.
                      0
                      2. И про «отложенные действия». Разве нельзя просто обычным for'ом коллекцию обходить?

                      Если во время for изменить коллекцию, то счетчик может пропустить следующий элемент, или наоборот — пройти его дважды. Нужно писать доп. условия для корректировки счетчика.
                      Обычно либо делают как автор, либо проходят не по самой коллекции, а по ее копии, содержащей те же элементы — накладные расходы на создание такой копии не особо велики.
                        0
                        понятно, спасибо
                          0
                          Чтобы не было таких проблем просто пишется for reverse.
                            0
                            Для удаления сработает. А если при определенном условии нужно добавить новый элемент, а не удалить текущий? В конечном итоге это будет весьма запутывать.
                          0
                          1. Попиксельная проверка нужна в любом случае. Например, если пришльцы еще с каким-то приближением вписываются в прямоугольник, то тачка игрока — совсем нет. Игроку было бы досадно и непонятно, почему пули разрываются в метре от капота, но его все же ранит. Кроме того, планировали делать боссов, форма которых тоже могла быть произвольной.
                          2. Комментатор выше все правильно описал, добавить нечего.
                            0
                            На тачку игрока можно пару прямоугольников взять
                            +1
                            for-ом не надо, нужно что-то типа:

                            Bullets.RemoveAll(_ => _.LeavesPlayfield());

                            А «отложеные действия» — это какой-то аццкий велосипед с треугольным колесом.
                              +1
                              Такой вариант мне пришел в голову первым, однако он не подходит по некоторым причинам.

                              Во-первых, в реальной игре логика обработки более сложна. Для той же самой пули:
                              1. Если улетела вверх за экран — уничтожить.
                              2. Если столкнулась с пришельцем — уничтожить, пришельцу нанести bullet.Damage единиц урона, проиграть звук.
                              3. Если столкнулась со щитом — уничтожить, создать сноп искр.

                              Как вы понимаете, такими темпами наш список пуль будет обходиться три раза вместо одного, не говоря уже о том, что overhead на вызов функции в цикле в сравнении с простым циклом довольно существенный. LINQ-стиль удобен, но для приложения, в котором важна скорость, я посчитал нужным от него отказаться.

                              Во-вторых, древовидная структура графа объектов не дает нам полного представления о том, сколько foreach'ей сейчас выполняются и какие объекты трогать нельзя. Например, в моем случае структура такая:

                              Scene.Objects -> WaveManager.Waves -> Wave.Aliens.

                              Если я, обходя список пришельцев в Wave, решу добавить новый объект (взрыв? powerup?) в корень сцены, ее коллекция изменится и у меня снова будет ошибка.

                              С отложенными действиями получается действительно удобнее и быстрее, нужно только немного в них вкурить :)
                                0
                                Много шума из ничего. Сколько там у вас этих пуль на экране может быть? Десять? Двадцать? Целых тридцать? И вы на обходе такого огромного, коллосального списка серьёзно хотите сэкономить? Задание контуров коллижнов из двух-трёх ректанглов даст ИМХО на порядок больший выигрыш.

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое