Как я делал коронаигру на Corona SDK
$google = "We don't allow apps that lack reasonable sensitivity towards or capitalize on a natural disaster, atrocity, conflict, death, or other tragic event";
mysql_query("UPDATE cvirus_users SET winners = winners+19, message='$google' WHERE imea='$imea' ");
Без паники, парни, статья не заразная! К тому же её автор умер еще в прошлом году. В этом, правда, воскрес. Ох! А вокруг — засада, работать нельзя, в районе — эпидемия. Остается одно — писать игры.
Ладно, сделал десяток казуалок. Опубликовал сами-знаете-где (Apple меня забанил на год) и призадумался.
Рубятся в новые игры не более 7 пользователей в день. И преодолеть магическую 7-ку ни одна из них не в силах. Мое честолюбие шепнуло: — А слабо придумать игру, чтобы в неё залипло больше 7 человек? Скажем, тысяча?
А не слабо.
Как пришла идея
Не в ванной в этот раз, но в процессе кликанья свежеиспеченных приложений. Две игры было карточных, одна — трехмерный BlockOut II (ветераны помнят), еще две на кубиках (yatzee и doodo) и, внимание!, пара головомоек за битву смайликов против корона-вирусов.
Последние две игры родились благодаря анимированным стикерам, которые мне прислал Telegram-бот. В них няшные смайлики прятались от забавных вирусов.
Отлаживая последнюю игру, я поймал баг, который сначала меня разозлил, потом рассмешил, а затем из него получился хит. Вывод — пишите небрежно, боты.
Движок
Уже года 3, как я подсел на Corona SDK. Я умудрялся писать на нем трехмерные игры и даже был в штате компании, однако в 2018 всех бездельников разогнали, а движок ушел в open-source. Напомню, что Corona SDK — это кросс-платформенный 45 (смотри значение в hex) движок под все мобилы, десктопы и ХТМЛ5.
Передаю приветы Северной Америке, Украине, Робу, Владу и спасибо за классный продукт. Коммичу ошибку: при билде на 11-ый iPhone из симулятора на телефон приложение не ставится. Приходится использовать обходной путь Xcode→Window→Devices→Drug and Drop.
Да, господа, движок очень шустрый для разработки, прототип игры делается за пару часов, затем столько же дней шлифовки и продукт готов. Для сравнения, публикация на Хабре заняла в два разатри раза четыре раза больше времени.
Арт
Повторю, картинки взял из анимированных стикеров в Телеграмме — там они хранятся в tgs формате, в самом деле это переименованный gzip, он разархивируется в json-файл и выглядит примерно так:
{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"05_STAYHOME_OUT","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm"
Сгоряча, я стал было изобретать собственную программу для отрисовки json-а, но, обжёгшись, плюнул в интернет. И получил в ответ телеграмм-бота #GIF Export Bot, конвертирующего tgs-стикеры в GIF файл. Боже, какой я старый, помню GIF-а еще вот таким маленьким… Каждая GIF-ка хранит примерно 100–200 фреймов размером 512×512. На GIF-ку пускается GIMP.app, затем скейл, трансформ, ну вы знаете, главное не залипнуть на редактировании арта…
Вот и один из отрицательных героев. Короновирус — отъе… ись © Слепаков.
Гейм и физика
В Corona SDK волшебный, невероятный box2D движок. Смотрите, как пишутся на нем игровое поле и ко-вирусы
physics.start()
physics.setGravity( 0, 0 )
physics.addBody( downBox, "static", {shape=rect } )
physics.addBody( cvirus, "dynamic", {shape=octogon, density=2,
friction=1, bounce=0.2 } )
physics.setGravity( 0, 9.8 )
-- тут играем
physics.stop()
-- а тут уже не играем
В принципе, добавить что-либо к этому куску кода мне нечего — вы все поняли.
Сама идея игры
Помните хит Панда? Там (в поле силы тяжести) мишек-панд и мамочку разлучают цветные кубики. Их надо убить (кубики, а не мишек), но они противные множатся, как головы лирнейской гидры и не хотят соединения банды Панды. Банды панды. Сергеич рифму бы одобрил. Так вот, цветные кубики в этой игре расположены на равномерной ортогональной регулярной сетке. Лет пять назад Панда была бестселлером, даже я написал к ней ремейк Hex Rabbit. На такой же сетке, только шестиугольной и с такими же пандами, только кроликами. Я удивился, что кроликов до сих пор гоняют — семь (черт побери!) человек.
Проверить мои слова вы не сможете, потому что Apple меня забанил на год.
Hex-кроликов тогда нарисовал мне художник Андрюха Чесноков. Светлая ему память. Хотя он жив, слава Богу, погнали дальше.
Итак? Скажите, а зачем вообще сетка в этой панда-игре? Уберем её прочь, добавим физику. Злобные кубики заменим на восьмиугольные квадраты (да простит меня профессор Пономарев и аналитическая геометрия) и банду панд превратим в колобков.
Ко-вирусы против ко-лобков.
Неприлично, но смешно. Смешно-то оно смешно, а в итоге получилось здорово. Гоняю колобков вторую неделю, жду когда надоест. И не забываю про остальные игры… Спасибо самоизоляции, времени хватает.
Дебаг физики
Для отладки физики в Corona есть соответствующая функция, вот такая
physics.setDrawMode( "debug" )
В этом режиме схематично изображены элементы физического мира
Кстати, здесь был косяк — стенки стакана изначально задавались не очень высокими (три экрана) и на некоторых уровнях колобки вываливались наружу. Я недоумевал — куда они подевались? Наверное, до сих пор летят к центру Земли… Хотя нет, скорее всего, уже долетели с учетом сопротивления воздуха K_{air} = 0.75.
Выключаем debug-режим
-- physics.setDrawMode( "debug" )
и получаем арт
Признаюсь вам, как художник художнику — анимацию спрайтов я ограничил двумя кадрами — добавил моргание героев, и хорош. Пусть Киса Воробьянинов рисует, я лично — пас.
Редактор уровней
Кнопочки на экране выбора уровней нарисовал сам, в коронавирус стиле.
Изначально определил 12 уровней, думая постепенно увеличивать на доске число ко-вирусов и кроликов, которые #онижеколобки.
Рассуждал примерно так:
На первых двух уровнях живут только 2 вида злодеев. Зеленые самые опасные © ДМБ.
На следующих двух — посложнее, живут 3 вида злодеев.
Затем — 4 вида.
Пять — планету не спасти.
Ш (Ж)есть — импосибль.
С удовольствием бросился отлаживать игру и через 24 часа заметил скукоту на уровнях с 5-ю или 6-ю типами монстров. Сложно и думать надо.
Что делать? Эх, взял я лыжи, вышел на улицу, вернулся, заменил лыжи на палки и побежал в лес. На 19-ой минуте бега включились мозги и я резко понял — добавлю горизонтальных палок в поле игры! По типу перегородок в open-space. Так-то open-space — редкая гадость по жизни, а в игре норм.
Пошло дело — всё задышало, каждый уровень начал демонстрировать свой характер, пульс и температуру. Вы тоже проверьте! В смысле, температуру… Количество игровых полей в такой постановке, сами понимаете, бесконечно. Но счётно. Не увлекаясь счётом, я ограничился дюжиной-другой уровней (плюс скрытые, сюрприз-сюрприз) и привлек сына-балбеса-третьеклассника к отладке, сунув ему в руку один из пятых iPhone. А он и счастлив, есть чем заняться на видео-уроке.
Вуаля, уроки сделаны, уровни отлажены и именованы местами на планете Земля, где мне было хорошо.
И хорошо бы эти места спасти. Сейчас не шутил.
Еще геометрии
Кто сказал, что герои игры — восьмиугольники? Я добавил квадраты, шестиугольники, круги, пентагоны. В честь профессоров Дубровского, Мищенко и Фоменко. Дифф. Геометрия вирусов заметно поменяла настроение игры. Сами посмотрите.
А можно колобков сделать квадратными. Русский народный колобок и не такое стерпит.
Ой! А куда делись вирусы? Убрал… Гугл со зверской серьезностью относится к словам вирусы, инфицированы, вылечи… Пришлось редактировать мета-данные, ко-вирусы стали цветными монстриками, все умерли стали попробуй еще и пр.
Параллельно заменил Telegram-картинки. Эх, эх, прости Павел Дуров, ирония и смех не спасут мир.
Ошибки, без них скучно
Box2D иногда вылетал по ошибке в функции onGlobalCollision. Особенно на реальных устройствах, то есть айФонах. Смешно, но у меня ни одного Андроида в доме, а приложение в ГуглПлее…
Код выглядит предельно простым, а ломается, как девочка.
Отключаю звук — не ломается. Девочка, выходит, глухая?
Смотрим код:
local function onGlobalCollision( event )
if ( event.phase == "began" ) then
local ball = event.object1
local u, v = ball:getLinearVelocity()
local speed = u*u + v*v
-- print( "speed: " .. speed )
if speed>10000 then
if speed<20000 then
audio.play( rockSound )
else
audio.play( woodenSound )
end
end
end
end
Видите, в функции onGlobalCollision я ловлю столкновения колобков и проигрываю 2 типа звуков от соударений. Так вот, на некоторых уровнях синус равнялся двум сталкивались более 500 объектов одновременно (или одновременно? Гришковец). В этот редкий момент при вызове sounds внутри collision происходит страшный crash в runtime.
Как пофиксить? Решил не вызывать напрямую play (sound) из функции onGlobalCollision, а копить число соударений в переменных boom1 и boom2. Затем в цикле runTimeLoop (эта функция вызывается в игре 10 раз в секунду, не пойми зачем) проверять boom1 и boom2 и играть звук при необходимости.
if boom1>0 then
audio.play( rockSound )
boom1=0
end
Ошибка изчезла, но звук стал неприятно ритмичным (оно и понятно, 10 раз в секунду)
Тогда я просто ввел time-delay шум. Рандом-задержка на 100 миллисекунд — и оппа! все стало звучать очень натурально:
local function play1()
audio.play( rockSound )
end
local function play2()
audio.play( woodenSound )
end
local function playSound()
local t = math.random(100)
if boom1>0 then
boom1=0
timer.performWithDelay(t, play1)
end
if boom2>0 then
boom2=0
timer.performWithDelay(t, play2)
end
end
Красиво? Кто молодец? Знаю-знаю, вы сами бы так сделали.
Google Play и YouTube
Подготовил скриншоты и снял ролик для ю-тьюба, за роялем — автор. Выглядит тормознуто, но я такой по жизни, это уже не исправить:
Видео записывал родной мак-программой QuickTime.app. Для Apple store полученное видео требуется отредактировать (установить frame-rate в 30). Для этого я обычно использую приложение handBrake.app, очень рекомендую.
Сервер, без него никак
Интерес к игре подогревается и заочным соперничеством. Лучшие результаты складируются на сервере. Masterhost — отдельная боль, вот нахрена они…
Ладно, забудем обиду… На сервере я храню лучшие результаты дня и ранжирую игроков. Игроки, разумеется, хотят стать первыми и взять Кубок.
Каждую полночь (по MSK timezone) таблица рекордов на сервере обнуляется и дневной Король превращается в ночную Тыкву. You turns Kings into bergers… Хорошая песня.
Победитель дня уходит в зал Славы, наступает новая битва за следующий Кубок.
Как сделал backend?
Создал 4-е mysql таблицы: cvirus_users, cvirus_events, cvirus_today, cvirus_ticks.
В таблице cvirus_ticks всего одна запись и два поля day и tick.
UPD. Сейчас понял, что поле tick — лишнее.
Каждую полночь запись в таблице сверяется с текущей датой и обновляется следующим образом.
$new_day_flag = 0;
$day = date("d"); // текущий день
$result = mysql_query("SELECT * FROM cvirus_ticks LIMIT 1 ");
if ($row = mysql_fetch_array($result)) {
$d=$row['day'];
if ($day<>$d) {
$new_day_flag = $day;
}
}
if ($newday <> 0) {
mysql_query( "UPDATE cvirus_ticks SET day = '$new_day_flag', tick = tick + 1 ");
}
После срабатывания скрипта, все результаты игроков записываются в таблицу cvirus_today с обновленным тегом tick. Таким образом, в таблице cvirus_today хранятся рекорды игроков, сортированные по дням. Неудачники могут посмотреть, кто становился победителем в предыдущие дни.
Любопытно наблюдать географию пользователей — для этого вытаскиваю из запроса игрока его ip-адрес, стравливаю адрес сервису api.wipmania.com, взамен получаю имя домена страны (например RU или UA) и показываю флаг RU.PNG или UA.PNG напротив фамилии игрока. В некоторых играх география пользователей впечатляет и наводит на размышления. Но мы не будем предаваться мечтам о путешествиях (какая нафик география в режиме самоизоляции?!), а посмотрим на php-пример, как превратить ip → country
$ipAddress = $_SERVER['REMOTE_ADDR'];
$ipCode = file_get_contents('http://api.wipmania.com/' . $ipAddress . '?k=Е3g-Y6ХренВамКодQrmGQ7');
if (strlen($ipCode)!=2) $ipCode = 'HN'; // да-да Гондурас)
We don’t allow apps that lack reasonable sensitivity towards or capitalize on a natural disaster, atrocity, conflict, death, or other tragic event.
Это не я, это гугл-нло…
Список использованных ссылок
Тем временем в Google Play N-ый 12-ый день ждет проверки моя несчастная игра. Совсем одна — без хозяина, без рекламы, без поддержки. Но я не плачу. Я уже заменил в ней слова, картинки и звуки. И спасибо Гуглу, стало лучше, кроме того исправил пару неровностей.
So, в итоге получилась игра в Google Play. Без рекламы и без цены. Я давно понял, что время казуальщиков-одиночек ушло.
Тем не менее, напомни мне, сынок, ссылок на скачивание давать нельзя? А то превратишься в злобного Буратино, дважды подлеца (подлец + подлец) и негодяя, верно?
Ага! Купились?!
Да, кто меня только не банил за неизвестно что — Google банил, Apple — банил, Habr — дважды банил… Во времена были.
А я не унываю — сижу дома, починяю примус, ем гречку. Нафига столько купил?
Html5 всех спасает
-Позвольте, а как же hml5 версия? Где хваленная кроссплатформенность?- воскликнет внимательный читатель.
И будет прав, Github ему в руку.
Напомню, что на Github-е у каждого пользователя есть возможность держать один бесплатный сайт — я потратил этот шанс для веб-версии игры.
Предупреждаю, д-дизайн старый, нецензурированный. Нервных (и без юмора) прошу не запускать.
Вспомнил! Была еще одна браузерная ошибка. При запуске игры в Сафари получал дюжину одинаковых сообщений/предупреждений
libpng warning: iCCP: CRC error
Ошибка не критическая (жмешь десять раз ОК и играешь), но неприятная. Запросил интернет и получил ответ, что виноват GIMP. Бесплатный GIMP сохраняет PNG файлы с неправильной контрольной суммой цветовой палитры. Советы из сети не помогли,
сними галку, конвертни
— не, не работает.
Исправил проблему тупо — открыл все PNG- файлы в приложении Preview (системное приложение Mac OS для просмотра и редактирования всех типов файлов), сделал два раза Flip и закрыл. Все картинки пересохранились в правильном формате.
Возможно у кого-то из читателей могут вылезти другие баги, что ж, я не святой HelloWorld, кодю быстро и небрежно.
Пишите про ошибки в комментариях, поржём.
Эпилог
Желаю вашим семьям здоровья. Тебе, читатель — терпения.
P.S. Все опечатки — умышленные, не пишите в личку.