Первая разработка и публикация игры в соцсетях

в 15:53, , рубрики: cocos2d-javascript, Facebook, game development, Gamedev, javascript, Вконтакте, одноклассники, разработка
Идея игры

У меня было давнее желание разработать полноценную игру для социальных сетей, но не было интересных идей. Однажды я увидел в аппсторе игру в слова, которая была только на английском языке. Мне захотелось сделать аналогичную с русским словарем.

Смысл игры – составлять слова из соприкосающихся гексагонов. В игре 3 режима:

  • игра против времени (при поиске слов добавляется 1 секунда за кажду букву);
  • найти выход за отведенное время (найти слова и тем самым проложить путь к выходу из центра);
  • свободный режим (поиск слов и завершение в любое время).

Внешний вид

image

Монетизация

У оригинальной игры монетизация заключалась в том, что пользователю доступен 1 тип игры из трех. Остальные разблокируются после 50 и 100 сыгранных игр.

Я решил делать аналогичную монетизацию. Но в последствии добавил еще функционал подсказок слов. Подсказки можно докупать.

Дополнительная реклама:

  • ВК – сервис знакомств и прелоадер
  • ОК – система управления офферным трафиком
Затраты времени

Разработка велась с нуля (включая изучение фреймворка) в свободное от основной работы время, в первый месяц очень активно.

График затрат чистого времени на разработку (не включает время на документацию и форумы):

image

Теория

В первую очередь перед разработкой игры мне пришлось изучить особенности математики с гексагонами. Отличная статья, которая покрывает всё что мне надо было знать www.redblobgames.com/grids/hexagons/

Выбор среды разработки

Разработка была разбита на 2 части – на 2 автономных проекта:

  1. JS код самой игры;
  2. PHP код, для сохранения/выдачи статистики игроков и доп файлы для взаимодействия с социальными сетями.

Изначально была задумка используя кроссплатформенный фреймворк разработать игру для браузера и затем портировать в ios и android. Поэтому выбор пал на фреймворк cocos2d версию html5.

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

Особенностью данного фреймворка является то, что на экране всё разделено на слои и объекты-спрайты. Которые по умолчанию отрисовываются с частотой 60 кадров/сек. Можно также активировать принудительно или автоматический выбор webgl режима, который задействует аппаратное ускорение для графики.

Графика

Всю графику я заказывал на фриланс сайте после того, как был готов прототип. Отдельно пришлось заказать иконку, которая стала более выигрышной.

Некоторые технические моменты и особенности разработки

Проблема 1 – кеширование спрайтов

Если посмотреть на основной экран игры, то можно увидеть множество объектов – это гексагоны и буквы. В двух играх их общее количество насчитывает порядка 5 тыс. Это вызвало первую проблему – фреймворк все их перерисовывает 60 раз в секунду и жутко тормозит.

Для решения подобной проблемы нашлось одно решение:

  1. Использование букв как картинок;
  2. Объединение всех картинок в один файл. С помощью программы Texture Packer все картинки были упакованы в 2 png файла. Один файл использовался для изображений в главном меню, второй – в самой игре. Она сохраняет одну общую картинку со спрайтами и дополнительно прикладывает xml файл с описанием границ каждого спрайта. Это все импортируется фреймворком и сохраняется в памяти;
  3. Использование специального контейнера SpriteBatchNode. Согласно документации все спрайты, включенные в SpriteBatchNode будут отрисовываться в едином вызове отрисовки WebGL, а все спрайты, не включенные в этот компонент отрисовываются каждый по отдельности. Обязательно все файлы, добавленные в контейнер, должны находится в одном файле.
    Пример кода, который использует контейнер SpriteBatchNode

    var size = cc.director.getWinSize();
    var x0 = size.width / 2 - (3 / 4 * this.game.tileWidth * this.game.size) / 2;
    var y0 = size.height / 2 + this.game.tileHeight * this.game.size / 2;
    var hex = cc.textureCache.addImage(res.Game_png);
    this._hexBatch = new cc.SpriteBatchNode(hex, 50);
    this._tableLayer.addChild(this._hexBatch);
    for (var i = 0; i < this.game.size; i++) {
        for (var j = 0; j < this.game.size; j++) {
            var layer = this.game.table[i][j];
            if (!layer || layer == '*') {
                continue;
            }
            var height = Math.sqrt(3) / 2 * layer.width - 2;
            var width = layer.width - 2;
            var x = x0 + i * 3 / 4 * width;
            var y = y0 - j * height / 2 - (j + 1) * height / 2 - (i % 2) * height / 2;
            var border = new cc.Sprite('#' + layer.hexagon_border);
            border.x = x;
            border.y = y;
            this._hexBatch.addChild(border);
            layer.attr({
                x: x,
                y: y
            });
            this._hexBatch.addChild(layer);
        }
    }
    this.addChild(this._tableLayer);
    this.wordPreviewLayer = WordPreviewLayer.create();
    this.addChild(this.wordPreviewLayer);
    

    Единственная проблема – это имеет смысл только при активном WebGL у пользователя, без наличия аппаратного ускорения, оно не будет работать аналогичным образом.

Проблема 2 – события

Весь проект поделен на отдельные компоненты, каждый из которых лежит в отдельном классе и файле. Напрямую они друг с другом не контактируют. Взаимодействие реализовано с помощью глобальных событий. Cocos2d содержит класс EventManager для работы с событиями.

Было создано некоторое количество собственных событий.

Пример инициализации событий

var Timer = cc.LabelBMFont.extend({
    started: false,
    paused: false,
    lastTime: null,
    timerLength: 0,
    alert: 20,
    listeners: [],
    init: function () {
        this.listeners.push(cc.eventManager.addCustomListener(EVENT_TIMER_START, function (data) {
            this.start(data.getUserData());
        }.bind(this)));
        this.listeners.push(cc.eventManager.addCustomListener(EVENT_TIMER_STOP, function () {
            this.stop();
        }.bind(this)));
        this.listeners.push(cc.eventManager.addCustomListener(EVENT_TIMER_PAUSE, function () {
            this.pause();
        }.bind(this)));
        this.listeners.push(cc.eventManager.addCustomListener(EVENT_TIMER_CONTINUE, function () {
            this.resume();
        }.bind(this)));
        this.listeners.push(cc.eventManager.addCustomListener(EVENT_TIMER_ADD, function (data) {
            var seconds = data.getUserData();
            this.add(seconds);
            cc.eventManager.dispatchCustomEvent(EVENT_TIMER, this.timerLength);
        }.bind(this)));
        this.listeners.push(cc.eventManager.addCustomListener(EVENT_TIMER_END, function (data) {
            this.timerLength = 0;
            this.stop();
        }.bind(this)));
        return true;
    },
    cleanup: function() {
        this._super();
        for (var i in this.listeners) {
            cc.eventManager.removeListener(this.listeners[i]);
        }
    },

Пример вызова события:

cc.eventManager.dispatchCustomEvent(EVENT_TIMER_START, this.timer_start);

При работе с кастомными событиями тоже есть некоторые особенности: после того как объект будет уничтожен, eventManager все равно будет его считать подписавшимся. Для решения этой проблемы для каждого объекта заполняется локальный массив listeners, и при уничтожении объекта всегда вызывается метод-деструктор cleanup, в котором я удаляю добавленные кастомные методы.

Проблема 3 – мобильная версия

С мобильной версией игры так и не сложилось.
Во-первых, игра запускается в мобильной среде в эмуляторе SpiderMonkey, в котором по какой то причине не заработало кеширование. Из-за чего игра ужасно тормозила на айпаде.

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

Затею портирования для ios и андроид в данный момент отложил.

Ачивки

Позже в игре были добавлены 50 ачивок. Работа с ними оказалось не такой сложной. Для реализации ачивок создаем массив, который содержит параметры и метод init для инициализации обработчика, который будет фиксировать выполнение условия ачивки:

Пример

var achievements = [
    {
        id: 'the_graduate',
        name: t('The Graduate'),
        description: t('You completed Tutorial'),
        game: 'Tutorial',
        icon: 'graduate.png',
        pts: 1,
        init: function () {
            var event = cc.eventManager.addCustomListener(EVENT_TUTORIAL_END, function () {
                sendAchieve(this, event);
            }.bind(this));
        }
    }, {
…
}];

И добавляем инициализацию всех ачивок, которые пользователь еще не заработал.

Код

cc.eventManager.addCustomListener(EVENT_GAME_LOADED, function () {
    for (var i in achievements) {
        if (!cc.UserData.achievements || cc.UserData.achievements.indexOf(achievements[i].id) == -1) {
            achievements[i].init();
        }
    }

    cc.eventManager.addCustomListener(EVENT_SEND_ACHIEVEMENT, function(data) {
        var achieveData = data.getUserData();
        if (!cc.UserData.achievements) {
            cc.UserData.achievements = [];
        }
        cc.UserData.achievements.push(achieveData.id);
    })
});

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

Поиск слов

При загрузке игры с сервера подгружается словарь слов, по которому осуществляется поиск при выделении каждой буквы. Первоначально использовался простой массив со словами, что сказывалось на общей производительности, т.к словарь содержит порядка 150 тыс. слов. Занявшись поиском решения этой проблемы, я наткнулся на понятие Trie или Префиксное дерево. В результате исходный массив был сконвертирован в файл в формате trie данных. Данное решение позволило заметно уменьшить тормоза при выделении каждой из буквы слова.

Сборка проекта

Перед тем, как передать js исходники во второй проект, необходима сборка и минификация всех файлов в один js файл. Для этого в cocos2d уже есть готовый build.xml файл, который был дополнен. И в результате сборка производится одной командой в консоли – ant. Готовый файл просто копируем во второй проект.

Сбор статистики и интеграция с социальными сетями

В качестве бекэнда выбор пал на symfony 2 и бд mongo только потому, что для меня это быстрый и простой способ реализовать бекэнд часть.

Здесь можно выделить только одну особенность – для каждой соц сети был создан отдельный файл интеграции и build.xml создавал 3 минифицированных файла для каждой социальной сети.

Оптимизация веб сервера

Весь проект размещается на одном vps сервере от digitalocean со стандартным тарифом за 10$: 1 core cpu, 1Gb ram, 30Gb ssd. ОС debian, веб сервер nginx + php-fpm. БД mongodb.

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

gzip_static  on; 

Для каждого js файла скрипт build.xml создавал сжатую версию с расширением .gz В результате nginx просто брал эти файлы и отдавал клиенту без какой либо нагрузки на процессор.

Статистика

Игра в первую очередь была запущена в ВК. На следующий день после одобрения игра была добавлена в раздел новые.
Отдельный всплеск был через 2 недели, когда я заказал новую иконку, которая смотрелась более логично.

График

image

С Фейсбуком не очень понятная ситуация. Все же мне кажется, туда без рекламы бессмысленно лезть — в каталоге много мобильных игр.

Статистика следующая. В начале января виден явный всплеск, который резко ушел в ноль.
Для перевода заработанных средств необходимо предоставить документы. Здесь тоже я не понял – можно зарегистрироваться как частное лицо, но они просят еще ОГРН.

График

image

Для запуска игры в ОК необходимо иметь ИП или юрлицо. Но можно найти людей, которые уже имеют свои игры в ОК и через них опубликоваться, договорившись на сумму от 10% перечисленных на р/с счет (ОК забирает себе чуть больше 50%).

В ОК статистика выглядит интереснее:

График

image

Общая статистика гугла с момента запуска:

График

image

Также у Гугла есть возможность сохранять события. Например, есть такая статистика по событиям в игре:

График

image

Статистика нагрузки на CPU:

График

image
Итого

Затраты

  • Разработка заняла порядка 200 часов свободного от работы времени;
  • Дизайн – 5500 руб.
«Доходы»

ВК:

  • Пользователей – 31,000 человек;
  • Платежи за весь период – 84 голоса;
  • Доход от рекламы – 1500 руб.

ОК:

  • Пользователей – 171,500 человек;
  • Платежи за январь – 27660 ОК;
  • Доход от рекламы (за 2 недели) – 2600 руб.

ФБ:

  • Пользователей – 12,900 человек;
  • Платежи – $4,70.

Итоги

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

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

Автор: galmi

Источник


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


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