Как я делал коронаигру на Corona SDK

image

$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, затем скейл, трансформ, ну вы знаете, главное не залипнуть на редактировании арта

image

Вот и один из отрицательных героев. Короновирус — отъе… ись © Слепаков.

Гейм и физика


В 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" )  
 

В этом режиме схематично изображены элементы физического мира

image

Кстати, здесь был косяк — стенки стакана изначально задавались не очень высокими (три экрана) и на некоторых уровнях колобки вываливались наружу. Я недоумевал — куда они подевались? Наверное, до сих пор летят к центру Земли… Хотя нет, скорее всего, уже долетели с учетом сопротивления воздуха K_{air} = 0.75.

Выключаем debug-режим

--  physics.setDrawMode( "debug" )  
 


и получаем арт

image

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

Редактор уровней


Кнопочки на экране выбора уровней нарисовал сам, в коронавирус стиле.
image

Изначально определил 12 уровней, думая постепенно увеличивать на доске число ко-вирусов и кроликов, которые #онижеколобки.
Рассуждал примерно так:
На первых двух уровнях живут только 2 вида злодеев. Зеленые самые опасные © ДМБ.
На следующих двух — посложнее, живут 3 вида злодеев.
Затем — 4 вида.
Пять — планету не спасти.
Ш (Ж)есть — импосибль.

С удовольствием бросился отлаживать игру и через 24 часа заметил скукоту на уровнях с 5-ю или 6-ю типами монстров. Сложно и думать надо.

Что делать? Эх, взял я лыжи, вышел на улицу, вернулся, заменил лыжи на палки и побежал в лес. На 19-ой минуте бега включились мозги и я резко понял — добавлю горизонтальных палок в поле игры! По типу перегородок в open-space. Так-то open-space — редкая гадость по жизни, а в игре норм.

Пошло дело — всё задышало, каждый уровень начал демонстрировать свой характер, пульс и температуру. Вы тоже проверьте! В смысле, температуру… Количество игровых полей в такой постановке, сами понимаете, бесконечно. Но счётно. Не увлекаясь счётом, я ограничился дюжиной-другой уровней (плюс скрытые, сюрприз-сюрприз) и привлек сына-балбеса-третьеклассника к отладке, сунув ему в руку один из пятых iPhone. А он и счастлив, есть чем заняться на видео-уроке.

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

Еще геометрии


Кто сказал, что герои игры — восьмиугольники? Я добавил квадраты, шестиугольники, круги, пентагоны. В честь профессоров Дубровского, Мищенко и Фоменко. Дифф. Геометрия вирусов заметно поменяла настроение игры. Сами посмотрите.
image

А можно колобков сделать квадратными. Русский народный колобок и не такое стерпит.

image

Ой! А куда делись вирусы? Убрал… Гугл со зверской серьезностью относится к словам вирусы, инфицированы, вылечи… Пришлось редактировать мета-данные, ко-вирусы стали цветными монстриками, все умерли стали попробуй еще и пр.
Параллельно заменил 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-е у каждого пользователя есть возможность держать один бесплатный сайт — я потратил этот шанс для веб-версии игры.
Предупреждаю, д-дизайн старый, нецензурированный. Нервных (и без юмора) прошу не запускать.

Подскажите, умники
Досадно, в remote web-версии (в отличие от локальной) почему-то невозможно сохранять рекорды и смотреть статистику по другим игрокам. Говорят, надо прописать в файле .htaccess атрибуты доступа Access-Control-Allow-Origin: papabubadiop.github.io, а у меня не получается. Возможно есть другое решение — подскажите, умники.

Вспомнил! Была еще одна браузерная ошибка. При запуске игры в Сафари получал дюжину одинаковых сообщений/предупреждений

libpng warning: iCCP: CRC error

Ошибка не критическая (жмешь десять раз ОК и играешь), но неприятная. Запросил интернет и получил ответ, что виноват GIMP. Бесплатный GIMP сохраняет PNG файлы с неправильной контрольной суммой цветовой палитры. Советы из сети не помогли,

сними галку, конвертни

 — не, не работает.
Исправил проблему тупо — открыл все PNG- файлы в приложении Preview (системное приложение Mac OS для просмотра и редактирования всех типов файлов), сделал два раза Flip и закрыл. Все картинки пересохранились в правильном формате.

Возможно у кого-то из читателей могут вылезти другие баги, что ж, я не святой HelloWorld, кодю быстро и небрежно.
Пишите про ошибки в комментариях, поржём.

Эпилог


Желаю вашим семьям здоровья. Тебе, читатель — терпения.

P.S. Все опечатки — умышленные, не пишите в личку.

© Habrahabr.ru