Картинка для привлечения внимания
Успокоившись после шока из-за того, что моя первая игра не взорвала интернет, я решил сделать еще одну. Первая мысль, с которой началась разработка второй игры, возникла в голове и без обдумывания стала краеугольным камнем в фундаменте игры: «если игра с бесконечным геймплеем не стала популярной, то нужно делать контенто-зависимую игру с уровнями».
Так как небольшой опыт создания игры у меня уже был, я вооружился не клавиатурой, как в прошлый раз, а карандашом и бумагой, и принялся «придумывать» игру. На разных листах А4 я записывал свои мысли по разным направлениям: игровые механики, сеттинг, интерфейс, правила, интересные мысли, запомнившиеся игры из детства и т. п.; при этом я параллельно шерстил Google Play Market в поисках вдохновения. В итоге, спустя несколько дней, было принято решение, что игра станет головоломкой, а именно — пазлом. За это время мой мозг и пальцы заскучали по программированию, в результате чего захотелось сделать хоть какой-нибудь прототип игры. Это сподвигло меня на более интенсивную проработку идеи пазла, и в итоге в качестве рабочего был принят вариант со следующими правилами: игровое поле состоит из квадратных кусочков пазла (далее они будут называться тайлами), на которых располагаются разноцветные линии; тайлы можно менять местами; цель игры: собрать пазл единственным образом, в котором линии одного цвета замкнуты в некий узор.
Совет №1: записывайте все, что приходит в голову (на бумагу, в ежедневник, в телефон (как временное хранилище мне понравился Google Keep), куда угодно), а потом многократно фильтруйте, отбирая самое ценное.
Прототип
Прототип был написан на связке html + js (использовался canvas) за пару вечеров. Т.к. раньше ничего серьезного на canvas я не делал, то было принято решение повелосипедить с целью более углубленного знакомства с этой технологией.
Рис. 1. Первая версия прототипа.
Выглядит не очень, знаю. Именно по этой причине вся моя мотивация быстро сошла на нет, а игра была заброшена на несколько месяцев. Заброшена, но не до конца: я продолжал записывать все игровые идеи, приходящие в голову, и иногда их перечитывал.
При этом, чтобы хоть как-то оправдать свое бездействие, я стал постоянно читать различные околоигровые статьи. В их поиске мне помогал Хабр (статьи и дайджесты игровой индустрии с подборками статей) и некоторые популярные сайты со статьями из мира разработки игр.
Совет №2: быстрый переход к программированию без достаточно проработанного бумажного прототипа может выбить из колеи надолго. Если это произошло и вам не удается заставить себя вернуться к проекту, то продолжайте работу косвенно: самые легкие варианты — чтение статей про игровую разработку и записывание мыслей.
Рисование — кардинальная смена деятельности
Спустя несколько месяцев бездействия на поприще создания игры, я уволился со своего постоянного места работы. Решение уйти зрело давно и никак не связано с созданием игры, но благодаря появлению свободного времени я смог вернуться к своему проекту. Снова вернувшись к игре я понял, что ее скудный внешний вид сильно демотивирует меня. С этой проблемой я справился вооружившись несколькими графическими редакторами: нарисовав чуть более красивое оформление я убил сразу нескольких зайцев:
1. Сменил род деятельности.
2. Повысил веру в себя (я сам нарисовал оформление к игре).
3. Придумал, что линии могут быть дорогами, а тайлы — участками земли. Так начала зарождаться суть игры.
4. Увидел, что центр тайла необходимо закрывать каким-нибудь изображением, т.к. разные варианты дорог некрасиво стыковались в центре. Так в центре тайлов появились дома.
Совет №3: отдыхайте от программирования, меняйте род занятий (проектирование на бумаге, продумывание монетизации, дизайна, игровых механик, чтение) иначе можно быстро охладеть к Идее. Смена рода деятельности на начальных этапах может обнаружить проблемы или тонкости, с которыми необходимо разбираться на старте проекта.
Рис. 2. Когда оформлением игры занимается программист.
Выглядит не сильно лучше, но если это помогает продолжать работу над проектом, то почему бы и нет.
Далее прототип был дополнен базовой функциональностью игры: были созданы экран со списком уровней и экран непосредственного прохождения уровней. Но для того чтобы проходить разные уровни, их сначала нужно создать. И я с удовольствием попробовал себя в новой роли — занялся созданием уровней.
Каждый уровень бережно сохранялся в json-формате до тех пор, пока размеры уровней не выросли и их стало сложно редактировать. Пришлось заниматься внеочередной задачей: созданием конструктора уровней. Это был простейший редактор, из которого я копировал json уровня и вставлял напрямую в код игры (бррр, извиняюсь перед всеми читателями за этот ужас). Но частые мысли о том, что код и архитектура игры (и архитектура конструктора в частности) не идеальны, заставляли меня переделывать многие куски игры заново, а иногда и по несколько раз.
Совет №4: перечитывайте свой код; ищите, что можно упростить, улучшить, ускорить. Это не только сохранит интерес, но и сделает ваш код клевым.
Развитие
Наступило лето и вместе с ним у меня возникло сильное желание найти работу. Я переехал на сайты с вакансиями, стал рассылать свое резюме пятилетней давности в разные компании и в процессе этого занятия осознал, что за 5 лет в отрасли изменилось многое, а я все это время жил в своем уютном мирке html и jQuery. Почитав требования к вакансиям и пообщавшись с рекрутерами, я понял, что я не знаю новинок, что не дает мне чувствовать уверенность в своих силах.
Нужно было что-то делать, и я взялся за самообразование, не забыв при этом про игру. Руководствуясь требованиями к вакансиям, я составил список технологий, инструментов и прочих фреймворков, с которыми, по моему мнению, мне стоило познакомиться, и начал их изучать. Сначала я инициализировал в директории с проектом репозиторий git. Спустя день работы я не мог представить, как я раньше работал без него. Затем начал разделять свой разросшийся суперобъект, в котором находилась вся логика игры, по модулям RequireJS. Это позволило избавиться от дублирования кода в игре и конструкторе (да здравствует DRY!), повысило читаемость кода, позволило сделать универсальный код на будущее для быстрого портирования игры в разные соцсети и, наконец, вернуло мне веру в мои силы. Я постепенно вычеркивал из списка все больше и больше пунктов (замыкания, Promises, Web Workers, gulp и т.д.), а смотреть на код становилось все приятнее и приятнее.
Совет №5: развивайтесь! Если этого не будете делать вы, то это будут делать ваши конкуренты, а вы отправитесь на свалку.
Прокрастинация
В какой-то момент времени меня подстерегла еще одна беда: я имею склонность к иррациональной прокрастинации, поэтому постепенно я стал все больше времени спускать впустую. Я решил взять себя в руки и попробовать какой-нибудь способ заставить себя работать. И тут мне попалась на глаза техника Pomodoro. Работает это так: четыре рабочих подхода по 25 минут с пятиминутными перерывами, после которых делаешь пятнадцатиминутный перерыв. Установив приложение на телефон, я не заметил, как проработал весь день без каких-либо проблем с мотивацией. На следующий день я доработал технику: договорился с собой, что перерыв должен проводить вне компьютера для отдыха глаз. Случилось чудо: за следующий день использования помидорной техники я починил/исправил/смазал/разобрал в доме столько всего, сколько не делал за целый год (субъективное ощущение). Естественно, в какой-то момент в перерывы стало нечем заняться и я придумал очередную хитрость: я стал отжиматься, приседать, делать упражнения на пресс (упражнения, которые можно делать дома без каких-либо тренажеров).
Совет №6: если страдаете прокрастинацией, рассеянностью, ленью, то пробуйте бороться с этим различными способами. Удовлетворение от победы над вредными привычками доставляет исключительно приятные эмоции, при этом пропадают проблемы с мотивацией и фантазией.
Спуск с небес
Мой внутренний перфекционист постоянно твердит мне, что я должен сделать игру в одиночку. Если бы я его слушал, то я бы никогда не запустил бы эту игру. К июлю меня перестала вдохновлять графика, нарисованная мной, и я созрел для того, чтобы искать художника. Сначала я нашел примеры изображений, которые мне нравятся, затем составил ТЗ своими словами и прошел следующие круги поиска иллюстраторов:
1. Хабр. Писал людям, которые делают игры или рисуют. Итог: безрезультатно.
2. Сообщества художников/иллюстраторов Вконтакте. Результат: нашел художницу, которая пропала после месяца работы оставив меня с незавершенными эскизами. Итог: безрезультатно.
3. Специализированные сайты, на которых можно посмотреть работы художников и связаться с ними: на одном из сайтов нашел много подходящих кандидатур, отправил ТЗ, отобрал двоих (плюс впоследствии на этом же сайте нашел еще одного), договорились о совместной работе, спустя месяц у меня был готов интерфейс и два мира (два стиля оформления экрана уровней и экрана игры).
Рис. 3. Заказанное у художников оформление. UPD: Используется шрифт по умолчанию.
Совет №7: ищите художника сразу после создания прототипа для создания эскизов (за деньги / за интерес), если не считаете себя профессиональным иллюстратором. Это здорово подогревает интерес. С момента появления профессионального оформления игры у меня не было проблем с мотивацией и работоспособностью.
Интересные факты
Шрифты
Шрифт для игры был найден за 5 дней до запуска игры, до этого в игре использовался Tahoma. Сначала я думал, что поставлю задачу по подбору шрифта художнику, но потом поискал на Хабре статьи про шрифты и, взяв на вооружение пару сайтов с бесплатными шрифтами, принялся за подбор самостоятельно. Это заняло 2 дня и принесло в копилку десяток приятных шрифтов, и один из них теперь радует глаз пользователей.
Рис. 4. Экран игры Ледяного мира с подобранным шрифтом.
UPD:
Рис. 5. Экран уровней Древнего Рима с подобранным шрифтом.
Музыка
На одном сайте, где можно найти исполнителя практически на любую задачу, я нашел человека с большим количеством положительных отзывов и заказал у него фоновую музыку и несколько эффектов. Через 2 дня я получил все, что нужно, оплатил работу исполнителя и понял, что музыку нужно как-то воспроизводить. Несмотря на небольшой опыт в данной сфере, я отказался от велосипедирования и принялся за поиск готовых решений. Я сравнил два варианта (SoundManager2 и Ion.Sound) и выбрал наиболее понравившийся мне (Ion.Sound). Тестирование и выбор заняли еще 2 дня.
В уровне должно быть только одно решение
Как я упомянул ранее, цель игры начинается словами: «собрать пазл единственным образом». В этой формулировке скрылась самая увлекательная задача из всех, с которыми мне пришлось столкнуться в процессе создания игры. При этом она заняла очень долгое время: полировка алгоритма проверки количества возможных комбинаций сбора уровня постоянно не давала мне покоя.
Постановка задачи: есть поле из N клеток, на которых расставлены N тайлов. На каждом тайле могут быть дороги разных типов и разных направлений. Поле считается собранным, если тайлы расставлены так, что каждая дорога соединяется с соседней дорогой такого же типа.
Поле должно собираться единственным образом по нескольким причинам:
1. Чтобы подсказки работали корректно.
2. Чтобы не было претензий и споров о том, как именно выглядит собранный уровень.
Начиная создавать уровни без конструктора, я упустил пару моментов, в которых уровни могли быть собраны несколькими способами. Поэтому немного подумав (к сожалению, немного), я сваял первую версию автосборщика уровня, который рекурсивно перебирал все возможные комбинации расположения (перестановки) тайлов и в конце выдавал количество перестановок, при которых уровень был собран. На первых порах, когда количество тайлов в уровне было 9-12 этот алгоритм отрабатывал менее чем за секунду, но дойдя до 16-тайловых уровней, я столкнулся с очень долгим временем выполнения. Напомню, что все это работает в браузере на js в одном потоке. В отдельных случаях, когда я оставлял выполнение алгоритма на ночь, вкладка браузера могла повиснуть надолго и больше не отвечать. Чтобы не засорять место в статье, последующие оптимизации я опишу списком:
1. Добавил массив уже проверенных перестановок (сохранял в него превращенный в строку json) и не проверял дважды одинаковые варианты.
2. Переписал рекурсию циклами — время исполнения значительно не изменилось.
3. Стал думать, как избавиться от N! со всеми перестановками. Вспомнил, как придумывал в детстве в голове алгоритм для решения судоку. Стал рисовать алгоритм на бумаге и параллельно наткнулся на статью Алгоритм Х или что общего между деревянной головоломкой и танцующим Линком?. Реализовал алгоритм X (ничего принципиально нового я не реализовал, поэтому если интересно описание алгоритма, то с ним можно ознакомиться в выше указанной статье).
4. Сделал «кэширование» всех возможных характеристик необходимых для работы алгоритма.
5. Переписал алгоритм X циклом вместо рекурсии — время исполнения значительно не изменилось.
6. Заменил массив уже проверенных перестановок деревом, в котором каждый i-й уровень вложенности соответствует всем проверенным тайлам в i-й позиции. Субъективно это сильно уменьшило время работы алгоритма в лучшем случае.
7. Попробовал решать в несколько потоков, используя Web Workers — время исполнения увеличилось на несколько секунд.
В результате всех оптимизаций удалось добиться времени сбора уровня с 25 тайлами до полутора минут. Следующая мысль: переписать алгоритм на языке программирования, умеющем работать с многопоточностью.
gulp.watch
На этапе активной разработки я запускал в консоли gulp watch и столкнулся с загадочным для себя поведением. Все работало корректно, пока я не начал править css. После запуска watch после 2-3 изменений css-файла gulp переставал сохранять выходной файл (он создавал в нужной директории пустой файл с нужным названием и на этом все заканчивалось). Поиски решения проблемы заняли полдня и ни к чему не привели. Остальные файлы обрабатывались корректно. Если кто-то сталкивался и сможет объяснить в комментариях, буду рад:
gulpfile.js
При удалении строк с cssmin и rename ситуация не менялась.
var cssmin = require('gulp-cssmin'),
rename = require('gulp-rename');
gulp.task('css', function() {
gulp.src(['css/style.css'])
.pipe(cssmin())
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest('build/'));
});
gulp.task('watch', function() {
gulp.watch('css/style.css', function() {
gulp.run('css');
});
});
При удалении строк с cssmin и rename ситуация не менялась.
RequireJS и gulp
Благодаря RequireJS мне удалось добиться модульности проекта, но перед запуском я понял, что нужно оптимизировать код, поэтому мне пришлось повозиться с разными вариантами оптимизации проекта на RequireJS при помощи gulp. Был глобально установлен requirejs. Далее в качестве кандидатов на использование в gulp были рассмотрены следующие модули: gulp-requirejs, gulp-requirejs-optimize и gulp-shell. Примеры использования:
gulp-requirejs:
var rjs = require('gulp-requirejs');
gulp.task('optimize', function() {
rjs({
name: 'script',
baseUrl: '.',
out: 'build/script.js'
});
});
gulp-requirejs-optimize:
var requireJSOptimize = require('gulp-requirejs');
gulp.task('optimize', function() {
gulp.src('script.js')
.pipe(requireJSOptimize(function() {
return {
name: "script",
baseUrl: '.'
}
}))
.pipe(gulp.dest('./build/'));
});
gulp-shell:
var shell = require('gulp-shell');
gulp.task('optimize', shell.task(['r.js -o baseUrl=. preserveLicenseComments=false name=script out=build/script.js']));
В итоге я выбрал вариант с gulp-shell, т.к. он единственный показывал в консоли корректное время выполнения задачи, и, соответственно, при использовании в зависимостях другой задачи, отрабатывал корректно. Время сборки итогового js-файла для одной социальной сети составляет 7-8 секунд.
VK API
Так как с прошлой игрой я намучился с изменением кода при попытке размещения игры в Одноклассниках и Фейсбуке, то в этом проекте на начальном этапе разработки я знал, что все вызовы API нужно выносить в отдельный модуль. RequireJS позволил мне сделать отдельный стартовый js-файл для социальной сети, в котором определяются функции и методы, которые вызывают различные методы API социальной сети. Часть стартового файла:
requirejs.config({
baseUrl: '.',
paths: {
'vk_api': '//vk.com/js/api/xd_connection.js?2'
}
});
requirejs([ 'vk_api', 'script' ], function(vk_api, script) {
script.environment = 'vk';
/**
* Tests social network's API
* @return Promise
*/
script.testSocNetwork = function(callback) {
return new Promise(function(resolve, reject) {
VK.init(function() {
if(typeof callback ==='function') {
callback();
}
resolve();
}, function() {
reject();
});
});
}
/**
* Shows invite friend dialog
*/
script.inviteFriends = function() {
VK.External.showInviteBox();
}
/**
* Gets array with IDs of friends and call callback with this array by argument
* @return Promise
*/
script.getFriendsIds = function(callback) {
return new Promise(function(resolve, reject) {
VK.api('friends.getAppUsers', function(data){
if(!('response' in data)) {
reject();
return;
}
var response = [];
for(var f in data.response) {
response.push(data.response[f]);
}
if(typeof callback === 'function') {
callback(response);
}
resolve();
});
});
}
/**
* Shows buy ingame money dialog
*/
script.buyMoney = function(type) {
VK.callMethod('showOrderBox', {type: 'item', item: type});
}
/**
* Shows request dialog
*/
script.makeExorcismWithPerson = function(uid, money) {
VK.callMethod('showRequestBox', uid, script.i18n('makeExorcismWithPerson', {'MONEY_COUNT': money, 'MONEY_CASES': script.i18n('numeralForms')(money, script.i18n('casesMoney'))}));
}
/**
* Adds callback for event onOrderSuccess
*/
script.bindOnOrderSuccess = function(callback) {
VK.addCallback('onOrderSuccess', function() {
if(typeof callback === 'function') {
callback();
}
});
}
//...
//Initializes game
script.init();
});
Таким образом возможно быстро разворачивать игру в других социальных сетях, заменяя функции API под конкретные случаи.
Место, в котором я затупил на час в VK API
В коде стартового js-файла приведены примеры вызова двух методов Client API: showOrderBox и showRequestBox. Из-за моей невнимательности и замылившегося глаза я упустил тот момент, что в первом случае в метод VK.callMethod вторым аргументом передается ассоциативный массив с параметрами, а во втором параметры передаются вторым и третьим аргументами.
Интернационализация
В коде вызова метода showRequestBox можно увидеть использование метода, возвращающего тексты для вывода с учетом выбранного языка. На данный момент в игре есть только один язык интерфейса, но на будущее задел есть.
Структура такова:
В суперобъекте script есть следующие свойства:
language — строка, в которой хранится двухсимвольное наименование текущего языка;
phrases — объект, в котором по двухсимвольным ключам хранятся объекты с фразами на соответствующем языке и функцией, возвращающей нужную форму слова в зависимости от предстоящего числительного.
Проще показать пример:
script.language = 'ru';
script.phrases = {
'ru': {
'numeralForms': function(number, titles) {
var cases = [2, 0, 1, 1, 1, 2];
return titles[ (number % 100 > 4 && number % 100 < 20)? 2 : cases[(number % 10 < 5) ? number % 10 : 5] ];
},
'ok': 'Хорошо',
'makeExorcismWithPerson': 'За изгнание вы получили #MONEY_COUNT# #MONEY_CASES#!',
'casesMoney': ['молнию', 'молнии', 'молний']
},
'en': {
'numeralForms': function(number, titles) {
return titles[ (number === 1)? 0 : 1 ];
},
'ok': 'OK',
'makeExorcismWithPerson': 'You get #MONEY_COUNT# #MONEY_CASES# for exorcism!',
'casesMoney': ['lightning', 'lightnings']
}
};
Метод script.i18n при вызове возвращает соответствующую фразу текущего языка либо функцию numeralForms, если первым аргументом задана она. Вторым аргументом можно передать ассоциативный массив с шаблонами, которые будут подставлены в фразу. Примеры:
var money = 10;
script.language = 'ru';
script.i18n('ok');
// вернет строку 'Хорошо'
money = 123;
script.i18n('makeExorcismWithPerson', {'MONEY_COUNT': money, 'MONEY_CASES': script.i18n('numeralForms')(money, script.i18n('casesMoney'))});
// вернет строку 'За изгнание вы получили 123 молнии!'
money = 1;
script.language = 'en';
script.i18n('makeExorcismWithPerson', {'MONEY_COUNT': money, 'MONEY_CASES': script.i18n('numeralForms')(money, script.i18n('casesMoney'))});
// вернет строку 'You get 1 lightning for exorcism!'
Монетизация
Монетизация в игре была реализована следующим образом: за реальные деньги можно покупать внутриигровую валюту — молнии, за которые можно покупать подсказки и хроны — переходы к следующему уровню.
Запуск
Первоначальная дата релиза была установлена на 31 октября. Естественно, к этому сроку игра не была готова. Точнее, по моему мнению, она не была готова. Выделив из списка задач приоритетные, без которых запуск был бы невозможен, я принялся за активную работу. За две недели мне удалось отрефакторить и сделать все то, что было необходимым и 16 ноября я подал заявку на размещение игры Вконтакте для дружеского бета-теста. На мое удивление игра была одобрена примерно через час после подачи заявки, о чем я был уведомлен смс-сообщением. Потестировав игру на товарищах в течении трех дней, я подал заявку на бесплатное размещение в блоке «Новые приложения». При подаче заявки нужно выбрать дату размещения: ближайшей датой был понедельник, 23 ноября — его я и выбрал.
Проснувшись утром 23 ноября я первым делом побежал к ноутбуку, чтобы убедиться, что я сделал очень популярную игру. Увиденное вызывало смешанные чувства: с одной стороны, за ночь у меня появилось 3000 участников, а с другой, игра не работала. Быстро оценив масштаб бедствия, я обнаружил следующее: доступ к серверу по ssh можно было получить только через putty, ни Eclipse, ни WinSCP не подключались к серверу (причем по логам оказалось, что соединение устанавливалось и все, дальше ничего не происходило), nginx очень медленно отдавал статику, а apache, выполняя php, долго отвечал на запросы. Позвонил знакомому администратору и попросил посмотреть в чем дело, а сам параллельно полез смотреть характеристики своего VPS и возможность апгрейда. Посмотрев сервер, знакомый сказал, что характеристики сервера достаточны для описанной нагрузки. Как оказалось, при покупке VPS я упустил из виду пункт Ширина канала. Для моего тарифного плана выделялось 10 Мбит/с. Пришлось экстренно покупать и настраивать новый VPS у другого хостера с каналом 100 Мбит/с и переносить туда игру. В итоге, из-за необходимости отлучиться по семейным делам, эта процедура была закончена лишь к вечеру 23 ноября.
Совет №8: проверяйте все характеристики своего хостинга/VPS/личного сервера.
Новые приложения
Отдельно стоит рассказать про этого спонсора бесплатного трафика. На странице приложений есть блок «Новые приложения», в котором показывается по 4 игры на странице, отсортированные по убыванию даты размещения в этом блоке. Т.е. 23 ноября моя игра была первой, 24 — второй, 25 — третьей, 26 — четвертой, соответственно, а 27 игра оказалась на второй странице, что вместе с пятницей резко снизило количество переходов по ссылке. В последствии два раза я наблюдал добавление двух приложений в день, что говорит о том, что ваше приложение может оказаться на второй странице быстрее, чем за четыре дня.
Совет №9: лучше всего размещаться в блоке Новые приложения в понедельник, чтобы получить большее количество участников.
Совет №10: если понаблюдать на выпущенные после моей игры, то можно сделать вывод, что аудитория Вконтакте любит игры про драгоценные камни и сокровища.
Цифры
На Хабре любят цифры, их есть у меня.
Потраченные деньги
Наименование | Рубли |
---|---|
13 месяцев VPS у двух разных хостеров | 4240 |
Вся графика | 61300 |
Музыка | 3780 |
Вся реклама | 700 |
Итого | 70020 |
Заработанные деньги
109 голосов = 317,19 рублей без договора = 385,86 рублей с договором = 697,60 рублей для рекламы приложения без вывода голосов из Вконтакте.
Статистика посещений
Статистика установок/удалений игры
Статистика количества участников
Статистика источников установок игры
Статистика по полу и возрасту посетителей
Статистика по географии посетителей
Статистика по браузерам
Статистика по разрешению экрана
Вывод
Игры, да и любые свои проекты, делать нужно обязательно, ведь это не только новые знания, профессиональное развитие, прокачка инженерных, организаторских, дизайнерских способностей, но и весело.
UPD: прочитав комментарии решил дополнить статью следующими постулатами:
1. Я в курсе сколько денег потратил и сколько заработал, и специально пишу об этом, потому что такая информация — редкость.
2. Что двигало мной при создании игры: люблю головоломки, мне нравится заниматься созданием игр, я устал от засилья выкачивающих деньги игр, хочу получить разносторонний опыт.
3. Цель статьи: ответить на некоторые вопросы, которые возникают у начинающих разработчиков, и помочь справиться с различными трудностями, возникающими в процессе разработки.
4. Пользуясь случаем, хотел бы попросить людей, прошедших путь создания социальной игры, дополнить мой рассказ своими примерами.