Read this manual in English

Lua-модули

Lua-модули позволяют структурировать проект и создавать многократно используемый библиотечный код. Избежание дублирования в проектах является хорошей идеей. Defold позволяет использовать функциональность Lua-модулей, предоставляя возможность включать файлы скриптов в другие файлы скриптов. Это позволяет инкапсулировать функциональность (и данные) во внешний файл скрипта для повторного использования в скриптах игровых объектов и GUI-скриптах.

Запрос Lua-файла

Lua-код, хранящийся в файлах с окончанием “.lua” где-либо в структуре игрового проекта, может быть затребован в файлах скриптов и GUI-скриптов. Чтобы создать новый файл Lua-модуля, кликните ПКМ по папке, в которой хотите его создать, в представлении Assets, затем выберите New... ▸ Lua Module. Дайте файлу уникальное имя и кликните Ok:

new file

Предположим, в файл “main/anim.lua” добавлен следующий код:

function direction_animation(direction, char)
    local d = ""
    if direction.x > 0 then
        d = "right"
    elseif direction.x < 0 then
        d = "left"
    elseif direction.y > 0 then
        d = "up"
    elseif direction.y < 0 then
        d = "down"
    end
    return hash(char .. "-" .. d)
end

В таком случае любой скрипт может потребовать этот файл и использовать функцию:

require "main.anim"

function update(self, dt)
    -- обновить позицию, задать направление и т.д.
    ...

    -- установить анимацию
    local anim = direction_animation(self.dir, "player")
    if anim ~= self.current_anim then
        msg.post("#sprite", "play_animation", { id = anim })
        self.current_anim = anim
    end
end

Функция require загружает конкретный модуль. Она начинает с просмотра таблицы package.loaded, чтобы определить, загружен ли уже модуль. Если да, то require возвращает значение, хранящееся в package.loaded[module_name]. В противном случае он загружает и оценивает файл с помощью загрузчика.

Синтаксис строки имени файла, передаваемой в require, немного своеобразен. Lua заменяет символы ‘.’ в строке имени файла на разделители путей: ‘/’ в macOS и Linux и ‘\’ в Windows.

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

Модули

Для инкапсуляции данных и функций Lua использует модули. Lua-модуль — это обычная Lua-таблица, используемая для хранения функций и данных. Таблица объявляется локальной, чтобы не засорять глобальную область видимости:

local M = {}

-- private
local message = "Hello world!"

function M.hello()
    print(message)
end

return M

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

local m = require "mymodule"
m.hello() --> "Hello world!"

Горячая перезагрузка модулей

Рассмотрим простой модуль:

-- module.lua
local M = {} -- создает новую таблицу в локальной области видимости
M.value = 4711
return M

И пользователь модуля:

local m = require "module"
print(m.value) --> "4711" (даже если "module.lua" изменен и перезагружен на горячую)

При горячей перезагрузке файла модуля код снова выполняется, но с m.value ничего не происходит. Почему?

Во-первых, таблица, созданная в “module.lua”, создается в локальной области видимости, и пользователю возвращается ссылка на эту таблицу. Перезагрузка “module.lua” снова оценивает код модуля, но при этом создается новая таблица в локальной области видимости вместо обновления таблицы, на которую ссылается m.

Во-вторых, Lua кэширует требуемые файлы. Когда файл требуется в первый раз, он помещается в таблицу package.loaded, чтобы его можно было быстрее прочитать при последующих запросах. Можно заставить файл быть повторно считанным с диска, установив запись файла в значение nil: package.loaded["my_module"] = nil.

Чтобы правильно выполнить горячую перезагрузку модуля, необходимо перезагрузить модуль, сбросить кэш, а затем перезагрузить все файлы, которые использует модуль. Это далеко не оптимальный вариант.

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

--- module.lua

-- Заменить на локальное M = {} после завершения
uniquevariable12345 = uniquevariable12345 or {}
local M = uniquevariable12345

M.value = 4711
return M

Модули и состояние

Модули с состоянием хранят внутреннее состояние, которое является общим для всех пользователей модуля, и их можно сравнить с синглтонами:

local M = {}

-- Все пользователи модуля будут совместно использовать эту таблицу
local state = {}

function M.do_something(foobar)
    table.insert(state, foobar)
end

return M

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

Использование таблицы состояний
Возможно, самый простой подход заключается в использовании функции-конструктора, которая возвращает новую таблицу, содержащую только состояние. Состояние явно передается в модуль в качестве первого параметра каждой функции, которая манипулирует таблицей состояний.
local M = {}
  
function M.alter_state(the_state, v)
    the_state.value = the_state.value + v
end
  
function M.get_state(the_state)
    return the_state.value
end
  
function M.new(v)
    local state = {
        value = v
    }
    return state
end
  
return M

Use the module like this:

local m = require "main.mymodule"
local my_state = m.new(42)
m.alter_state(my_state, 1)
print(m.get_state(my_state)) --> 43
Использование метатаблиц
Другой подход заключается в использовании функции-конструктора, которая при каждом вызове возвращает новую таблицу с состоянием и публичными функциями модуля:
local M = {}
  
function M:alter_state(v)
    -- self is added as first argument when using : notation
    self.value = self.value + v
end
  
function M:get_state()
    return self.value
end
  
function M.new(v)
    local state = {
        value = v
    }
    return setmetatable(state, { __index = M })
end
  
return M

Use the module like this:

local m = require "main.mymodule"
local my_state = m.new(42)
my_state:alter_state(1) -- "my_state" is added as first argument when using : notation
print(my_state:get_state()) --> 43
Использование замыканий
Третий способ — вернуть замыкание, содержащее все состояния и функции. Нет необходимости передавать экземпляр в качестве аргумента (явно или неявно с помощью оператора двоеточия), как при использовании метатаблиц. Этот метод также несколько быстрее, чем использование метатаблиц, поскольку вызовы функций не должны проходить через метаметоды __index, но при этом каждое замыкание содержит свою собственную копию методов, вследствие чего потребление памяти выше.
  local M = {}
  
  function M.new(v)
      local state = {
          value = v
      }
  
      state.alter_state = function(v)
          state.value = state.value + v
      end
  
      state.get_state = function()
          return state.value
      end
  
      return state
  end
  
  return M

Используйте модуль следующим образом:

  local m = require "main.mymodule"
  local my_state = m.new(42)
  my_state.alter_state(1)
  print(my_state.get_state())