Практический пример – надувная мышь

Введение

Надувная мышь

После публикации Bouncy Mouse для iOS и Android в конце прошлого года я усвоил несколько очень важных уроков. Ключевым из них было то, что проникнуть на устоявшийся рынок сложно. На насыщенном рынке iPhone добиться успеха было очень сложно; на менее насыщенном Android Marketplace прогресс был проще, но все равно не так просто. Учитывая этот опыт, я увидел интересную возможность в Интернет-магазине Chrome. Хотя Интернет-магазин ни в коем случае не пуст, его каталог высококачественных игр на основе HTML5 только начинает набирать обороты. Для нового разработчика приложений это означает, что создавать диаграммы рейтинга и добиваться заметности становится намного проще. Имея в виду эту возможность, я приступил к портированию Bouncy Mouse на HTML5 в надежде, что смогу донести свой последний игровой опыт до захватывающей новой базы пользователей. В этом примере я немного расскажу об общем процессе переноса Bouncy Mouse на HTML5, а затем углублюсь в три области, которые оказались интересными: аудио, производительность и монетизация.

Портирование игры C++ на HTML5

В настоящее время Bouncy Mouse доступна на Android (C++), iOS (C++), Windows Phone 7 (C#) и Chrome (Javascript). Иногда возникает вопрос: как написать игру, которую можно легко портировать на несколько платформ? У меня такое ощущение, что люди надеются на какое-то волшебное средство, которое они смогут использовать для достижения такого уровня портативности, не прибегая к ручному портированию. К сожалению, я не уверен, что такое решение еще существует (наиболее близким, вероятно, является фреймворк Google PlayN или движок Unity , но ни один из них не достигает всех целей, которые меня интересовали). Мой подход, по сути, был ручным портом. Сначала я написал версию для iOS/Android на C++, затем портировал этот код на каждую новую платформу. Хотя это может показаться большой работой, на создание версий WP7 и Chrome ушло не более 2 недель каждая. Итак, теперь вопрос в том, можно ли что-нибудь сделать, чтобы кодовую базу можно было легко переносить вручную? Я сделал пару вещей, которые помогли в этом:

Сохраняйте небольшую кодовую базу

Хотя это может показаться очевидным, на самом деле это главная причина, по которой мне удалось так быстро портировать игру. Клиентский код Bouncy Mouse состоит всего из 7000 строк C++. 7000 строк кода — это не пустяки, но они достаточно малы, чтобы ими можно было управлять. Обе версии клиентского кода на C# и Javascript оказались примерно одинакового размера. Сохранение небольшой кодовой базы в основном сводилось к двум ключевым практикам: не писать лишнего кода и делать как можно больше кода предварительной обработки (не во время выполнения). Отсутствие написания лишнего кода может показаться очевидным, но это то, над чем я всегда борюсь сам с собой. У меня часто возникает желание написать вспомогательный класс/функцию для всего, что можно включить в хелпер. Однако, если вы на самом деле не планируете использовать помощник несколько раз, это обычно приводит к раздуванию вашего кода. В случае с Bouncy Mouse я старался никогда не писать помощника, если не собирался использовать его как минимум три раза. Когда я писал вспомогательный класс, я старался сделать его понятным, переносимым и пригодным для повторного использования в моих будущих проектах. С другой стороны, при написании кода только для Bouncy Mouse с низкой вероятностью повторного использования я сосредоточился на том, чтобы выполнить задачу кодирования как можно проще и быстрее, даже если это был не самый «красивый» способ написания кода. код. Вторая и более важная часть сохранения небольшой кодовой базы заключалась в том, чтобы как можно больше вложить в этапы предварительной обработки. Если вы можете взять задачу времени выполнения и переместить ее в задачу предварительной обработки, ваша игра не только будет работать быстрее, но и вам не придется переносить код на каждую новую платформу. В качестве примера: изначально я хранил данные геометрии уровня в довольно необработанном формате, собирая фактические буферы вершин OpenGL/WebGL во время выполнения. Это потребовало небольшой настройки и нескольких сотен строк кода времени выполнения. Позже я перенес этот код на этап предварительной обработки, записывая полностью упакованные буферы вершин OpenGL/WebGL во время компиляции. Фактический объем кода был примерно таким же, но эти несколько сотен строк были перенесены на этап предварительной обработки, а это значит, что мне никогда не приходилось переносить их на какие-либо новые платформы. В Bouncy Mouse есть множество примеров этого, и то, что возможно, будет варьироваться от игры к игре, но просто следите за всем, что не должно происходить во время выполнения.

Не принимайте зависимости, которые вам не нужны

Еще одна причина, по которой Bouncy Mouse легко портировать, заключается в том, что у нее практически нет зависимостей. В следующей таблице приведены основные зависимости библиотек Bouncy Mouse для каждой платформы:

Андроид iOS HTML5 WP7
Графика OpenGL ES OpenGL ES ВебГЛ XNA
Звук OpenSL ES ОпенАЛ Веб-аудио XNA
Физика Коробка2D Коробка2D Box2D.js Box2D.xna

Вот и все. Никаких больших сторонних библиотек не использовалось, кроме Box2D , который можно переносить на все платформы. Что касается графики, и WebGL, и XNA отображают почти 1:1 с OpenGL, так что это не было большой проблемой. Только в области звука библиотеки отличались. Однако звуковой код в Bouncy Mouse небольшой (около сотни строк кода для конкретной платформы), так что это не было большой проблемой. Сохранение Bouncy Mouse свободным от больших непереносимых библиотек означает, что логика кода времени выполнения может быть почти одинаковой в разных версиях (несмотря на изменение языка). Кроме того, это избавляет нас от привязанности к непереносимой цепочке инструментов. Меня спросили, приводит ли кодирование с использованием OpenGL/WebGL напрямую к увеличению сложности по сравнению с использованием такой библиотеки, как Cocos2D или Unity (также есть несколько помощников WebGL). На самом деле я верю как раз в обратное. Большинство игр для мобильных телефонов/HTML5 (по крайней мере, такие как Bouncy Mouse) очень просты. В большинстве случаев игра просто рисует несколько спрайтов и, возможно, текстурированную геометрию. Общий объем кода OpenGL в Bouncy Mouse, вероятно, составляет менее 1000 строк. Я был бы удивлен, если бы использование вспомогательной библиотеки действительно уменьшило бы это число. Даже если это сократит это число вдвое, мне придется потратить значительное время на изучение новых библиотек/инструментов только для того, чтобы сэкономить 500 строк кода. Вдобавок ко всему, мне еще предстоит найти вспомогательную библиотеку, переносимую на все интересующие меня платформы, поэтому использование такой зависимости значительно ухудшит переносимость. Если бы я писал 3D-игру, для которой требовались карты освещения, динамический уровень детализации, анимация со скинами и т. д., мой ответ наверняка изменился бы. В этом случае я бы заново изобрел велосипед, чтобы попытаться вручную закодировать весь свой движок под OpenGL. Я хочу сказать, что большинство игр для мобильных устройств/HTML5 (пока) не относятся к этой категории, поэтому не нужно усложнять ситуацию до того, как это станет необходимым.

Не стоит недооценивать сходство между языками

Последний трюк, который сэкономил много времени при переносе моей кодовой базы C++ на новый язык, заключался в осознании того, что большая часть кода на каждом языке практически идентична. Хотя некоторые ключевые элементы могут измениться, их гораздо меньше, чем тех, которые не меняются. Фактически, для многих функций переход с C++ на Javascript просто включал выполнение нескольких замен регулярных выражений в моей кодовой базе C++.

Выводы по портированию

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

Аудио

Одной из областей, которая вызвала у меня (и, похоже, у всех остальных) некоторые проблемы, был звук. На iOS и Android доступен ряд надежных вариантов аудио (OpenSL, OpenAL), но в мире HTML5 дела обстоят мрачнее. Хотя HTML5 Audio доступен, я обнаружил, что при его использовании в играх возникают некоторые серьезные проблемы. Даже в новейших браузерах я часто сталкивался со странным поведением. Например, Chrome, похоже, имеет ограничение на количество одновременно создаваемых элементов Audio ( source ). Кроме того, даже когда звук воспроизводился, иногда он необъяснимо искажался. В общем, я немного волновался. Поиск в Интернете показал, что почти у всех одна и та же проблема. Решением, к которому я изначально пришел, был API под названием SoundManager2. Этот API использует HTML5 Audio, когда он доступен, и в сложных ситуациях возвращается к Flash. Хотя это решение работало, оно по-прежнему было глючным и непредсказуемым (чуть меньше, чем чистый HTML5 Audio). Через неделю после запуска я поговорил с некоторыми полезными людьми из Google, которые указали мне на API веб-аудио Webkit. Первоначально я рассматривал возможность использования этого API, но уклонялся от него из-за ненужной (для меня) сложности, которую этот API, казалось, имел. Я просто хотел воспроизвести несколько звуков: с HTML5 Audio это составляет пару строк Javascript. Однако при беглом взгляде на Web Audio меня поразила его огромная (70 страниц) спецификация, небольшое количество сэмплов в сети (типичное для нового API) и отсутствие «воспроизведения», «паузы». или функцию «stop» в любом месте спецификации. Получив заверения Google, что мои опасения необоснованны, я снова углубился в API. Посмотрев еще несколько примеров и проведя небольшое исследование, я обнаружил, что Google был прав: API определенно может удовлетворить мои потребности, и он может сделать это без ошибок, от которых страдают другие API. Особенно полезна статья «Начало работы с API веб-аудио» , которая станет отличным источником информации, если вы хотите глубже понять API. Моя реальная проблема заключается в том, что даже после понимания и использования API он все еще кажется мне API, который не предназначен для «просто воспроизведения нескольких звуков». Чтобы обойти это опасение, я написал небольшой вспомогательный класс, который позволил мне использовать API так, как я хотел: воспроизводить, приостанавливать, останавливать и запрашивать состояние звука. Я назвал этот вспомогательный класс AudioClip. Полный исходный код доступен на GitHub под лицензией Apache 2.0, подробности этого класса я буду обсуждать ниже. Но сначала немного информации об API веб-аудио:

Графики веб-аудио

Первое, что делает API веб-аудио более сложным (и более мощным), чем элемент HTML5 Audio, — это его способность обрабатывать/микшировать звук перед его выводом пользователю. Несмотря на свою мощь, тот факт, что любое воспроизведение звука включает в себя график, делает ситуацию немного более сложной в простых сценариях. Чтобы проиллюстрировать возможности API веб-аудио, рассмотрим следующий график:

Базовый граф веб-аудио
Базовый граф веб-аудио

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

Графики могут быть простыми

Первое, что делает API веб-аудио более сложным (и более мощным), чем элемент HTML5 Audio, — это его способность обрабатывать/микшировать звук перед его выводом пользователю. Несмотря на свою мощь, тот факт, что любое воспроизведение звука включает в себя график, делает ситуацию немного более сложной в простых сценариях. Чтобы проиллюстрировать возможности API веб-аудио, рассмотрим следующий график:

Тривиальный граф веб-аудио
Тривиальный граф веб-аудио

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

Но давайте даже не будем беспокоиться о графике

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

Аудиоклип
Аудиоклип

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

// At startup time
var sound = new AudioClip("ping.wav");

// Later
sound.play();

Детали реализации

Давайте кратко рассмотрим код вспомогательного класса: Конструктор. Конструктор обрабатывает загрузку звуковых данных с помощью XHR. Хотя здесь это не показано (для простоты примера), элемент HTML5 Audio также можно использовать в качестве исходного узла. Это особенно полезно для больших выборок. Обратите внимание, что API веб-аудио требует, чтобы мы извлекали эти данные как «буфер массива». Как только данные получены, мы создаем из этих данных буфер веб-аудио (декодируя его из исходного формата в формат PCM времени выполнения).

/**
* Create a new AudioClip object from a source URL. This object can be played,
* paused, stopped, and resumed, like the HTML5 Audio element.
*
* @constructor
* @param {DOMString} src
* @param {boolean=} opt_autoplay
* @param {boolean=} opt_loop
*/
AudioClip = function(src, opt_autoplay, opt_loop) {
// At construction time, the AudioClip is not playing (stopped),
// and has no offset recorded.
this.playing_ = false;
this.startTime_ = 0;
this.loop_ = opt_loop ? true : false;

// State to handle pause/resume, and some of the intricacies of looping.
this.resetTimout_ = null;
this.pauseTime_ = 0;

// Create an XHR to load the audio data.
var request = new XMLHttpRequest();
request.open("GET", src, true);
request.responseType = "arraybuffer";

var sfx = this;
request.onload = function() {
// When audio data is ready, we create a WebAudio buffer from the data.
// Using decodeAudioData allows for async audio loading, which is useful
// when loading longer audio tracks (music).
AudioClip.context.decodeAudioData(request.response, function(buffer) {
    sfx.buffer_ = buffer;
    
    if (opt_autoplay) {
    sfx.play();
    }
});
}

request.send();
}

Воспроизведение. Воспроизведение нашего звука включает в себя два шага: настройку графика воспроизведения и вызов версии «noteOn» в источнике графика. Источник можно воспроизвести только один раз, поэтому мы должны заново создавать источник/график каждый раз, когда воспроизводим. Большая часть сложности этой функции связана с требованиями, необходимыми для возобновления приостановленного клипа ( this.pauseTime_ > 0 ). Чтобы возобновить воспроизведение приостановленного клипа, мы используем noteGrainOn , который позволяет воспроизводить подобласть буфера. К сожалению, noteGrainOn не взаимодействует с циклом желаемым для этого сценария способом (он будет зацикливать подобласть, а не весь буфер). Поэтому нам нужно обойти эту проблему, воспроизведя оставшуюся часть клипа с помощью noteGrainOn , а затем перезапустив клип с начала с включенным циклом.

/**
* Recreates the audio graph. Each source can only be played once, so
* we must recreate the source each time we want to play.
* @return {BufferSource}
* @param {boolean=} loop
*/
AudioClip.prototype.createGraph = function(loop) {
var source = AudioClip.context.createBufferSource();
source.buffer = this.buffer_;
source.connect(AudioClip.context.destination);

// Looping is handled by the Web Audio API.
source.loop = loop;

return source;
}

/**
* Plays the given AudioClip. Clips played in this manner can be stopped
* or paused/resumed.
*/
AudioClip.prototype.play = function() {
if (this.buffer_ && !this.isPlaying()) {
// Record the start time so we know how long we've been playing.
this.startTime_ = AudioClip.context.currentTime;
this.playing_ = true;
this.resetTimeout_ = null;

// If the clip is paused, we need to resume it.
if (this.pauseTime_ > 0) {
    // We are resuming a clip, so it's current playback time is not correctly
    // indicated by startTime_. Correct this by subtracting pauseTime_.
    this.startTime_ -= this.pauseTime_;
    var remainingTime = this.buffer_.duration - this.pauseTime_;

    if (this.loop_) {
    // If the clip is paused and looping, we need to resume the clip
    // with looping disabled. Once the clip has finished, we will re-start
    // the clip from the beginning with looping enabled
    this.source_ = this.createGraph(false);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime)

    // Handle restarting the playback once the resumed clip has completed.
    // *Note that setTimeout is not the ideal method to use here. A better 
    // option would be to handle timing in a more predictable manner,
    // such as tying the update to the game loop.
    var clip = this;
    this.resetTimeout_ = setTimeout(function() { clip.stop(); clip.play() },
                                    remainingTime * 1000);
    } else {
    // Paused non-looping case, just create the graph and play the sub-
    // region using noteGrainOn.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteGrainOn(0, this.pauseTime_, remainingTime);
    }

    this.pauseTime_ = 0;
} else {
    // Normal case, just creat the graph and play.
    this.source_ = this.createGraph(this.loop_);
    this.source_.noteOn(0);
}
}
}

Воспроизведение как звуковой эффект. Функция воспроизведения, описанная выше, не позволяет воспроизводить аудиоклип несколько раз с перекрытием (второе воспроизведение возможно только после завершения или остановки клипа). Иногда игре требуется воспроизвести звук много раз, не дожидаясь завершения каждого воспроизведения (сбор монет в игре и т. д.). Для этого в классе AudioClip есть метод playAsSFX() . Поскольку одновременно может происходить несколько воспроизведений, воспроизведение из playAsSFX() не связано 1:1 с AudioClip. Поэтому воспроизведение нельзя остановить, приостановить или запросить состояние. Зацикливание также отключено, так как невозможно остановить зацикливание звука, воспроизводимого таким образом.

/**
* Plays the given AudioClip as a sound effect. Sound Effects cannot be stopped
* or paused/resumed, but can be played multiple times with overlap.
* Additionally, sound effects cannot be looped, as there is no way to stop
* them. This method of playback is best suited to very short, one-off sounds.
*/
AudioClip.prototype.playAsSFX = function() {
if (this.buffer_) {
var source = this.createGraph(false);
source.noteOn(0);
}
}

Остановка, пауза и состояние запроса. Остальные функции довольно просты и не требуют особых объяснений:

/**
* Stops an AudioClip , resetting its seek position to 0.
*/
AudioClip.prototype.stop = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.startTime_ = 0;
this.pauseTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Pauses an AudioClip. The offset into the stream is recorded to allow the
* clip to be resumed later.
*/
AudioClip.prototype.pause = function() {
if (this.playing_) {
this.source_.noteOff(0);
this.playing_ = false;
this.pauseTime_ = AudioClip.context.currentTime - this.startTime_;
this.pauseTime_ = this.pauseTime_ % this.buffer_.duration;
this.startTime_ = 0;
if (this.resetTimeout_ != null) {
    clearTimeout(this.resetTimeout_);
}
}
}

/**
* Indicates whether the sound is playing.
* @return {boolean}
*/
AudioClip.prototype.isPlaying = function() {
var playTime = this.pauseTime_ +
            (AudioClip.context.currentTime - this.startTime_);

return this.playing_ && (this.loop_ || (playTime < this.buffer_.duration));
}

Аудиозаключение

Надеюсь, этот вспомогательный класс будет полезен разработчикам, которые сталкиваются с теми же проблемами со звуком, что и я. Кроме того, такой класс кажется разумным началом, даже если вам нужно добавить некоторые более мощные функции API веб-аудио. В любом случае, это решение удовлетворило потребности Bouncy Mouse и позволило игре стать настоящей игрой HTML5 без каких-либо условий!

Производительность

Еще одна область, которая меня беспокоила в отношении порта Javascript, — это производительность. После завершения версии v1 моего порта я обнаружил, что на моем четырехъядерном настольном компьютере все работает нормально. К сожалению, с нетбуком или Chromebook дела обстояли немного хуже. В этом случае меня спас профилировщик Chrome, показав, на что именно тратится время всех моих программ. Мой опыт подчеркивает важность профилирования перед любой оптимизацией. Я ожидал, что физика Box2D или, возможно, код рендеринга станут основным источником замедления; однако большая часть моего времени фактически была потрачена на функцию Matrix.clone() . Учитывая сложную математическую природу моей игры, я знал, что мне пришлось много создавать/клонировать матрицы, но я никогда не ожидал, что это станет узким местом. В конце концов оказалось, что очень простое изменение позволило игре сократить загрузку ЦП более чем в 3 раза, увеличившись с 6-7% ЦП на моем настольном компьютере до 2%. Возможно, это общеизвестно разработчикам Javascript, но меня, как разработчика C++, эта проблема удивила, поэтому я расскажу об этом подробнее. По сути, мой исходный матричный класс представлял собой матрицу 3x3: массив из 3 элементов, каждый элемент которого содержал массив из 3 элементов. К сожалению, это означало, что когда пришло время клонировать матрицу, мне пришлось создать 4 новых массива. Единственное изменение, которое мне нужно было сделать, — это переместить эти данные в один массив из 9 элементов и соответствующим образом обновить мои математические вычисления. Это единственное изменение полностью ответственно за наблюдаемое мною трехкратное сокращение ЦП, и после этого изменения моя производительность стала приемлемой на всех моих тестовых устройствах.

Больше оптимизации

Хотя мое выступление было приемлемым, я все же заметил несколько незначительных сбоев. После небольшого профилирования я понял, что это произошло из-за сборки мусора Javascript. Мое приложение работало со скоростью 60 кадров в секунду, а это означало, что на отрисовку каждого кадра требовалось всего 16 мс. К сожалению, когда сборка мусора начиналась на более медленной машине, она иногда занимала ~10 мс. Это приводило к зависаниям каждые несколько секунд, поскольку для отрисовки полного кадра игре требовалось почти полные 16 мс. Чтобы лучше понять, почему я генерирует так много мусора, я воспользовался профилировщиком кучи Chrome. К моему большому отчаянию, оказалось, что подавляющее большинство мусора (более 70%) генерирует Box2D. Устранение мусора в Javascript — дело непростое, и о переписывании Box2D не могло быть и речи, поэтому я понял, что загнал себя в угол. К счастью, у меня все еще был один из самых старых приемов в книге: если вы не можете достичь 60 кадров в секунду, бегайте со скоростью 30 кадров в секунду. Все согласны с тем, что бегать со стабильной скоростью 30 кадров в секунду гораздо лучше, чем бегать с нервными 60 кадрами в секунду. На самом деле я до сих пор не получил ни одной жалобы или комментария о том, что игра работает со скоростью 30 кадров в секунду (это действительно трудно сказать, если не сравнивать две версии рядом). Эти дополнительные 16 мс на кадр означали, что даже в случае ужасной сборки мусора у меня все еще было достаточно времени для рендеринга кадра. Хотя работа со скоростью 30 кадров в секунду явно не разрешена API синхронизации, который я использовал (отличный requestAnimationFrame от WebKit), это можно сделать очень тривиальным способом. Возможно, это не так элегантно, как явный API, но можно добиться 30 кадров в секунду, зная, что интервал RequestAnimationFrame соответствует VSYNC монитора (обычно 60 кадров в секунду). Это означает, что нам просто нужно игнорировать все остальные обратные вызовы. По сути, если у вас есть обратный вызов «Tick», который вызывается каждый раз при запуске «RequestAnimationFrame», это можно сделать следующим образом:

var skip = false;

function Tick() {
skip = !skip;
if (skip) {
return;
}

// OTHER CODE
}

Если вы хотите быть особенно осторожными, вам следует убедиться, что VSYNC компьютера не находится на уровне или ниже 30 кадров в секунду при запуске, и в этом случае отключить пропуск. Однако я еще не видел этого ни в одной протестированной мной конфигурации настольного компьютера/ноутбука.

Распространение и монетизация

И последнее, что меня удивило в порте Bouncy Mouse для Chrome, — это монетизация. Приступая к этому проекту, я рассматривал HTML5-игры как интересный эксперимент по изучению перспективных технологий. Чего я не осознавал, так это того, что порт охватит очень большую аудиторию и будет иметь значительный потенциал для монетизации.

Bouncy Mouse была запущена в конце октября в Интернет-магазине Chrome. Выпустив продукт в Интернет-магазине Chrome, я смог использовать существующую систему для обнаружения, взаимодействия с сообществом, ранжирования и других функций, к которым я привык на мобильных платформах. Что меня удивило, так это то, насколько широк был охват магазина. В течение одного месяца после выпуска я достиг почти четырехсот тысяч установок и уже получал пользу от участия сообщества (сообщения об ошибках, отзывы). Еще одна вещь, которая меня удивила, — это потенциал монетизации веб-приложения.

У Bouncy Mouse есть один простой метод монетизации — рекламный баннер рядом с контентом игры. Однако, учитывая широкий охват игры, я обнаружил, что этот рекламный баннер способен приносить значительный доход, и в пиковый период приложение приносило доход, сопоставимый с моей самой успешной платформой, Android. Одним из факторов, способствующих этому, является то, что более крупные объявления AdSense, отображаемые в версии HTML5, приносят значительно более высокий доход за показ, чем меньшие объявления AdMob, отображаемые на Android. Мало того, рекламный баннер в версии HTML5 гораздо менее навязчив, чем в версии для Android, что обеспечивает более чистый игровой процесс. В целом я был очень приятно удивлен таким результатом.

Нормализованная прибыль с течением времени.
Нормализованная прибыль с течением времени

Хотя доходы от игры оказались намного лучше, чем ожидалось, стоит отметить, что охват Chrome Web Store все еще меньше, чем у более зрелых платформ, таких как Android Market. Несмотря на то, что Bouncy Mouse смогла быстро занять 9-е место в рейтинге самых популярных игр в Интернет-магазине Chrome, количество новых пользователей, приходящих на сайт, значительно замедлилось с момента первого выпуска. Тем не менее, игра по-прежнему стабильно развивается, и мне не терпится увидеть, во что превратится платформа!

Заключение

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