В движок Defold встроен язык Lua для написания скриптов. Lua — это легкий динамический язык, мощный, быстрый и простой для встраивания. Он широко используется в качестве скриптового языка для видеоигр. Программы на Lua написаны с применением простого процедурного синтаксиса. Язык динамически типизирован и запускается интерпретатором байткода. В нем реализовано автоматическое управление памятью с инкрементальной сборкой мусора.
Это руководство вкратце знакомит с основами программирования на Lua в целом и с тем, что необходимо учитывать при работе с Lua в Defold. Если у вас уже есть некоторый опыт работы с Python, Perl, Ruby, JavaScript или аналогичным динамическим языком, вы быстро освоитесь. Если вы совсем новичок в программировании, вам лучше начать с книги по Lua, предназначенной для начинающих. Существует множество книг на выбор.
Мы стремимся сохранить Defold одинаковым на всех платформах, но в настоящее время существуют несколько незначительных расхождений в версии Lua на разных платформах:
Платформа | Версия Lua | JIT |
---|---|---|
Windows | LuaJIT 2.1.0-beta3 | Yes |
macOS | LuaJIT 2.1.0-beta3 | Yes |
Linux | LuaJIT 2.1.0-beta3 | Yes |
Android | LuaJIT 2.1.0-beta3 | Yes |
iOS | LuaJIT 2.1.0-beta3 | No* |
Nintendo Switch | LuaJIT 2.1.0-beta3 | No* |
HTML5 | Lua 5.1.4 | N/A |
*=JIT-скомпилированный код не допускается
LuaJIT — это высоко оптимизированная версия Lua, подходящая для использования в играх и других программах, где важна производительность. LuaJIT полностью совместим с Lua 5.1. Он поддерживает все стандартные функции библиотеки Lua и полный набор Lua/C API функций.
LuaJIT также добавляет ряд языковых расширений и некоторые возможности из Lua 5.2.
Чтобы гарантировать работоспособность игры на всех поддерживаемых платформах, мы настоятельно рекомендуем использовать ТОЛЬКО языковые возможности из Lua 5.1.
Defold включает все стандартные библиотеки Lua 5.1, а также сокеты и библиотеку битовых операций:
assert()
, error()
, print()
, ipairs()
, require()
etc)Все библиотеки документированы в справочнике по API.
Программы имеют простой, легко читаемый синтаксис. Выражения записываются по одному на каждой строке, и нет необходимости отмечать их конец. По желанию можно использовать точки с запятой ;
для разделения выражений. Блоки кода разделены ключевыми словами и заканчиваются ключевым словом end
. Комментарии могут быть как блочными, так и до конца строки:
--[[
Вот блок комментариев, который может занимать
несколько строк в исходном файле.
--]]
a = 10
b = 20 ; c = 30 -- Два выражения в одной строке
if my_variable == 3 then
call_some_function(true) -- Здесь приведен строчный комментарий
else
call_another_function(false)
end
Lua является динамически типизированным языком, что означает, что переменные не имеют типов, а их значения имеют. В отличие от типизированных языков, любой переменной можно присвоить любое значение. В Lua существует восемь основных типов данных:
nil
nil
. Обычно он обозначает отсутствие полезного значения, например, неприсвоенные переменные.
print(my_var) -- Выведет 'nil', поскольку переменной 'my_var' еще не присвоено значение
true
или false
. Условия, имеющие значение false
или nil
, становятся ложными. Любое другое значение делает условие истинным.
flag = true
if flag then
print("flag is true")
else
print("flag is false")
end
if my_var then
print("my_var is not nil nor false!")
end
if not my_var then
print("my_var is either nil or false!")
end
print(10) --> выведет '10'
print(10.0) --> '10'
print(10.000000000001) --> '10.000000000001'
a = 5 -- целое число (integer)
b = 7/3 -- число с плавающей точкой (float)
print(a - b) --> '2.6666666666667'
\0
). Lua не делает никаких предположений о содержимом строки, поэтому в ней можно хранить любые данные. Строковые литералы записываются в одинарных или двойных кавычках. Lua выполняет конвертацию между числами и строками во время выполнения программы. Строки можно конкатенировать (объединять) с помощью оператора ..
.
Строки могут содержать следующие управляющие последовательности в стиле языка C:
Последовательность | Character |
---|---|
\a |
bell - звуковой стгнал |
\b |
back space - забой |
\f |
form feed |
\n |
newline - новая строка |
\r |
carriage return |
\t |
horizontal tab - горизонтальная табуляция |
\v |
vertical tab - вертикальная табуляция |
\\ |
backslash - обратный слеш |
\" |
double quote - двойная кавычка |
\' |
single quote - одниарная кавычка |
\[ |
left square bracket - левая квадратная скобка |
\] |
right square bracket - левая квадратная скобка |
\ddd |
символ, обозначаемый его числовым значением, где ddd - последовательность до трех десятичных цифр |
my_string = "hello"
another_string = 'world'
print(my_string .. another_string) --> "helloworld"
print("10.2" + 1) --> 11.2
print(my_string + 1) -- ошибка, не удается преобразовать "hello"
print(my_string .. 1) --> "hello1"
print("one\nstring") --> one
--> string
print("\097bc") --> "abc"
multi_line_string = [[
Здесь представлен фрагмент текста, занимающий несколько строк.Все это помещается
в строку что иногда бывает очень удобно.
]]
function name(param1, param2) ... end
).
-- Присвоить переменной 'my_plus' функцию
my_plus = function(p, q)
return p + q
end
print(my_plus(4, 5)) --> 9
-- Удобный синтаксис для присвоения функции переменной 'my_mult'
function my_mult(p, q)
return p * q
end
print(my_mult(4, 5)) --> 20
-- Принимает функцию в качестве параметра 'func'
function operate(func, p, q)
return func(p, q) -- Вызывает предоставленную функцию с параметрами 'p' и 'q'
end
print(operate(my_plus, 4, 5)) --> 9
print(operate(my_mult, 4, 5)) --> 20
-- Создать функцию суммирования и вернуть ее
function create_adder(n)
return function(a)
return a + n
end
end
adder = create_adder(2)
print(adder(3)) --> 5
print(adder(10)) --> 12
1
, а не 0
.
-- Инициализировать таблицу как последовательность
weekdays = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(weekdays[1]) --> "Sunday"
print(weekdays[5]) --> "Thursday"
-- Инициализировать таблицу как запись со значениями последовательности
moons = { Earth = { "Moon" },
Uranus = { "Puck", "Miranda", "Ariel", "Umbriel", "Titania", "Oberon" } }
print(moons.Uranus[3]) --> "Ariel"
-- Построить таблицу из пустого конструктора {}
a = 1
t = {}
t[1] = "first"
t[a + 1] = "second"
t.x = 1 -- тоже самое, что и t["x"] = 1
-- Итерация по парам ключ-значение таблицы
for key, value in pairs(t) do
print(key, value)
end
--> 1 first
--> 2 second
--> x 1
u = t -- u теперь ссылается на ту же таблицу, что и t
u[1] = "changed"
for key, value in pairs(t) do -- продолжает итерацию над t!
print(key, value)
end
--> 1 changed
--> 2 second
--> x 1
+
, -
, *
, /
, унарный -
(отрицание) и экспоненциальный ^
.
a = -1
print(a * 2 + 3 / 4^5) --> -1.9970703125
Lua обеспечивает автоматические преобразования между числами и строками во время выполнения. Любая числовая операция, применяемая к строке, пытается преобразовать строку в число:
print("10" + 1) --> 11
<
(меньше чем), >
(больше чем), <=
(меньше или равно), >=
(больше или равно), ==
(равно), ~=
(не равно). Эти операторы всегда возвращают true
или false
. Значения разных типов считаются разными. Если типы одинаковы, они сравниваются в соответствии с их значениями. Lua сравнивает таблицы, userdata и функции по ссылке. Два таких значения считаются равными, только если они ссылаются на один и тот же объект.
a = 5
b = 6
if a <= b then
print("a меньше или равно b")
end
print("A" < "a") --> true
print("aa" < "ab") --> true
print(10 == "10") --> false
print(tostring(10) == "10") --> true
and
, or
и not
. and
возвращает первый аргумент, если он равен false
, иначе возвращает второй аргумент. or
возвращает первый аргумент, если он не является false
, иначе возвращает второй аргумент.
print(true or false) --> true
print(true and false) --> false
print(not false) --> true
if a == 5 and b == 6 then
print("a равно 5 и b равно 6")
end
..
. Числа при конкатенации преобразуются в строки.
print("donkey" .. "kong") --> "donkeykong"
print(1 .. 2) --> "12"
#
. Длина строки — это количество байт. Длина таблицы — это длина ее последовательности, количество индексов, пронумерованных от 1
и выше, в которых значение не равно nil
. Примечание: Если в последовательности есть “дыры” со значением nil
, то длиной может быть любой индекс, предшествующий значению nil
.
s = "donkey"
print(#s) --> 6
t = { "a", "b", "c", "d" }
print(#t) --> 4
u = { a = 1, b = 2, c = 3 }
print(#u) --> 0
v = { "a", "b", nil }
print(#v) --> 2
Lua предоставляет традиционный набор конструкций для контроля над потоком выполнения.
then
, если условие истинно, в противном случае выполняет необязательную часть else
. Вместо вложенных операторов if
можно использовать elseif
. Это заменяет оператор switch, которого в Lua нет.
a = 5
b = 4
if a < b then
print("a is smaller than b")
end
if a == '1' then
print("a is 1")
elseif a == '2' then
print("a is 2")
elseif a == '3' then
print("a is 3")
else
print("I have no idea what a is...")
end
weekdays = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
-- Напечатать каждый день недели
i = 1
while weekdays[i] do
print(weekdays[i])
i = i + 1
end
weekdays = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
-- Напечатать каждый день недели
i = 0
repeat
i = i + 1
print(weekdays[i])
until weekdays[i] == "Saturday"
for
: числовой и общий. Числовой for
принимает 2 или 3 числовых значения, а общий for
перебирает все значения, возвращаемые функцией iterator.
-- Печатать числа от 1 до 10
for i = 1, 10 do
print(i)
end
-- Печатать числа от 1 до 10 и каждый раз увеличивать на 2
for i = 1, 10, 2 do
print(i)
end
-- Печатать числа от 10 до 1
for i=10, 1, -1 do
print(i)
end
t = { "a", "b", "c", "d" }
-- Итерировать последовательность и вывести значения
for i, v in ipairs(t) do
print(v)
end
break
используется для выхода из внутреннего блока цикла for
, while
или repeat
. Оператор return
используется для возврата значения из функции или для завершения выполнения функции и возврата к вызывающей стороне. break
или return
могут появляться только в качестве последнего оператора блока.
a = 1
while true do
a = a + 1
if a >= 100 then
break
end
end
function my_add(a, b)
return a + b
end
print(my_add(10, 12)) --> 22
Все объявляемые переменные по умолчанию являются глобальными, то есть они доступны во всех частях контекста среды выполнения Lua. Переменные можно явно объявить локальными
, что означает, что переменная будет существовать только в текущей области видимости.
Каждый исходный файл Lua задает отдельную область видимости. Локальные объявления на самом верхнем уровне файла означают, что переменная является локальной для файла Lua-скрипта. Каждая функция создает еще одну вложенную область видимости, а каждый блок управляющей структуры создает дополнительные области видимости. Область видимости можно явно создать с помощью ключевых слов do
и end
. Lua представляет собой лексическую область видимости, то есть область видимости, которая имеет полный доступ к локальным переменным из объемлющей области видимости. Следует помнить, что локальные переменные должны быть объявлены до их использования.
function my_func(a, b)
-- 'a' и 'b' являются локальными для этой функции и доступны из ее области видимости
do
local x = 1
end
print(x) --> nil. 'x' недоступна за пределами области видимости do-end
print(foo) --> nil. 'foo' объявлена после 'my_func'
print(foo_global) --> "value 2"
end
local foo = "value 1"
foo_global = "value 2"
print(foo) --> "value 1". 'foo' доступна в самой верхней области видимости после объявления.
Стоит заметить, что если объявить функции локальными
в файле скрипта (что, как правило, является хорошей идеей), необходимо следить за упорядочиванием кода. Можно использовать предварительное объявление, если у есть функции, которые взаимно вызывают друг друга.
local func2 -- Предварительно объявить 'func2'
local function func1(a)
print("func1")
func2(a)
end
function func2(a) -- или func2 = function(a)
print("func2")
if a < 10 then
func1(a + 1)
end
end
function init(self)
func1(1)
end
В случае когда пишется функция, вложенная в другую функцию, она (вложенная) также имеет полный доступ к локальным переменным объемлющей функции. Это весьма эффективная конструкция.
function create_counter(x)
-- 'x' является локальной переменной в 'create_counter'
return function()
x = x + 1
return x
end
end
count1 = create_counter(10)
count2 = create_counter(20)
print(count1()) --> 11
print(count2()) --> 21
print(count1()) --> 12
Локальные переменные, объявленные в блоке, будут затенять переменные с тем же именем из соседнего блока.
my_global = "global"
print(my_global) -->"global"
local v = "local"
print(v) --> "local"
local function test(v)
print(v)
end
function init(self)
v = "apple"
print(v) --> "apple"
test("banana") --> "banana"
end
Функции выполняются от начала до конца, и нет возможности остановить их на полпути. Корутины позволяют это сделать, что может быть очень удобно в некоторых случаях. Предположим, мы хотим создать весьма специфическую покадровую анимацию, в которой мы перемещаем игровой объект из позиции 0
по оси Y в некоторые определенные позиции по Y с кадра 1 по кадр 5. Мы могли бы решить эту задачу с помощью счетчика в функции update()
(см. ниже) и списка позиций. Однако с помощью корутины мы получаем очень чистую реализацию, которую легко расширять и работать с ней. Все состояние содержится в самой корутине.
Когда корутина завершает работу, она возвращает управление вызывающей стороне, но запоминает точку выполнения, чтобы в дальнейшем продолжить работу с этого места.
-- Это наша корутина
local function sequence(self)
coroutine.yield(120)
coroutine.yield(320)
coroutine.yield(510)
coroutine.yield(240)
return 440 -- вернуть окончательное значение
end
function init(self)
self.co = coroutine.create(sequence) -- Создать корутину. 'self.co' is a thread object
go.set_position(vmath.vector3(100, 0, 0)) -- Задать начальную позицию
end
function update(self, dt)
local status, y_pos = coroutine.resume(self.co, self) -- Продолжить выполнение корутины
if status then
-- Если корутина все еще не завершена/мертва, используйте ее возвращаемое значение в качестве новой позиции
go.set_position(vmath.vector3(100, y_pos, 0))
end
end
Все объявляемые переменные по умолчанию являются глобальными, что означает, что они доступны во всех частях контекста среды выполнения Lua. В Defold есть параметр shared_state в game.project, который управляет этим контекстом. Если опция включена, все скрипты, GUI-крипты и рендер-скрипт оцениваются в едином Lua-контексте, и глобальные переменные доступны повсюду. Если опция не установлена, движок выполняет скрипты, GUI-скрипты и рендер-скрипт в отдельных контекстах.
Defold позволяет использовать один и тот же файл скрипта в нескольких отдельных компонентах игрового объекта. Все локально объявленные переменные совместно используются компонентами, в которых запущен один и тот же скрипт.
-- 'my_global_value' будет доступно из всех скриптов, GUI-скриптов, рендер-скрипта и модулей (Lua-файлы)
my_global_value = "global scope"
-- Это значение будет общим для всех экземпляров компонентов, которые используют этот конкретный файл скрипта
local script_value = "script scope"
function init(self, dt)
-- Это значение будет доступно для данного экземпляра компонента скрипта
self.foo = "self scope"
-- Это значение будет доступно внутри init() и после его объявления
local local_foo = "local scope"
print(local_foo)
end
function update(self, dt)
print(self.foo)
print(my_global_value)
print(script_value)
print(local_foo) -- выведет nil, так как local_foo виден лишь в init()
end
В высокопроизводительной игре, которая должна работать на плавной скорости 60 FPS, небольшие просчеты в производительности могут сильно повлиять на впечатления от игры. Есть несколько простых общих аспектов, на которые следует обратить внимание, и некоторые нюансы, которые могут показаться незначительными.
Начинайте с простых вещей. Как правило, хорошей идеей является написание простого кода, без лишних циклов. Иногда возникает необходимость итерации над списками элементов, но следует быть осторожным, если список элементов достаточно велик. Этот пример выполняется за чуть более 1 миллисекунду на довольно приличном ноутбуке, что может иметь значение, если каждый кадр длится всего 16 миллисекунд (при 60 FPS), при этом движок, рендер-скрипт, симуляция физики и пр. съедают часть этого времени.
local t = socket.gettime()
local table = {}
for i=1,2000 do
table[i] = vmath.vector3(i, i, i)
end
print((socket.gettime() - t) * 1000)
-- DEBUG:SCRIPT: 0.40388
Используйте значение, возвращаемое из socket.gettime()
(seconds since system epoch) для проверки подозрительного кода.
По умолчанию сборка мусора в Lua выполняется автоматически в фоновом режиме и возвращает память, которую выделила среда выполнения Lua. Сборка мусора может оказаться трудоемкой задачей, поэтому целесообразно сократить количество объектов, подлежащих уборке:
local v = 42
)local s = "some_string"
создаст новый объект и присвоит ему s
. Сама локальная s
не будет генерировать мусор, но объект строки будет. Использование одной и той же строки несколько раз не требует дополнительных затрат памяти.{ ... }
) создается новая таблица.function () ... end
, а не вызов определенной функции)function(v, ...) end
) создают таблицу для символов замещения (ellipsis) каждый раз, когда функция вызывается (в Lua до версии 5.2, или если не используется LuaJIT).dofile()
и dostring()
Существует множество случаев, когда можно избежать создания новых объектов и вместо этого повторно использовать уже имеющиеся. Например, в конце каждого update()
обычно выполняется следующее:
-- Сбросить скорость
self.velocity = vmath.vector3()
Легко забыть, что каждый вызов vmath.vector3()
создает новый объект. Давайте узнаем, сколько памяти использует один vector3
:
print(collectgarbage("count") * 1024) -- 88634
local v = vmath.vector3()
print(collectgarbage("count") * 1024) -- 88704. 70 байт было выделено всего
70 байт было добавлено между вызовами collectgarbage()
, но это включает выделения для большего, чем объект vector3
. Каждая печать результата из collectgarbage()
строит строку, которая сама по себе добавляет 22 байта мусора:
print(collectgarbage("count") * 1024) -- 88611
print(collectgarbage("count") * 1024) -- 88633. 22 байта выделено
Таким образом, vector3
весит 70-22=48 байт. Это не много, но если создавать по одному в каждом кадре в игре с частотой 60 FPS, то внезапно возникают 2.8 кБ мусора в секунду. С 360 компонентами скрипта, которые создают по одному vector3
каждый кадр, мы получим 1 МБ мусора в секунду. Цифры могут складываться очень быстро. Когда среда выполнения Lua собирает мусор, это может съесть много драгоценных миллисекунд — особенно на мобильных платформах.
Один из способов избежать выделения — создать vector3
и затем продолжать работать с этим же объектом. Например, для сброса vector3
мы можем использовать следующую конструкцию:
-- Вместо того чтобы делать self.velocity = vmath.vector3(), который создает новый объект,
-- мы обнуляем существующий вектор скорости компонента объекта
self.velocity.x = 0
self.velocity.y = 0
self.velocity.z = 0
Схема сбора мусора по умолчанию может быть неоптимальной для некоторых критически важных приложений. Если в игре или приложении наблюдаются зависания, возможно, вы стоит настроить сбор мусора в Lua с помощью функции collectgarbage()
Lua function. Например, можно запускать сборщик на короткое время каждый кадр с низким значением шага
. Чтобы получить представление о том, сколько памяти потребляет игра или приложение, можно вывести текущее количество мусорных байтов с помощью функции:
print(collectgarbage("count") * 1024)
Общим соображением в дизайне реализации является вопрос о том, как структурировать код для совместного поведения. Возможны несколько подходов.
Кроме того, даже если возможно, чтобы код модуля напрямую изменял внутренности игрового объекта (посредством передачи self
в функцию модуля), настоятельно не рекомендуется делать это, так как это создаст очень тесную связь.
go.property()
, который указывает на целевой игровой объект.
Преимущество такой настройки заключается в том, что можно бросить игровой объект поведения в коллекцию, содержащую целевой объект. При этом дополнительный код не требуется.
В ситуациях, когда необходимо управлять большим количеством игровых объектов, такая конструкция не является предпочтительной, поскольку объект поведения дублируется для каждого экземпляра, и каждый объект будет занимать память.
Did you spot an error or do you have a suggestion? Please let us know on GitHub!
GITHUB