GameDev, Indie, Corona SDK, GameJam 48h, DevConf, Go, Laser Flow, ZipQuest, $5 Prize

в 11:40, , рубрики: Без рубрики

GameDev, Indie, Corona SDK, GameJam 48h, DevConf, Go, Laser Flow, ZipQuest, $5 Prize Приветствую! Сразу извиняюсь за заголовок — столько всего хотелось в нём рассказать, но получалось слишком длинно.

Рассказ пойдёт о моей игре (iOS, Android), сделанной с помощью Corona SDK, о самой короне и разработке с ней, о соревновании «напиши игру за 48 часов», о прошедшей недавно DevConf, про язык Go и о небольшом конкурсе для хабравчан с символическим призом $5.

Введение

Я уже давно разрабатываю игру в жанре Tower Defense с короной, но игра требует огромного количества времени, чтобы стать по настоящему качественной, а качественные игры это пункт #1 на пути к успеху (пункт #2 это куча денег на маркетинг, если кто не в курсе). Поэтому давно уже хотелось что-то сделать пускай небольшое, но своё и в короткие сроки.

«Осторожно многабукав!»

GIGJam 48

И тут в начале мая устраивается соревнование от небольшой студии Glitch Games совместно с Corona Labs по разработке игры за 48 часов. Как Ludum Dare, только для Corona SDK. Захотелось сделать головоломку, разработка идеи, две почти бессонные ночи и я сделал игру Laser Flow — Experiments with Lasers. Тогда она ещё называлась просто Laser Experiments. Подобных игр немного и моя отличается адекватным управлением, смешиванием цветов и надеюсь приятна на глаз и слух. Мне самому очень нравится. Но по результатам соревнования, к сожалению, мои взгляды не совпадают со взглядами жюри. Всего было 23 игры от порядка 35 участников.

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

После отправки игр началось томное ожидание результатов. Шли дни, результатов нет. Потом объявили, что жюри потребуется несколько недель на оценивание, и в IRC комнате #corona начались беспорядки…
Тяжелое было время. Результатов пришлось ждать в итоге больше месяца.

Кстати вот скрины игры, показывающие как она изменилась с момента отправки на конкурс.
GameDev, Indie, Corona SDK, GameJam 48h, DevConf, Go, Laser Flow, ZipQuest, $5 Prize GameDev, Indie, Corona SDK, GameJam 48h, DevConf, Go, Laser Flow, ZipQuest, $5 Prize

DevConf 2013

Ближе к концу мая со мной связался организатор DevConf и предложил поучаствовать в их конкурсе докладов. Я предложил две темы: про Corona SDK и про Go. Взяли оба доклада, чему я был очень рад. Спасибо организаторам за это, за бесплатный проезд, проживание и питание.

Я подумал, как было бы здорово закончить и выпустить игру к конференции, чтобы вести доклад сразу на живом примере, чтобы люди могли скачать и поиграть. Так же в планах было устроить небольшой конкурс — реши секретный уровень быстрее остальных и получи футболку с логотипом игры (иконкой). Но во-первых, из-за бага с покупками моё приложение отклоняли и вышло оно только на второй день конференции, а во-вторых, футболку не смогли напечатать с яркими нужными мне цветами (специальный профиль CMYK). Кстати 1024x1024 пикселей вполне достаточно для печати на футболке рисунка 20x20см.

Получилось, что мне потребовалось чуть больше месяца, чтобы закончить игру.

Слайды к докладам я начал делать ещё дома, продолжил в самолёте и закончил уже в гостинице. Использовались в поездке iPad и Asus Eee Pc 701 4G с лубунтой. Тот самый, что с сенсорным экраном, синезубом и дополнительным слотом для SD. Модем пришлось убрать. Сами слайды делал в LibreOffice.

Спасибо соседу по номеру Павлу за предоставленный МакБук для перекомпиляции игры и её отправки в App Store после неожиданного реджекта. Как ни странно этого было достаточно, чтобы игру одобрили в этот раз.

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

Мои доклады были последними в сетке и собрали, откровенно говоря, немного слушателей. Доклад по короне я готовил с расчётом показать тонкости, какие-то выверенные временем лучшие практики и советы, чтобы доклад не был сухим пересказом главной страницы сайта и документации, хотел сделать доклад действительно полезным. Однако почти никто из слушателей не был знаком ни с короной, ни с Lua. Пришлось перестроить доклад ближе к ознакомительному, показать на что способна корона и сделай некое введение в Lua.

Неожиданно для меня, пока я рассказывал про корону, первый день конференции закончилась. А доклад по Go я ещё и не начинал. Быстрый соцопрос показал, что слушатели оказывается ждут этот доклад и мы приняли решение продолжить на первом этаже гостиницы. Нас было немного, мне дали ноут с большим экраном (спасибо!) вместо проектора, к сожалению не все комфортно сидели и не всем было нормально видно, но ладно. Слайды я выложил на сайте, все кому нужно скачают. После доклада мы ещё немного поговорили про Go, про его перспективы, про то, как он нам всем нравится и была подарена книга по этому языку одному из слушателей (тоже хочу!)

На втором дне были весьма интересные доклады про high load.

С конференции у меня остались бейсболка с логотипом DevConf, блокнотики, ручки, значки и мини зубная щётка из гостиницы. На этом про DevConf в принципе всё. Было классно.

Слайды по моим докладам можно скачать с моего сайта. Ссылка внизу топика.

Corona SDK

Корона хороша. Она является самым простым в использовании фреймворком для разработки игр, недавно обзавелась бесплатной версией и крайне рекомендуется к ознакомлению. Зная Python и PHP, у меня ушла всего лишь пара дней на изучение Lua, ещё неделя ушла на осознание большинства концепций языка, ООП и основной части API короны.

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

local txt = a and a.text and a.text:lower() or 'default'

-- Эквивалент:
local txt = 'default'
If a then
  if a.txt then
    txt = a.txt:lower()
  end
end

Часто допускают ошибку при использовании callback'ов.

local function myFunc()
  print('Nya')
end
timer.performWithDelay(1000, myFunc(), 1)

В этом случае таймеру передаётся не сама функция, а результат её выполнения (nil). Необходимо просто убрать скобочки после имени функции.
Один раз в IRC спросили как сказать таймеру, чтобы он выполнил функцию не только через указанное время, но и прямо сейчас. Ответ — вызовите её просто в коде в том же месте, где и создаёте таймер.

Ещё Lua идёт с сахаром:

object.property -> object['property']
local function nya() -> local nya = function()
object:method(params) -> object.method(object, params)
someFunction{name = 'Someone'} -> someFunction({name = 'Someone'})
и др.

ООП в Lua

ООП можно организовать на основе псевдоклассов с использованием метаметодов (аналог магических методов в PHP). А может вполне себе неплохо строится на основе простых таблиц. Причём и private, и public, и даже protected можно организовать без проблем.

Показать код ООП

Простейший вариант с пустым объектом:

local function newObject()
    local object = {}
    return object
end

local myObject = newObject()

Добавим public переменные:

local function newObject()
    local object = {}
    object.somePublicVar = 100
    return object
end

local myObject = newObject()
print(myObject.somePublicVar)

Теперь добавим какой-нибудь метод:

local function newObject()
    local object = {}
    object.somePublicVar = 100
    function object:setName(name)
        self.name = name
    end
    return object
end

local myObject = newObject()
myObject:setName('Nyashka')

print(myObject.name)

Обратите внимание на использование двоеточия. В этом случае внутри функции-метода появляется специальная переменная self, которая является ссылкой на сам объект. Вызывать такие методы тоже нужно через двоеточие, либо с одной точкой, но в качестве первого аргумента указывать объект.

Добавим private переменную, например деньги:

local function newObject()
    local object = {}
    object.somePublicVar = 100
    local money = 0
    function object:setName(name)
        self.name = name
    end
    function object:addMoney(amount)
        money = money + amount
    end
    return object
end

local myObject = newObject()
myObject:setName('Nyashka')
myObject:addMoney(50)

print(myObject.money) -- nil

Если попытаемся обратиться к money через точку, то получим nil, так как эта переменная находится не в самом объекте-таблице, а в области видимости функции, создавшей этот объект. Поэтому эта переменная видна в методе addMoney().

Теперь наследование:

local function newObject()
    local object = {}
    object.somePublicVar = 100
    local money = 0
    function object:setName(name)
        self.name = name
    end
    function object:addMoney(amount)
        money = money + amount
    end
    return object
end

local function newChild()
    local object = newObject()
    return object
end

local myChild = newChild()

В этом случае объект создаётся не пустой таблицей {}, а другим базовым объектом. Так в функции newChild можно добавлять свойства и методы объекта или даже переопределять их.

Protected реализуется чуть интереснее. Создаём private таблицу, в которую складываем все protected переменные, и возвращаем её вместе с созданным объектом через return. Допустим мы хотим передавать деньги по наследству.

local function newObject()
    local object = {}
    object.somePublicVar = 100
    local protected = {money = 0}
    function object:setName(name)
        self.name = name
    end
    function object:addMoney(amount)
        protected.money = protected.money + amount
    end
    function object:getMoney()
        return protected.money
    end
    return object, protected
end

local function newChild()
    local object, protected = newObject()
    function object:spendMoney(amount)
        protected.money = protected.money - amount
    end
    return object
end

local myChild = newChild()
myChild:addMoney(100)
myChild:spendMoney(60)
print(myChild:getMoney()) -- 40

Если необходимо переопределить какой-либо метод, то он объявляется точно так же. Новый затирает собой старый. Если необходимо сделать обёртку, то сначала сохраняется старый метод в какую либо переменную, и затем он вызывается из нового метода:

local function newObject()
    local object = {}
    object.somePublicVar = 100
    local protected = {money = 0}
    function object:setName(name)
        self.name = name
    end
    function object:addMoney(amount)
        protected.money = protected.money + amount
    end
    function object:getMoney()
        return protected.money
    end
    return object, protected
end

local function newChild()
    local object, protected = newObject()
    function object:spendMoney(amount)
        protected.money = protected.money - amount
    end
    local parent_setName = object.setName
    function object:setName(name)
        name = 'Mr. ' .. name
        parent_setName(self, name)
    end
    return object
end

local myChild = newChild()
myChild:setName('Nyashka')
print(myChild.name) -- Mr. Nyashka

В случае объектов, отображаемых на экране, то достаточно заменить пустую таблицу {} при создании объекта на вызов какой-либо функции из API Corona SDK, возвращающую графический объект. Это может быть display.newImage(), display.newRect() или чаще всего я использую группу display.newGroup(). Если использовать группу, то такие объекты очень просто расширять другими графическими элементами. Но имейте в виду, что чрезмерное количество групп снижает производительность, поэтому если Ваш код начинает тормозить, то старайтесь использовать их как можно меньше.

Модуль это тоже таблица и тоже объект. Можно придерживаться тех же принципов, что и при ООП. Раньше модули писали с использованием ключевого слова/функции module(). Ей передавалось имя создаваемого модуля и она подменяла окружение скрипта на собственное, локальное, чтобы не надо было писать кучу «local», все переменные и функции помещались в это локальное пространство. И всё бы ничего, но при этом внутри модуля не было доступа до глобальных функций вроде print и других. Тогда придумали, что при добавлении специальной функции в качестве второго аргумента функции module(), будет проводиться поиск всех глобальных вещей там где должно быть — в _G. За это отвечала эта специальная функция package.seeall. Но на практике мало того, что все глобальные функции и переменные оказывались внутри всех модулей, так и модули создавались и помещались функцией module в глобальное пространство имён. Каша. Лунная каша. Глобальная лунная каша.

Вставлю свой пример модуля с pastebin:

Модуль

local _M = {} -- module table
 
_M.someProperty = 1 -- class properties
 
local function createText()
   -- local function are still valid, but not seen from outside - "private"
end
 
local privateVar -- so do local variables
 
_GLOBAL_VAR -- without local it's global
 
function _M.staticMethod(vars)
    -- this is class method like function (dot)
    -- there is no "self"
end
 
function _M:someMethod(vars)
    -- this is object method like function (colon)
    -- there is "self"
end
 
function _M:newBaseObject()
    -- Here goes constructor code
    local object = display.newImage(...) -- could be a display object or an empty table {}
    object.vars = 'some vars'
    object.name = 'BaseObject'
    object.property = self.someProperty -- from module
 
    function object:sign(song)
        print(self.name .. ' is singing ' .. song)
    end
 
   
    function object:destroy()
       -- optional destructor, you can override removeSelf() as well
       self.vars = nil
       self:removeSelf()
    end
 
    return object
end
 
-- Now inheritance
function _M:newChildObject()
    local object = _M:newBaseObject()
    -- override any methods or add new
    object.name = 'ChildObject'
    function object:tell(story)
        print(self.name .. ' is telling ' .. story)
    end
    return object
end
 
return _M -- return this table as a module to require()

Создаём таблицу-модуль и ручками внутрь помещаем содержимое какое хотим. Всё удобно, всё замечательно.
В конце файла пишем обязательно return _M. Иначе модуль не загрузится через require.

Остальное про Corona SDK

А вы знаете, что три точки… означают переменный список аргументов текущей функции? Так легче писать обёртки или вариативные функции.

local function my_warning_print(...)
    print('Warning:', unpack(arg))
end

Скорость Lua примерно в 20 раз ниже чем С. LuaJIT всего в пару тройку раз медленнее, но компиляция в реальном времени запрещена в iOS. Пичалька. На андроиде можно компилировать, но в короне это не реализованно. Хорошая новость в том, что Lua очень редко узкое место. Только при интенсивных вычислениях.

Скорость разработки очень важна. И короне можно простить многое только лишь за это.

Всё что не локальное — глобальное. Храните переменные в таблицах для удобства.
Много времени тратится на поиск переменной в служебных таблицах видимости. Сначала видимость текущего блока или функции, затем по цепочке вверх до самого _G. Так происходит в случае глобальных переменных. Соответственно они медленные. Кэшируйте с помощью local все переменные и функции для увеличения производительности в конкретном месте. Но не переусердствуйте. Большинству кода такой кэш не даст какой-либо заметной прибавке к скорости. оптимизируйте только те места, которые вызываются очень часто. Это может быть функция в бесконечном таймере или обработчик события enterFrame вызываемый каждый кадр. Пока Lua не завершит обработку всего кода для текущего кадра, Corona не запустит рендерер. Так если вы изменяете параметр x допустим картинки на экране много раз внутри функции, то только последнее значение отразится на экране.

Кэшируйте даже такие вещи как math.random, table.insert, ipars, pairs и другие встроенные функции.

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

Вполне возможно переопределять всё что находится в _G, но это конечно не рекомендуется. Чтобы посмотреть что там находится просто пройдитесь принтом по нему. Запомните выведенные переменные и никогда не используйте их. Некоторые говорят, что ни в коем случае ничего не кладите в _G. Но это перебор. Вполне нормально в глобальной видимости иметь несколько элементов для себя вроде таблицы для ваших модулей (как namespace, например «app»), конфигурации или просто встроенные модули, чтобы в каждой сцене не писать кучу require.

Когда задумываются о производительности, добавляют модуль по мониторингу FPS, памяти текстур и памяти Lua. В первый раз после добавления такого модуля у всех паника «откуда такая утечка памяти». На самом деле плавный рост потребляемой памяти это норма, примерно до 10kB в секунду. При следующем вызове сборщика мусора память вернётся к своему минимальному значению. Сборщик мусора можно настраивать, если он вам мешает в ваших алгоритмах. Например запускать его самостоятельно после окончания вычислений, чтобы он не тормозил их.

Память Lua на практике не превышает пары мегабайтов. Основной объём это текстуры. Их желательно оптимизировать. Внутри видеочипа все текстуры (имеются ввиду любые изображения) выравниваются по степеням двойки. Поэтому для наилучшего результата придерживайтесь степеней двойки для ваших файлов.

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

Анимируйте всё. Очень просто добиться этого если придерживаться правил хорошего тона при проектировании вашего приложения. Использовать объекты для всего. Хотя бы даже функции обёртки над стандартными функциями newImage и др. В таком случае вы можете добавить анимацию один раз в генераторе вашего объекта и она появится сразу во всём приложении. Только не переборщите с ними. Перегруженность или заторможенность в том плане, что вы заставляете пользователя ждать, очень плохо сказывается на общем впечатлении от приложения.

Вот моя обёртка для стандартного newImage. Можно в одну строчку задать положение на экране и добавить в группу.

Обёртка над newImage и полезные мелочи

local _M = {}
_W = display.contentWidth
_H = display.contentHeight
_T = display.screenOriginY -- Top
_L = display.screenOriginX -- Left
_R = display.viewableContentWidth - _L -- Right
_B = display.viewableContentHeight - _T-- Bottom
_CX = math.floor(_W / 2)
_CY = math.floor(_H / 2)

_COLORS = {}
_COLORS['white'] = {255, 255, 255}
_COLORS['black'] = {0, 0, 0}
_COLORS['red'] = {255, 0, 0}
_COLORS['green'] = {0, 255, 0}
_COLORS['blue'] = {0, 0, 255}
_COLORS['yellow'] = {255, 255, 0}
_COLORS['cyan'] = {0, 255, 255}
_COLORS['magenta'] = {255, 0, 255}

function _M.setRP (object, ref_point)
    ref_point = string.lower(ref_point)
    if ref_point == 'topleft' then
        object:setReferencePoint(display.TopLeftReferencePoint)
    elseif ref_point == 'topright' then
        object:setReferencePoint(display.TopRightReferencePoint)
    elseif ref_point == 'topcenter' then
        object:setReferencePoint(display.TopCenterReferencePoint)
    elseif ref_point == 'bottomleft' then
        object:setReferencePoint(display.BottomLeftReferencePoint)
    elseif ref_point == 'bottomright' then
        object:setReferencePoint(display.BottomRightReferencePoint)
    elseif ref_point == 'bottomcenter' then
        object:setReferencePoint(display.BottomCenterReferencePoint)
    elseif ref_point == 'centerleft' then
        object:setReferencePoint(display.CenterLeftReferencePoint)
    elseif ref_point == 'centerright' then
        object:setReferencePoint(display.CenterRightReferencePoint)
    elseif ref_point == 'center' then
        object:setReferencePoint(display.CenterReferencePoint)
    end
end

function _M.newImage(filename, params)
    params = params or {}
    local w, h = params.w or _W, params.h or _H
    local image = display.newImageRect(filename, w, h)
    if params.rp then
        _M.setRP(image, params.rp)
    end
    image.x = params.x or _CX
    image.y = params.y or _CY
    if params.g then
        params.g:insert(image)
    end
    return image
end

function _M.setFillColor (object, color)
    if type(color) == 'string' then
        color = _COLORS[color]
    end
    local color = table.copy(color)
    if not color[4] then color[4] = 255 end
    object:setFillColor(color[1], color[2], color[3], color[4])
end

function _M.setTextColor (object, color)
    if type(color) == 'string' then
        color = _COLORS[color]
    end
    local color = table.copy(color)
    if not color[4] then color[4] = 255 end
    object:setTextColor(color[1], color[2], color[3], color[4])
end

function _M.setStrokeColor (object, color)
    if type(color) == 'string' then
        color = _COLORS[color]
    end
    local color = table.copy(color)
    if not color[4] then color[4] = 255 end
    object:setStrokeColor(color[1], color[2], color[3], color[4])
end

function _M.setColor (object, color)
    if type(color) == 'string' then
        color = _COLORS[color]
    end
    local color = table.copy(color)
    if not color[4] then color[4] = 255 end
    object:setColor(color[1], color[2], color[3], color[4])
end
return _M

Немного больше про размеры экрана pastebin.com/GWG3sJLZ

Также включает в себя крайне полезную функцию setRP, избавляющую от необходимости писать длинные значение опорных точек. А также способ задания цвета объектов по имени.

Для поддержки кнопки назад в Android, достаточно добавить следующий код в Ваш main.lua

Runtime:addEventListener('key', function (event)
        if event.keyName == 'back' and event.phase == 'down' then
            local scene = storyboard.getScene(storyboard.getCurrentSceneName())
            if scene and type(scene.backPressed) == 'function' then
                return scene:backPressed()
            end
        end
    end);

И определите в каждой сцене, где требуется реакция на эту кнопку, функцию:

function scene:backPressed()
    storyboard.gotoScene('scenes.menu', 'slideRight', 200)
    return true
end

Не забудьте добавить диалоговое окно «Вы уверены, что хотите выйти» и вызов native.requestExit() (только для Android, для iOS — os.exit()).

config.lua панацея для фрагментации устройств

Динамическое масштабирование прекрасно. Почти.
Контент подгоняется под текущие размеры экрана, при этом сохраняя видимой виртуальную часть вашего приложения (обычно 320x480) и добавляя места по каким либо двум краям (letterbox). Располагайте ваши элементы по якорям — углы экрана, центры сторон или центр всего экрана. Старайтесь не использовать элементы, которые нужно было бы масштабировать. Если на разных экранах элементы управления выглядят чуть меньше или больше — не страшно. Главное делайте их достаточно большими. Не ниже 32 пикселей для кнопки. Лучше 64 или хотя бы 48. Есть углы вашего контента и есть углы непосредственно экрана, в этом отличие виртуальной области экрана от реальной.

Мало кто знает что в config.lua можно писать код. Можно динамически назначать область контента. Для чего? Чтобы добиться pixel perfect графики на большинстве устройств. В противном случае картинка немного размывается и безупречный вид теряется.

Складывайте свои файлы по подпапкам — lib для библиотек, scenes для сцен storyboard, images для картинок, sounds и возможно music для чего вы вполне себе догадались. Всегда используйте нижний регистр для ваших файлов, чтобы избежать проблем с загрузкой файлов на файловых системах с учётом регистра.

Используйте bitbucket.com для бэкапа и контроля версий вашего приложения. Он бесплатный для закрытых проектов с командами из всего нескольких человек.

Музыку для вашей игры вы можете искать на soundcloud, indiegamemusic, google. Не забывайте про Garabe band, FrootyLoops, Audacity и bfxr.

В Garage Band с синтезатором 8 битных звуков можно добиться хорошего современного звучания олдскулла. С применением эффектов и эквалайзера.

Вызвать onRelease виджета кнопки можно через button._view._onReleas().

Внимательно просматривайте библиотеку на наличие стандартных возможностей. Мало кто знает что в короне реализована поддержка twitter клиента под ios (native.showPopu('twitter')). Или что можно показывать страницу маркета прямо из приложения.

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

Используйте Flurry, чтобы понять чего не так с вашей игрой, как её улучшить, чтобы больше пользователей её рекомендовали.

В качестве IDE для Corona SDK я выбрал Zero Brane Studio — открытый исходный код, русскоговорящий разработчик, написана на Lua + wxWidgets, кроссплатформенная. Есть и другие IDE, но эта в целом нравится больше.

Go

Go классный. Всем использовать.

Написал на нём серверную часть другой игры (заказ) и мне это очень понравилось. Скорость и удобство разработки выше чем у C/C++, скорость выполнения либо чуть медленнее, либо такая же. Сейчас уже обширное сообщество у языка и достаточно много появилось самых разных библиотек.

Laser Flow

Игра вышла в App Store 15 июня. В этот день было 3000 закачек и 10 покупок. Возможно игра получилась слишком сложной, а может и как раз для поклонников игры.

Хочу рассказать про генератор уровней и про редактор уровней. Несколько дней я убил на то, чтобы написать алгоритм поиска решений. Применял разные вариации перебора, с разными условиями выхода, со всеми оптимизациями кода и с использованием LuaJIT, результат оказался плачевным — уровни решаются очень медленно. Нужно разрабатывать алгоритм на основе алгоритмов поиска кратчайшего пути, но это непросто. Оставил от генератора только генератор поля и назвал это Abyss of Random.

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

В итоге я решил скомбинировать. Нагуглил кейлоггер для Mac OS на python и скрипт определения текущего активного окна. Таким образом если в данный момент активен симулятор короны, то отправлять в него по UDP нажатые клавиши простым текстом. В короне поднимается UDP сервер и слушает команды, когда находимся в сцене с непосредственно игровым полем.

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

Ещё три бонусных уровня скрыты в самой игре, найти их несложно.

Код кейлоггера

from AppKit import NSWorkspace
from Cocoa import *
from Foundation import *
from PyObjCTools import AppHelper
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 
class AppDelegate(NSObject):
    def applicationDidFinishLaunching_(self, aNotification):
        NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(NSKeyDownMask, handler)
 
def handler(event):
    if NSWorkspace.sharedWorkspace().activeApplication()['NSApplicationName'] == 'Corona Simulator':
        s = unicode(event).split(' ')
        if s[8]:
            s = s[8][-2:-1]
            if (s >= 'a' and s <= 'z') or s == '`' or s == '[' or s == ']':
                sock.sendto(s, ('127.0.0.1', 5000))
        
def main():
    app = NSApplication.sharedApplication()
    delegate = AppDelegate.alloc().init()
    NSApp().setDelegate_(delegate)
    AppHelper.runEventLoop()
 
if __name__ == '__main__':
   main()
Код сервера на Lua

local udp = socket.udp()
udp:setsockname('127.0.0.1', 5000)
udp:settimeout(0.001)
local char, ip, port
timer.performWithDelay(200, function ()
        char, ip, port = udp:receivefrom()
        if char then
            local scene = storyboard.getScene(storyboard.getCurrentSceneName())
            if type(scene.keyboardPressed) == 'function' then
                scene:keyboardPressed(char)
            end
        end
    end, 0)

По такому же принципу можно сделать кнопку сохранения скриншотов прямо из игры для загрузки в различные магазины приложений. Использовать нужно функцию display.save().

Перед публикацией на хабре добавил поддержку русского языка, сделал доступные изначально уровни полегче, добавил аналитику, сделал скидку на покупку всех уровней и выложил в Google Play.

ZipQuest

А вот специально для хабравчан я создал ещё один уровень, найти который сможет лишь истинный IT-шник. Или группа таких людей.

Кто быстрее решит этот уровень и пришлёт скрин решения личным сообщением мне в личку — получит скромные $5 на PayPal.

Выкладывать в общий доступ решение этого уровня нельзя. Можно и нужно помогать остальным в поиске подсказок. Пользователям Android несколько проще, но я надеюсь они будут делиться найденными подсказками. Дизассемблировать игру для поиска секретного уровня не нужно.

Вам потребуется:

  • Разблокированное Android или iOS устройство с процессором уровня не ниже ARMv7;
  • Опыт отлаживания мобильных приложений;
  • Любопытство;
  • Терпение;
  • Смекалка.

Общайтесь на IRC как Corona SDK, так и Go. На freenode.net: #corona и #go-nuts.
Игроки в Го сильно обижаются, если зайти к ним на канал (#go) и начать грузить вопросами по программированию.

Ссылки

Laser Flow
iOS itunes.apple.com/us/app/laser-flow/id647540345?ls=1&mt=8
Android play.google.com/store/apps/details?id=com.spiralcodestudio.laserflow

Слайды с конференции: Corona SDK, Go Language.

Corona SDK coronalabs.com
Go Language golang.org

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

Очепятки и неточности прошу отправлять в личку.

Автор: Lerg

Источник

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


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