This translation is community contributed and may not be up to date. We only maintain the English version of the documentation. Read this manual in English
У рушій Defold вбудовано мову Lua для написання скриптів. Lua — легка динамічна мова, потужна, швидка й проста для вбудовування. Її широко використовують як мову скриптів у відеоіграх. Програми Lua пишуть із простим процедурним синтаксисом. Мова має динамічну типізацію, а програми виконує інтерпретатор байткоду. Вона підтримує автоматичне керування пам’яттю з інкрементальним збиранням сміття.
Цей посібник стисло ознайомлює з основами програмування мовою Lua загалом і з тим, що слід враховувати під час роботи з Lua у Defold. Якщо ви вже маєте певний досвід роботи з Python, Perl, Ruby, Javascript або подібною динамічною мовою, то зможете досить швидко розпочати роботу. Якщо ви лише починаєте програмувати, варто спершу прочитати книжку з Lua для початківців. Таких книжок чимало.
Defold використовує LuaJIT — високооптимізовану версію Lua, придатну для ігор та іншого програмного забезпечення, для якого критична продуктивність. Вона зберігає повну сумісність із Lua 5.1 та підтримує всі функції стандартних бібліотек Lua і повний набір функцій API Lua/C.
LuaJIT також додає кілька розширень мови і деякі можливості Lua 5.2 та 5.3.
Ми прагнемо забезпечити однакову роботу Defold на всіх платформах, але наразі між платформами є кілька невеликих відмінностей у версії мови Lua:
Щоб гарантувати роботу гри на всіх підтримуваних платформах, наполегливо рекомендуємо використовувати ЛИШЕ можливості мови Lua 5.1.
Defold містить усі стандартні бібліотеки Lua 5.1, а також бібліотеки для роботи із сокетами та бітових операцій:
assert(), error(), print(), ipairs(), require() тощо)Усі бібліотеки описано в довіднику API.
Програми мають простий, зрозумілий синтаксис. Інструкції записують по одній на рядок, і позначати кінець інструкції не потрібно. За бажанням можна розділяти інструкції крапкою з комою ;. Блоки коду обмежуються ключовими словами й завершуються ключовим словом end. Коментарі можна записувати як окремий блок або до кінця рядка:
--[[
Here is a block of comments that can run
over several lines in the source file.
--]]
a = 10
b = 20 ; c = 30 -- two statements on one line
if my_variable == 3 then
call_some_function(true) -- Here is a line comment
else
call_another_function(false)
end
Lua має динамічну типізацію: змінні не мають типів, а значення мають. На відміну від статично типізованих мов, ви можете довільно присвоювати будь-яке значення будь-якій змінній.
У Lua є вісім основних типів:
nilnil. Зазвичай воно позначає відсутність корисного значення, наприклад у змінних, яким ще нічого не присвоєно.
print(my_var) -- will print 'nil' since 'my_var' is not yet assigned a value
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) --> prints '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:
| Послідовність | Символ |
|---|---|
\a |
звуковий сигнал |
\b |
повернення на один символ |
\f |
переведення сторінки |
\n |
новий рядок |
\r |
повернення каретки |
\t |
горизонтальна табуляція |
\v |
вертикальна табуляція |
\\ |
зворотна скісна риска |
\" |
подвійні лапки |
\' |
одинарні лапки |
\[ |
ліва квадратна дужка |
\] |
права квадратна дужка |
\ddd |
символ, заданий числовим значенням, де ddd — послідовність щонайбільше з трьох десяткових цифр |
my_string = "hello"
another_string = 'world'
print(my_string .. another_string) --> "helloworld"
print("10.2" + 1) --> 11.2
print(my_string + 1) -- error, can't convert "hello"
print(my_string .. 1) --> "hello1"
print("one\nstring") --> one
--> string
print("\097bc") --> "abc"
multi_line_string = [[
Here is a chunk of text that runs over several lines. This is all
put into the string and is sometimes very handy.
]]
function name(param1, param2) ... end).
-- Assign 'my_plus' to function
my_plus = function(p, q)
return p + q
end
print(my_plus(4, 5)) --> 9
-- Convenient syntax to assign function to variable 'my_mult'
function my_mult(p, q)
return p * q
end
print(my_mult(4, 5)) --> 20
-- Takes a function as parameter 'func'
function operate(func, p, q)
return func(p, q) -- Calls the provided function with parameters 'p' and 'q'
end
print(operate(my_plus, 4, 5)) --> 9
print(operate(my_mult, 4, 5)) --> 20
-- Create an adder function and return it
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.
-- Initialize a table as a sequence
weekdays = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
print(weekdays[1]) --> "Sunday"
print(weekdays[5]) --> "Thursday"
-- Initialize a table as a record with sequence values
moons = { Earth = { "Moon" },
Uranus = { "Puck", "Miranda", "Ariel", "Umbriel", "Titania", "Oberon" } }
print(moons.Uranus[3]) --> "Ariel"
-- Build a table from an empty constructor {}
a = 1
t = {}
t[1] = "first"
t[a + 1] = "second"
t.x = 1 -- same as t["x"] = 1
-- Iterate over the table key, value pairs
for key, value in pairs(t) do
print(key, value)
end
--> 1 first
--> 2 second
--> x 1
u = t -- u now refers to the same table as t
u[1] = "changed"
for key, value in pairs(t) do -- still iterating over t!
print(key, value)
end
--> 1 changed
--> 2 second
--> x 1
userdata дає змогу зберігати довільні дані C у змінних Lua. Defold використовує об’єкти Lua типу userdata для зберігання хешованих значень (hash), об’єктів URL (url), математичних об’єктів (vector3, vector4, matrix4, quaternion), ігрових об’єктів (game object), вузлів GUI (node), предикатів рендерингу (predicate), цільових буферів рендерингу (render_target) і буферів констант рендерингу (constant_buffer)+, -, *, /, унарний - (зміна знака) і піднесення до степеня ^.
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 is less than or equal to 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 is 5 and b is 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"}
-- Print each weekday
i = 1
while weekdays[i] do
print(weekdays[i])
i = i + 1
end
weekdays = {"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"}
-- Print each weekday
i = 0
repeat
i = i + 1
print(weekdays[i])
until weekdays[i] == "Saturday"
for: числовий і узагальнений. Числовий for приймає 2 або 3 числові значення, а узагальнений for перебирає всі значення, які повертає функція-_ітератор_.
-- Print the numbers 1 to 10
for i = 1, 10 do
print(i)
end
-- Print the numbers 1 to 10 and increment with 2 each time
for i = 1, 10, 2 do
print(i)
end
-- Print the numbers 10 to 1
for i=10, 1, -1 do
print(i)
end
t = { "a", "b", "c", "d" }
-- Iterate over the sequence and print the values
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. Ви можете явно оголосити змінні як local, тоді змінна існуватиме лише в поточній області видимості.
Кожен файл вихідного коду Lua визначає окрему область видимості. Оголошення local на найвищому рівні файлу означає, що змінна локальна для цього файлу скрипту Lua. Кожна функція створює ще одну вкладену область видимості, а кожен блок керівної конструкції — додаткові області видимості. Ви можете явно створити область видимості за допомогою ключових слів do та end. Lua використовує лексичну область видимості: кожна область має повний доступ до локальних змінних зовнішньої області видимості. Зауважте, що локальні змінні потрібно оголосити до їх використання.
function my_func(a, b)
-- 'a' and 'b' are local to this function and available through its scope
do
local x = 1
end
print(x) --> nil. 'x' is not available outside the do-end scope
print(foo) --> nil. 'foo' is declared after 'my_func'
print(foo_global) --> "value 2"
end
local foo = "value 1"
foo_global = "value 2"
print(foo) --> "value 1". 'foo' is available in the topmost scope after declaration.
Зауважте: якщо ви оголошуєте функції як local у файлі скрипту (що зазвичай варто робити), потрібно стежити за порядком коду. Для функцій, що взаємно викликають одна одну, можна використовувати попередні оголошення.
local func2 -- Forward declare 'func2'
local function func1(a)
print("func1")
func2(a)
end
function func2(a) -- or 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' is a local variable in '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 у точно задані позиції за цією віссю протягом кадрів від 1 до 5. Це можна реалізувати за допомогою лічильника у функції update() (див. нижче) і списку позицій. Однак співпрограма дає дуже зрозумілу реалізацію, яку легко розширювати та з якою зручно працювати. Увесь стан міститься в самій співпрограмі.
Коли співпрограма призупиняється, вона повертає керування коду, що її викликав, але запам’ятовує точку виконання, щоб згодом продовжити з неї.
-- This is our coroutine
local function sequence(self)
coroutine.yield(120)
coroutine.yield(320)
coroutine.yield(510)
coroutine.yield(240)
return 440 -- return the final value
end
function init(self)
self.co = coroutine.create(sequence) -- Create the coroutine. 'self.co' is a thread object
go.set_position(vmath.vector3(100, 0, 0)) -- Set initial position
end
function update(self, dt)
local status, y_pos = coroutine.resume(self.co, self) -- Continue execution of coroutine.
if status then
-- If the coroutine is still not terminated/dead, use its yielded return value as a new position
go.set_position(vmath.vector3(100, y_pos, 0))
end
end
Усі змінні, які ви оголошуєте, за замовчуванням глобальні, тобто доступні в усіх частинах контексту середовища виконання Lua. У Defold цим контекстом керує налаштування shared_state у файлі game.project. Якщо цей параметр увімкнено, усі скрипти, скрипти GUI та скрипт рендерингу виконуються в одному контексті Lua, а глобальні змінні видимі всюди. Якщо параметр вимкнено, рушій виконує скрипти, скрипти GUI та скрипт рендерингу в окремих контекстах.

У Defold можна використовувати той самий файл скрипту в кількох окремих компонентах (component) ігрових об’єктів. Усі локально оголошені змінні є спільними для компонентів, які виконують той самий файл скрипту.
-- 'my_global_value' will be available from all scripts, gui_scripts, render script and modules (Lua files)
my_global_value = "global scope"
-- this value will be shared through all component instances that use this particular script file
local script_value = "script scope"
function init(self, dt)
-- This value will be available on this script component instance
self.foo = "self scope"
-- this value will be available inside init() and after it's declaration
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) -- will print nil, since local_foo is only visible in 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() (секунди від початку системної епохи), щоб вимірювати швидкодію підозрілих ділянок коду.
За замовчуванням збирання сміття в Lua автоматично працює у фоні й вивільняє пам’ять, виділену середовищем виконання Lua. Збирання великої кількості сміття може забирати багато часу, тому варто зменшувати кількість об’єктів, які потребуватимуть збирання:
local v = 42)local s = "some_string" створить новий об’єкт і присвоїть посилання на нього змінній s. Сама локальна змінна s не генеруватиме сміття, але об’єкт рядка — генеруватиме. Багаторазове використання того самого рядка не збільшує витрати пам’яті.{ ... }) створює нову таблицю.function () ... end, а не виклик визначеної функції)function(v, ...) end) створюють таблицю для трикрапки щоразу, коли функцію викликають (у Lua до версії 5.2 або якщо не використовується LuaJIT).dofile() і dostring()У багатьох випадках можна уникнути створення нових об’єктів і натомість повторно використовувати вже наявні. Наприклад, наприкінці кожного update() часто можна побачити таке:
-- Reset velocity
self.velocity = vmath.vector3()
Легко забути, що кожен виклик vmath.vector3() створює новий об’єкт. З’ясуймо, скільки пам’яті використовує один vector3:
print(collectgarbage("count") * 1024) -- 88634
local v = vmath.vector3()
print(collectgarbage("count") * 1024) -- 88704. 70 bytes in total has been allocated
Між викликами collectgarbage() додалося 70 байтів, але це включає виділення пам’яті не лише для об’єкта vector3. Кожне виведення результату collectgarbage() формує рядок, який сам по собі додає 22 байти сміття:
print(collectgarbage("count") * 1024) -- 88611
print(collectgarbage("count") * 1024) -- 88633. 22 bytes allocated
Отже, vector3 займає 70-22=48 байтів. Це небагато, але якщо створювати один такий об’єкт кожного кадру в грі з частотою 60 FPS, це вже 2,8 КБ сміття за секунду. Якщо 360 компонентів-скриптів створюють по одному vector3 кожного кадру, утворюється 1 МБ сміття за секунду. Ці обсяги можуть зростати дуже швидко. Коли середовище виконання Lua збирає сміття, це може забирати багато цінних мілісекунд — особливо на мобільних платформах.
Один зі способів уникнути виділення пам’яті — створити vector3, а потім працювати з тим самим об’єктом. Наприклад, щоб обнулити vector3, можна використати таку конструкцію:
-- Instead of doing self.velocity = vmath.vector3() which creates a new object
-- we zero an existing velocity vector object's components
self.velocity.x = 0
self.velocity.y = 0
self.velocity.z = 0
Стандартна схема збирання сміття може бути неоптимальною для деяких застосунків, чутливих до затримок. Якщо ви помічаєте ривки у грі чи застосунку, варто налаштувати збирання сміття в Lua за допомогою функції collectgarbage(). Наприклад, можна запускати збирач ненадовго кожного кадру з малим значенням step. Щоб оцінити, скільки пам’яті споживає ваша гра чи застосунок, можна вивести поточний обсяг сміття в байтах:
print(collectgarbage("count") * 1024)
Під час проєктування реалізації часто постає питання, як структурувати код спільної поведінки. Можливі кілька підходів.

Крім того, хоча код модуля може безпосередньо змінювати внутрішній стан ігрового об’єкта (якщо передати self функції модуля), ми наполегливо не рекомендуємо цього робити, оскільки це створює дуже тісну зв’язаність.

go.property(), що вказує на цільовий ігровий об’єкт.

Перевага такої організації в тому, що ви можете додати ігровий об’єкт поведінки до колекції (collection), яка містить цільовий об’єкт. Додатковий код узагалі не потрібен.
Коли потрібно керувати великою кількістю ігрових об’єктів, цей підхід менш придатний, оскільки об’єкт поведінки дублюється для кожного екземпляра (instance), а кожен об’єкт споживає пам’ять.