Lua для всієї родини. Основи Lua




Всім привіт.

Сьогодні ми поверхово пройдемося мовою Lua, його деяким можливостям, а також запуску наших сценаріїв у RakBot.
Lua - скриптова мова програмування, призначена для швидкої обробки даних. За допомогою даної мови багато розробників створюють штучний інтелект в іграх, пишуть алгоритми генерації рівнів, а також він використовується для розробки ресурсів/ігрових мод в Multi Theft Auto: San Andreas (аналог SA:MP). Насправді це найпростіша моваі за допомогою нього ми будемо вчитися писати власну логіку для роботів, яку використовуватиме RakBot.

Пройдемося за основами програмування, з якими нам доведеться працювати.

Зверніть увагу : ця стаття буде урізана в плані мови Lua, тому що в RakBot використовується лише невелика її частина. Багато можливостей Lua просто відсутня в RakBot, тому я буду орієнтуватися на версію з RakBot.

Є традиція у всіх авторів книг та документацій різних мов, це перша програма, що друкує "Hello World".
Що ж, давайте спробуємо написати її, але вже в RakBot. Переходимо на офіційний сайт RakBot та шукаємо розділ "Доступні функції", розділ "Події".

Нам потрібна подія onScriptStart(), які викликається автоматично при завантаженні скрипта самим RakBot.

У цій функції нам необхідно описати логіку, яка буде писати в чат-лог RakBot"a "Hello World". Для цього, на тій же сторінці в документації, подивимося на розділ "Функції".

Перша функція printLog(text)- Це те, що нам і потрібно. За допомогою цієї функції ми відправимо повідомлення в чат RakBot"а. Для цього ми напишемо:

Ми написали логіку у якомусь текстовому документі, але як сказати RakBot, щоб він виконав наш сценарій? Для цього необхідно зберегти файл із розширенням .lua і покласти його в папку scriptsу папці з RakBot.
Я зберіг текстовий документз ім'ям " example.lua Давайте спробуємо запустити RakBot і подивитися, що у нас вийшло.

Як ми бачимо, при запуску RakBot, він знаходить скрипт. example.lua", після чого виконує його. З цього ми можемо зробити висновок, що ініціалізація сценарію відбувається при запуску самого RakBot або при перезавантаженні всіх сценаріїв командою !reloadscripts.

Вітаю, Ви тільки-но написали свій власний сценарій для RakBot!

Ми вже навчилися писати Hello World у консолі RakBot"а, але ми хочемо писати складних ботів, які будуть робити всю роботу за нас, враховуючи ті чи інші умови. На цьому ми зупинимося.
Практично все, що відбувається у програмуванні, можна описати так: візьми дані, щось з ними зроби, віддай результат.
У цьому випадку даними виступає сам RakBot. Він сам запускає наші сценарії, а також сам передає нам дані, які ми можемо обробити так, як хочемо і в кінці отримати результат.

Давайте напишемо найпростіший сценарій із умовою. Умовою буде нік бота. Якщо нік бота "СМaster", значить ми виведемо в чат RakBot"а "CM FOREVER", якщо ж нік бота зовсім інший - виведемо в чат "Nonamer".
Для цього нам допоможе умовний оператор if else, він же оператор розгалуження. Він приймає він умову, яке має повернути або true , або false . Якщо умова дорівнює true, тоді код усередині буде виконано, якщо false - не буде виконано.
У цьому будується більшість логіки будь-якого докладання. Дослівно if перекладається як "ЯКЩО", then - "ЗНАЧИТЬ", else - "ІНАЧЕ" Якщо це дуже складно - не переживайте, Ви зрозумієте все далі.

У Lua є наступні оператори порівняння:
> Більше
< Меньше
>= Більше чи одно
<= Меньше или равно
~= Не дорівнює
== Рівно

Якщо ми напишемо " CMaster" == "CM" - у нас буде значення False , тобто брехня
Якщо ми напишемо " CMaster" == "CMaster" - У нас буде значення True, тобто істина.

5 > 10 - брехня 5< 10 -- истина 10 ~= 15 -- истина 10 >= 5 - істина

Спробуємо використати логіку розгалуження у нашому попередньому сценарії.

Той код, який ми писали раніше:

Function onScriptStart() printLog("Hello world!"); end

Перетворимо так:

Function onScriptStart() botName = getNickName() if(botName == "CMaster") then printLog("CM FOREVER"); else printLog("Nonamer"); end end

Давайте розберемо цей код, починаючи зверху. Я раджу одразу починати вчити читати код. Тому спробуємо прочитати, що в нас вийшло

Function onScriptStart() - створюємо фукнцію з ім'ям onScriptStart botName = getNickName() - у змінну botName записуємо ім'я бота if(botName == "CMaster") then - якщо ім'я бота дорівнює "CMaster", значить printLog("CM FOREVER"); - пишемо у чат "CM Forever". else-ІНАЧЕ, або якщо ім'я бота НЕ РІВНЕ "CMaster" printLog("Nonamer"); - пишемо в чат "Nonamer" end-кінець умов end-кінець функції

Спробуймо перевірити код, який ми написали. Я зберіг змінений код так само під назвою " example.lua "і запустив RakBot з ніком" Mason_Bennett".

Після завантаження нашого сценарію, RakBot написав у чат Nonamer. Спробуємо зайти з ніком. CMaster".

Як бачимо, наша умова успішно працює і ми бачимо у чаті те, що й хотіли.

Пройдемося трохи змінними. У Вас є аркуш паперу і Ви хочете зберегти його. Зберегти як - кудись покласти, ніж втратити його. Наприклад, ми можемо покласти наш аркуш паперу в шафку і дістати тоді, коли нам буде потрібно. Якщо у нас буде новий листок і нам не потрібен буде старий – ми викинемо старий листок та покладемо новий.
Це і є логіка змінної. Ми можемо створювати змінну з іменами, якими хочемо і записувати в них значення, що ми зробили в попередньому прикладі зі змінною botName.

У Lua ми можемо записувати у змінну все, що хочемо. Наприклад, я хочу створити змінну з ім'ям PaperList і записати до неї текст "Lua – урок №2". Для цього я напишу:

PaperList = "Lua - урок №1"

Що ми тут зробили? Написали ім'я та використали оператор присвоєння "=" і тепер я можу використовувати цю змінну в будь-якому місці свого сценарію.
Думаю, якщо Ви згадаєте математику на рівні максимум 5 класу - тут буде все зрозуміло.

У Lua є кілька типів змінних, це nil, boolean, number, string. Не бійтеся, це дуже просто.

Насправді їх трохи більше, але я вже казав, що у RakBot більша частина функціоналу відсутня.

nil - відсутність значення.
boolean - логічні значення, що приймає два варіанти значень - або true, або false.
number - дійсне число з подвійною точністю. У Lua немає цілого чисельного типу, тому він виступає в якості і речовинного і цілого типу.
string – рядок, тут, я думаю, все зрозуміло.
Що ж, спробуємо створити кілька змінних і "погратися" з ними.

number = 0; - Створюємо змінну з ім'ям number і присвоюємо значення 0
number = number + 5; - надання значення змінної number + 5 (тобто, 0 + 5), тепер у нас зберігається тут число 5.
number ++; - ++ - Інкремент. Іншими словами - ви берете змінну та збільшуєте її на одну одиницю. Тобто (5 + 1) – тепер 6 лежить у нас у змінній number.
number --; - - Декремент. Іншими словами – зменшуємо на одну одиницю. (6 - 1) - тепер значення дорівнює 5.

string = "Hello" - створюємо змінну string зі значенням "Hello"
string = string .. "," - конкатенація рядків, воно ж додавання рядків. Що ми тут зробили? Вкажіть ім'я змінної, вказали оператор конкатенації ".. ", після чого вказали ще один рядок, який необхідно додати до першого. Тепер у нас у змінній "string" лежить значення "Hello,".
string = string .. getNickName() - Тепер, до "Hello," ми додали нік бота, нехай буде "Michel". Тепер у нас у змінній string лежить значення "Hello, Michel".

boolean = true; - Створюємо змінну boolean зі значенням true (ІСТИНА).
boolean = getNickName() == "Dimosha" - порівнюємо ім'я робота з рядком Dimosha. Так як ім'я бота у нас Michel, з попереднього прикладу, сюди запишеться значення false (БРЕХНЯ).

Трохи про функції. Існують функції, які повертають значення, а є ті, які не повертають значення. Як Ви встигли помітити, наша функція onScriptStartне повертає значення, а просто виконує код, вказаний усередині.
Ми можемо створювати власні функції для того, щоб ізолювати частину логіки з методу та виконувати ті чи інші операції.
Фукнції так само можуть приймати він значення, а можуть і приймати.

Давайте пройдемося найпростішим шляхом: фукнція без параметрів і без значення, що повертається, яка складатиме 5 + 10 і виводити результат в консоль RakBot'а.

Я створю функцію з ім'ям Add :

Function Add() -- Створюємо фукнцію Add printLog(5 + 10) -- використовуємо метод RakBot для виведення в консоль end-- Кінець функції

Ми створили не зовсім універсальну функцію з двох причин:
- якщо мені потрібно буде складати інші числа - мені доведеться створювати ще одну таку саму фукнцію
- я не можу використати отримане значення за межами фукнції

Спробуємо виправити перший недолік. Для цього ми додамо два параметри у функцію, які прийматимуть на себе числа для складання. Зробимо ми це так:

Function Add(a, b) printLog(5 + 10) end

Тепер у методі у нас доступні два значення, які містяться у двох нових змінних a та b, але на консоль у мене все одно виводиться 15. Виправимо це:

Function Add(a, b) printLog(a + b) end

Ідеально. Тепер, при виклику цього методу, ми отримуватимемо результат додавання в консолі. Спробуємо протестувати. Змінимо наш код у example.lua на наступний:

Function Add(a, b) printLog(a + b) end function onScriptStart() Add(5, 10); Add(123, 4324); Add(555, 111); end

І спробуємо запустити RakBot. Подивимося, що з цього вийде:

Це вирішило нашу першу проблему. Спробуймо вирішити другу, щоб наша функція повертала результат.

Перепишемо функцію Add :

Function Add(a, b) return a + b end

return - ключове слово повернення значення з функції. Перепишемо тепер метод onScriptStart:

Function onScriptStart() printLog("Перше значення: "..Add(5, 10)); printLog("Друге значення: "..Add(123, 4324)); printLog("Третє значення: "..Add(555, 111)); end

Подивимося, що вийшло.

Ми могли створити три змінні, надавши їм значення з функцій Addі після їх передавати в метод printLogале я не став цього робити, оскільки код виглядає більш читабельним і приємнішим.

Тепер ми навчилися створювати власні функції з параметрами, без параметрів і повертати з них значення. Я вважаю, що цих основ Вам вистачить сповна, щоб написати власного бота в рамках RakBot.

У цій серії уроків, яку я задумав, обговорюватиметься мова програмування Lua. Я постараюся зробити виклад якомога доступнішим для початківців, і саме на них орієнтуватимуся. Тобто, досвідчені Lua-кодери, швидше за все, не почерпнуть звідси нічого нового (впевнений, тут вони знайдуть лише простір для причіпок та зауважень, які, власне, з їхнього боку навіть вітається), але якщо у вас за плечима немає багатого досвіду програмування , то, думаю, дещо ви винесете.

Вся серія не підпорядковуватиметься якійсь системі. Уроки будуть послідовно вводити ряд конструкцій мови, щоб до третього чи четвертого уроку ви вже могли писати свої програми. Моя мета – підштовхнути вас до самостійного вивчення мови, допомогти відчути її, а не роз'яснити від А до Я – якщо хочете освоїти мову повністю, читайте довідкове керівництво (яке, хоч і погано, перекладено російською мовою: http://www.lua . ru/doc/). Чим раніше ви перейдете від уроків "для чайників" у Мережі до вивчення довідника, тим краще.

Якщо щось незрозуміло - обов'язково поставте питання в коментарях, і я та інші учасники постараємося вам допомогти.

Lua - популярна, нескладна для освоєння вбудовувана інтерпретована динамічно типізована мова програмування загального призначення. Ні, вам необов'язково розуміти і половини слів, сказаних у попередній пропозиції – головне знайте, що він популярний та нескладний. До речі, простотою, а також маленьким розміром дистрибутива (близько 150 кілобайт), він і заслужив на свою популярність. Скрипти на Lua підтримуються великою кількістю програм, у тому числі іграми. World of Warcraft та S.T.A.L.K.E.R. використовують мову Lua. Мій улюблений ігровий двигун дозволить вам за допомогою Lua з легкістю створювати різноманітні ігри. Як бачите, Lua відкриває вам чималі горизонти!

Перш ніж ми почнемо, вам слід облаштувати середовище для програмування: тобто знайти програму, яка б приймала написаний вами код на Lua і виконувала його: інтерпретатор. Тут є три варіанти:

1. Завантажити офіційний дистрибутив Lua з одного з сайтів, що їх поставляють.

З офіційного сайту Lua можна завантажити лише вихідні коди інтерпретатора. Однак, повивчивши http://lua.org/download.html у розділі Binaries, ви можете виявити посилання на сайти з виконуваними файламидля Windows. Один з них: . Завантажте звідти один з архівів (збігається з вашою платформою: Win32 або Win64) і розпакуйте його кудись, бажано в каталог з коротким шляхом: на кшталт C:\lua. Відтепер я вважатиму, що ви користуєтеся Windows, і ваш інтерпретатор лежить саме там.

Користувачам операційних систем на базі Linux у цьому сенсі простіше: їм достатньо скористатися пакетним менеджером та встановити Lua з репозиторіїв. У Debian і Ubuntu це робиться командою apt-get install lua, а Fedora, Red Hat і похідних дистрибутивах - yum install lua. Однак не довіряйте мені сліпо і зверніться до вашого довідника операційної системищоб дізнатися, як саме це робиться у вас.

2. Використовувати онлайн інтерпретатор.

Знаходиться за адресою http://www.lua.org/demo.html. Спочатку його може вистачити, проте надалі, коли ми торкнемося модулів, ви будете змушені використовувати офлайн-версію. Користуватися онлайн-інтерпретатором дуже просто: введіть у віконце з текстом вашу програму та натисніть кнопку Run. Програма буде виконана, у вікні Output з'явиться висновок вашої програми, а також звіти про помилки, якщо такі були допущені вами.

3. Використовувати IDE.

Наприклад, ZeroBrane Studio: http://studio.zerobrane.com/ . Є й інші – пошукайте в Інтернеті.

У ході сьогодні дві дещо різні версії Lua: 5.1 і 5.2. Я орієнтуватимусь на саму останню версію- версію 5.2, але обов'язково вкажу на важливі відмінності між нею та 5.1, оскільки остання теж досить поширена. До речі, Lua 5.1 виконує код у півтора рази швидше за Lua 5.2, щоб ви знали.

=== Урок №1 ===

Тож почнемо. Створіть в ізольованій від сторонніх файлівпапці файл main.lua і напишіть у нього:

200?"200px":""+(this.scrollHeight+5)+"px");">
-- main.lua --
print("Hello world!")

Після чого запустіть у командному рядку(не забудьте переміститися в директорію з main.lua за допомогою cd):

200?"200px":""+(this.scrollHeight+5)+"px");">
> C:\lua\lua.exe main.lua

У відповідь інтерпретатор Lua видасть:

200?"200px":""+(this.scrollHeight+5)+"px");">
Hello world!

В принципі, цього слід було очікувати. У програмі ми викликали функцію print. Функція print приймає довільне число параметрів і виводить їх на екран. У даному прикладіми передали їй рядок (ланцюжок символів) "Hello world!". З таким же успіхом можна передати як параметр:

200?"200px":""+(this.scrollHeight+5)+"px");">
print(8) - яке-небудь десяткове число
-- виведе: 8

Print(0xDEADBEEF) - шістнадцяткове число
-- виведе: 3735928559

Print("0xDEADBEEF") -- а це рядок, не число! Бачите лапки?
-- виведе: 0xDEADBEEF

Print(1.35e-4) -- число з плаваючою комою (дрібне число)
- Виведе 0.000135. 1.35e-4 слід розуміти як "1.35, помножене
- на десять в мінус четвертого ступеня, якщо хто не знає.

Print((198*99)-3*500 + 14/88) - вираз
-- Виведе значення виразу: 18102.159090909. Непогана альтернатива
- Настільному калькулятору!

Print(198/7, "fertilizer", 2^9) - кілька параметрів довільного
- Типу. Буде виведено значення кожного з них, розділені знаками
- Табуляції:
-- 28.285714285714 fertilizer 512
- Зверніть увагу, що лапки навколо fertilizer не виводяться!

Print(1,35) - два числа, а не десятковий дріб 1,35!
-- Кома використовується для розділення параметрів.
- Виведе:
-- 1 35

Знак "--" – не просто імітація знака тире, яка вставлена ​​для краси. Знаком "--" у Lua відзначаються коментарі: підказки для програміста, які ігноруються інтерпретатором, та призначені для того, щоб у коді було легше розібратися. Можете спробувати написати у програмі:

200?"200px":""+(this.scrollHeight+5)+"px");">
- print("nothing")

Інтерпретатор подумає, що це коментар, і не виконуватиме інструкцію.

Господині на замітку: якщо ви бажаєте надрукувати лише один рядок, можна написати виклик print так, без дужок:

200?"200px":""+(this.scrollHeight+5)+"px");">
print "Just one string"

Зручність, безперечно, сумнівна: просто майте на увазі, що так можна. Водночас такі виклики неприпустимі:

200?"200px":""+(this.scrollHeight+5)+"px");">
print 2 - не спрацює, 2 - не рядок.
print 2*2 + 6 - тим більше не спрацює

Str = "string!!" -- надали змінної str значення "string!!"
-- про змінні читайте нижче
print str - теж не спрацює.

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

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

200?"200px":""+(this.scrollHeight+5)+"px");">
<имя_переменной> = <выражение>

Наприклад:

200?"200px":""+(this.scrollHeight+5)+"px");">
star = 8 -- Тепер у змінній star зберігається число 8
wars = "owl" - У змінній wars - рядок "owl"
jedi = 42/2 - У змінній jedi - число 21
luke = star * jedi -- У змінній luke - число 168 (так, 21 помножити на 8)

Значення змінних та виразів з ними також можна вивести на екран:

200?"200px":""+(this.scrollHeight+5)+"px");">
print(star, wars, jedi, jedi-star+luke)
- Виведе:
-- 8 owl 21 181

Тільки не намагайтеся скласти змінні star і wars - спробувавши додати 8 до "owl", ви нічого хорошого не досягнете!

Як ви мали помітити, ім'я у змінної може бути практично будь-яким: головне, щоб воно не починалося з цифри. Серйозно, ви навіть можете оголосити змінну з назвою print, і тоді функція print перестане працювати, тому що ім'я print посилатиметься на оголошену змінну. Але є група слів, які заборонено використовувати як назви змінних - це ключові слова мови, з якими ми поки що не познайомилися, але які точно варто подивитися:

200?"200px":""+(this.scrollHeight+5)+"px");">
and break do else elseif end
false for function goto if in
local nil not or repeat return
then true until while

Створивши змінну з однією з цих назв, ви викличете помилку у програмі, і працювати вона точно не буде. Зверніть увагу: у Lua 5.1 ключового слова goto ні, і змінну так можна назвати, але ви краще так не робіть.
Також врахуйте, що імена змінних чутливі до регістру. Це означає, що foo, fOo, fOO і FOO - чотири різні змінні, тому якщо ви написали ім'я якоїсь змінної малими літерами, а пізніше написали його великими, то, швидше за все, програма не буде працювати коректно.

А тепер один важливий момент: що буде якщо ви випадково чи навмисно звернетеся до неіснуючої змінної? У більшості інших мов це викликає помилку, але в Lua така ситуація припустима. Вона трактується так, ніби неіснуюча змінна насправді існує, але її значення одно nil. nil- Запам'ятайте це слово! - особливий тип значення Lua, який означає "ніщо". Чи не нуль і не порожній рядок (рядок виду "" - спробуйте його вивести на екран), а просто ніщо. Порівняйте це з такою моделлю: є дві людини, одна з них має банківський рахунок, але на ньому немає грошей, а інший банківський рахунок немає взагалі. У термінах Lua буде вважатися, що на рахунку у першого – 0 доларів, а на рахунку у другого – nil. І навіть не доларів, а просто nil. Сподіваюся, я вас не заплутав.

Спробуйте, наприклад, запустити таку програму:

200?"200px":""+(this.scrollHeight+5)+"px");">
-- main.lua --
foo = "bar"
print(foo, baz)
- Виведе:
-- bar nil

Таким чином, у змінної baz, якої немає, але вважається, що вона є, значення nil, і функція print розуміє це і виводить його на екран у вигляді рядка "nil". У Lua є хороший метод перевірки існування змінної: якщо значення змінної не дорівнює nil, вона принаймні оголошена. З іншого боку, можна явно оголосити змінну, рівну nil:

200?"200px":""+(this.scrollHeight+5)+"px");">
cool_var=nil

Так можна робити, і хоча це на перший погляд і здається дурним, так іноді роблять. У наступних уроках ви дізнаєтеся, хто і навіщо, і, напевно, почнете робити так само. Іноді, звичайно.
Будьте обережні з nil"ом: надрукувати nil можна, але робити з ним арифметичні операції не можна! Тобто, якщо print(nil) зійде вам з рук, то конструкція на кшталт 99+nil викличе помилку, навіть якщо вам хотілося б, щоб 99+ nil дорівнював 99. Повірте, я теж засмутився, коли дізнався.

Резюме:
1. Ми дізналися про функцію print, що вона вміє і як правильно викликати її без дужок.
2. Дізналися, як оголошувати змінні, як обчислювати висловлювання (щоправда, дуже трішки), які можуть бути імена у змінних.
3. Дізналися про nil, перейнялися його містичною загадковістю і здобули впевненість у тому, що в майбутньому багато буде пов'язано з ним.

Для допитливих і охочих зміцнити свої знання пропоную прості вправи,які можна не виконувати, якщо ви почуваєтеся і так достатньо компетентним:
1. Напишіть програму, яка виводить приспів вашої улюбленої пісні.
2. Спробуйте вивести наступні вирази. Спробуйте зрозуміти, чому деякі з них працюють, а деякі – ні. Подивіться, які помилки викликають вислови, що не спрацювали.

200?"200px":""+(this.scrollHeight+5)+"px");">
2 + "string";
6 + "14";
"box" - "vox";
1 * "11b"
"148" * "1e6";


3. Напишіть програму, яка обмінює дві змінні значення. Тобто:

200?"200px":""+(this.scrollHeight+5)+"px");">
a = 6502
b = 8086


Зробіть так, щоб a дорівнювала 8086, а b - 6502. Для цього створіть третю змінну і здійсніть нехитрі перестановки. Переконайтеся, що завдання вирішено правильно, викликавши print(a,b) до обміну та print(a,b) після.

Нещодавно мій близький друг ходив на співбесіду щодо влаштування на роботу до місцевої компанії розробки ігор. Я не збираюся тут називати імена, скажу тільки, що це був своєрідний великий бутік Розробки Ігор у Ванкувері. Він не отримав роботи, але сьогодні не про нього. Особисто я вважаю, що одна з причин була через його недостатньо дружні стосунки зі скриптом, який вони використовують.

Вступ

Я займаюся цією областю, тому що навчаю студентів програмування ігор, але саме цій темі я приділив мало уваги у минулому. Ми охоплюємо Unreal Script як частину курсу "Використання існуючих". Але ми практично не розглядали скрипт-движок, як частина утиліт або частина двигуна. Так, озброївшись веб-сайтом, я вирішив зламати цей невеликий бар'єр. Результат описано у цьому документі.

Єдине, я не впевнений, наскільки більшим буде цей документ. Я можу розбити його на кілька невеликих частин або опублікувати цілком довгою тирадою від початку до кінця. Так чи інакше, я вирішу трохи пізніше, коли оформлю свої записи в більш осмислений і послідовний формат.

Чому й чому б ні?

Насамперед, навіщо використовувати скрипт-мову? Більшість ігрової логіки може бути описана на скрипт-мові для різних цілей, замість програмувати її як частина ігрового движка. Наприклад, завантаження або ініціалізації рівня. Після завантаження рівня, можливо, Ви захочете перевести сцену до ігрового плану або, можливо, захочете показати деякий попередній текст. Використовуючи скрипт-систему, Ви можете змусити деякі об'єкти гри виконувати певні завдання. Також, подумайте про реалізацію штучного інтелекту. Чи не Ігрові Персонажі повинні знати, що робити. Програмування кожного NPC «вручну», в тілі ігрового двигуна надмірно ускладнить завдання. Коли ви захочете змінити поведінку NPC, вам доведеться перекомпілювати ваш проект. З скрипт-системою, Ви можете робити це в інтерактивному режимі, змінюючи поведінку та зберігаючи налаштування.

Я трохи торкнувся цієї проблеми в останньому параграфі, ми ще поговоримо про це трохи пізніше. Питання, чому не написати логіку виключно на C/C++? Простіше кажучи, що в перспективі у програміста те, що все лягає безпосередньо на нього і почне він відповідно з ігрового коду, заразом доведеться писати і двигун і утиліти і т.д. Але ми тепер можемо з простою скриптом перекласти деякі завдання функціональних можливостей на дизайнерів рівнів. Вони можуть почати возитися з рівнем та оптимізувати геймплей. Ось власне приклад:

Давайте уявимо, що Джо, наш нещасний програміст, пише весь ігровий движок, інструменти та логіку гри сам. Так, Джо доведеться туго, але давайте припустимо, що йому все байдуже. У нас також є Брендон, ігровий дизайнер. Брендон досить розвинений хлопчина з розкішними ідеями щодо гри. І так, наш кодер Джо, повзає і здійснює всю ігрову логіку використовуючи інструментарій, який він розробив, ґрунтуючись на початковому проекті Брендона. Все добре у конторці. Перший етап закінчено і Джо з Брендоном сидять у залі засідань та перевіряють свої чималі праці. Брендон помічає кілька проблем у геймплей, який поводиться не належним чином. Отже, Джо повертається до коду і робить необхідні зміни. Цей процес може зайняти день, принаймні якщо це не тривіальна зміна. Потім ще день для перекомпілювання проекту. Щоб не втрачати зайву добу, більшість контор залишають процес складання на ніч. Так, як ми бачимо проходить 24 години, перш ніж Брендон побачить зміни, які він вимагав.

Тепер, уявимо, що наш головний герой Джо вирішив, що реалізація ігрової логіки використовує скрипт-движок у його інтересах. Це займе спочатку деякий час, але він відчуває, що в кінцевому рахунку це принесе користь. І так, він перекладає з ігрового двигуна деякі функціональні можливостіна скрипт-систему гри. Він також пише всю ігрову логіку у згаданій раніше скрипт-системі. І так, коли він зустрічається з Брендоном і дизайнер помічає дещо, що не відповідає його задуму, Джо швиденько відкриває консоль, робить деякі зміни у скрипті, перезапускає гру і вже бачить нову поведінку. Зміни можуть бути одразу внесені та показані негайно, замість того, щоб чекати на рекомпілінг. І якщо Джо був особливо виразним, скрипт-система могла бути використана для утиліт і доступна левел-дизайнерам при побудові рівнів. Якщо рухатися таким шляхом, то при невеликому навчанні проектувальники рівнів могли б самі встановлювати ігрові події, такі як тригери, двері, інші ігрові події та радіти життю не напружуючи програміста.

Це досить надуманий приклад і може бути трохи перебільшеним, але я сподіваюся, що він покаже різницю між підходами. Отже, спробуємо зробити з такою моделлю, що означає рух до більшої кількості автоматично керованих даних. Так, по суті, куди ми рухаємось:

  1. Кодер зацікавлений у написанні коду движка/інструментів, а не логіки гри.
  2. Час було витрачено на написання движка/інструментів гри.
  3. Дизайнерам подобається "балуватися" з речами. Скриптинг відкриває їм свободу у проектуванні рівнів та функціональних можливостей. Це також додає їм більше гнучкості, щоб експериментувати з речами, для яких вони зазвичай залучали програміста.
  4. Ви не повинні перекомпілювати, якщо хочете змінити функціональні можливості гри. Просто змініть сценарій.
  5. Ви хочете зруйнувати зв'язок між машинним та ігровим кодом. Вони мають бути двома окремими частинами. Таким чином, буде зручно використовувати двигун для наступних сіквелів (я сподіваюся).

Тут зроблю кілька прогнозів. Протягом 5 років, дизайнери рівнів повинні робити більше, ніж просто будувати рівні. Вони повинні бути здатними використовувати сценарій для ігрових сцен. Декілька компаній з передовими поглядами вже застосували цей підхід. Також, Ви можете побачити цей спосіб інтеграції в редакторах як UnrealEd і Aurora toolset Bioware.

Роз'яснення та розголошення

Сподіваюся, зараз Ви вже купилися на мої слова і захотіли включити скрипт-компонент у вашу гру. І так, наступне питання: як, чорт забирай, Ви це робите?

Що я збираюся використовувати для мого скрипт-компонента - це впроваджуваний скрипт-движок Lua. На початку скажу, що я не фахівець у Lua, але це відносно проста мова і не вимагатиме стомливого вивчення для оволодіння ним. Деякі наступні приклади, якими я пробігатимуся, досить прості. Наприкінці цього документа, я збираюся включити деякий додатковий довідковий матеріал. По справедливості, є інші скрипт-мови, типу Small, Simkin, Python, Perl. Однак Lua приємна і чиста мова. Це справді гарна перевага.

Lua має відкритий вихідний код. Це добре, тому що: (a) Ви отримуєте вихідні мови і можете ритися в них скільки заманеться, (b) він безкоштовний. Ви можете використовувати його в комерційних програмах, і не розкидатися грошима. Ну а для некомерційних проектів самі розумієте безкоштовно = добре.

Так, хто зараз використовує Lua? Lua написаний шарашкіною конторкою і його використовують лише бідні? Ммм... не зовсім так. Lua з'явився не вчора і використовувався досить відомими особистостями:

  • Lucasarts
    • Grim Fandango
    • Escape from Monkey Island
  • Bioware
    • Neverwinter Nights

Ок, досить з хто є хто з lua розробників. Ви можете побачити це самі на вебсайті lua.

Давайте почнемо з справді простого. Перша річ, яку ми повинні побудувати, покаже нам, як використовується lua інтерпретатор. Що для цього потрібно:

  1. Отримання коду інтерпретатора Lua.
  2. Налаштування середовища розробки.
  3. Складання інтерпретатора з нуля.

Гей, я подумав, Ви сказали достатньо розмов?

Ну, що, досить? Тож давайте перейдемо до справи. Ви можете отримати весь вихідний код Lua на офіційному сайті. Я також хотів би взяти секунду та звернути Вашу увагу, що на горизонті є Нова версія lua 5.0. Я не збираюся обговорювати цю версію у цій статті. Я розберуся з нею пізніше, а поки ми будемо використовувати 4.0.1.

Перша річ, яку ми зробимо – зберемо бібліотеку lua. Таким чином, нам не потрібно включати вихідні джерела щоразу при складанні проекту. Це не складно, і це не мета наших уроків. Тому я заздалегідь увімкнув бібліотеку як частину цієї статті. Я використав статичну бібліотеку для цього прикладу. Так, можливо, я зібрав би її як DLL, але для скрипт-системи статична бібліотека працює трохи швидше. Зауважте, не набагато, але швидше.

Наш сьогоднішній гість – справжній боєць прихованого фронту. Ви могли бачити його в іграх (World of Warcraft, Angry Birds, X-Plane, S.T.A.L.K.E.R.) або продуктах компанії Adobe (Lightroom), але навіть не замислювалися про його існування. Тим часом цій мові вже майже 25 років і весь цей час він непомітно робив наше віртуальне життя трохи кращим.

Коротка довідка

Lua був би придуманий у 1993 році в Католицькому університеті Ріо-де-Жанейро. Назва перекладається з португальської, як Місяць, причому творці переконливо просять не писати LUA, щоб, не дай Боже, хтось не прийняв назву за абревіатуру. Є мультипарадигмальним скриптовою мовою, що використовує прототипну модель ООП

Типізація тут динамічна, а для успадкування використовуються метатаблиці, тобто це чудовий інструмент для розширення можливостей вашого продукту. Причому через компактність він придатний для використання практично на будь-якій платформі. Посудіть самі: tarball Lua 5.3.4 важить всього 296 кілобайт (в розтисненому вигляді - 1.1 мегабайт), інтерпретатор (написаний на C) для Linux - від 182 до 246 кілобайт, а стандартний набір бібліотек - ще 421 кілобайт.

Код

за зовнішньому вигляду, Та й можливостям Lua схожий на чергову спробу переробити JavaScript, якби не той факт, що останній з'явився на два роки пізніше. Дивіться самі:

Почнемо з традиційного:

print("Hello World")

Погодьтеся, знайоме і не надто інформативно. Цікавіший приклад з погляду знайомства з Lua - обчислення факторіалу введеного числа:

Function fact (n)
if n == 0 then
return 1
else
return n * fact(n-1)
end
end

Print("enter a number:")
a = io.read("*number") - read a number
print(fact(a))

Все цілком зрозуміло. До речі, у Lua підтримується паралельне присвоєння:

І на завершення досить простий приклад з використанням бібліотек:

#include
#include
#include
#include
#include

Int main (void) (
char buff;
int error;
lua_State * L = lua_open(); /* opens Lua */
luaopen_base(L); /* opens the basic library */
luaopen_table(L); /* opens the table library */
luaopen_io(L); /* opens the I/O library */
luaopen_string(L); /* opens the string lib. */
luaopen_math(L); /* opens the math lib. */

While (fgets(buff, sizeof(buff), stdin) != NULL) (
error = luaL_loadbuffer(L, buff, strlen(buff), "line") ||
lua_pcall(L, 0, 0, 0);
if (error) (
fprintf(stderr, "%s", lua_tostring(L, -1));
lua_pop (L, 1); /* pop error message from the stack */
}
}

Lua_close(L);
return 0;
}

Переваги і недоліки

Отже, чим же гарний Lua?

По-перше, як вже було зазначено, своєю компактністю, а разом з тим, що вихідники написані на С, ви отримуєте повну взаємодію з однією з найпопулярніших мов на планеті та широкий спектр доступних платформ.

Середовища розробки

LDT (Lua Development Tools) для Eclipse – розширення для однієї з найбільш популярних IDE;

ZeroBrane Studio – спеціалізоване середовище, написане на Lua;

Decoda - не найпопулярніша кросплатформова IDE, але як альтернатива підійде;

SciTE - хороший редактор, що повноцінно підтримує Lua;

WoWUIDesigner - вгадайте, для якої гри це середовище допомагає обробляти скрипти, зокрема Lua?

Корисні посилання

http://www.lua.org/home.html - офіційний сайт з усією необхідною інформацією, підручником, книгами, документацією та навіть є трохи специфічного гумору;

http://tylerneylon.com/a/learn-lua/ - відмінне навчання від Tyler Neylon. Підійде програмістам із досвідом, хто добре знає англійська мова(Втім, зі словником теж не виникне великих проблем) і бажає розширити свій кругозір;

https://zserge.wordpress.com/2012/02/23/lua-за-60-хвилин/ - основи Lua за 60 хвилин від явно небайдужої до цієї мови програміста. Російською мовою;

http://lua-users.org/wiki/LuaTutorial - вікі-підручник;

https://youtube.com/watch?v=yI41OL0-DWM- відеоуроки на YouTube, які допоможуть вам наочно розібратися з налаштуванням IDEта базовими принципами мови.

Lua gives you the power; you build the mechanisms.
// Roberto Ierusalimsky


Вступ

Lua - мова програмування, призначена для вбудовування в інші програми, щоб дати користувачам можливість писати конфігураційні скрипти і високорівневі сценарії. Lua підтримує процедурний, об'єктний та функціональний стилі програмування, але є водночас простою мовою. Інтерпретатор Lua написаний на ANSI-C і є бібліотекою, яку можна підключити до будь-якої програми. У цьому випадку програма управління може викликати бібліотечні функції для виконання ділянки коду на Lua і роботи з даними, визначеними в цьому коді. Також програма управління може реєструвати власні функції таким чином, що їх можна буде викликати з коду на Lua . Остання можливість дозволяє використовувати Lua як мову, яку можна адаптувати до довільної сфери застосування. Інше застосування Lua – написання простих незалежних скриптів. Для цієї мети є простий інтерпретатор Lua, яка використовує цю бібліотеку для виконання коду, що вводиться з консолі або файлу.

Лексичні угоди

Ідентифікатори можуть містити літери, цифри та символи підкреслення та не можуть починатися з цифри.

Ідентифікатори, що починаються з підкреслення і складаються лише з великих букв, зарезервовані для внутрішнього використання інтерпретатором.

В ідентифікаторах відрізняється верхній та нижній регістри літер.

Рядкові літерали можна укладати в одинарні або подвійні лапки. Вони можна використовувати такі спеціальні послідовності символів:

\n переклад рядка (LF = 0x0a) \a bell \r повернення каретки (CR = 0x0d) \b backspace \t табуляція \f form feed \\ символ зворотної косої риси \v вериткальна табуляція \" лапка \[ ліва квадратна дужка \ " апостроф \] права квадратна дужка \ddd символ з кодом ddd (десятковим) \0 символ з кодом 0

Якщо наприкінці рядка вихідного файлу стоїть символ зворотної косої межі, то наступному рядку може бути продовжено визначення рядкового літералу, у якому у цьому місці вставиться символ нового рядка.

Рядкові літерали можна також укладати в подвійні квадратні дужки [[...]] . У цьому випадку літерал може бути визначений на кількох рядках (символи перекладу рядка включаються до рядкового літералу) і в ньому не інтерпретуються спеціальні послідовності символів.

Якщо безпосередньо після символів "[[" йде переклад рядка, він не включається в рядковий літерал.

В якості обмежувачів рядка крім подвійних квадратних дужок може використовуватися символ [===[ .... ]===] в якому між квадратними дужками, що повторюються, розташована довільна кількість знаків рівності (однакова для відкриває і закриває обмежувача).

У числових константах можна вказувати необов'язкову дробову частину та необов'язковий десятковий порядок, що задається символами "e" або "E". Цілочисельні числові константи можна задавати в 16-річній системі, використовуючи префікс 0x.

Коментар починається символами "--" (два мінуси поспіль) і продовжується до кінця рядка. Якщо безпосередньо після символів "--" йдуть символи "[[", то коментар є багаторядковим і продовжується до символів "]]". Багаторядковий коментар може містити вкладені пари символів [[...]] . В якості обмежувачів багаторядкових коментарів крім подвійних квадратних дужок може також використовуватися символ [===[ .... ]===] в якому між квадратними дужками, що повторюються, розташована довільна кількість знаків рівності (однакова для відкриває і закриває обмежувача). Рядкова константа екранує символи початку коментаря.

Якщо перший рядок файлу починається з символу "#", він пропускається. Це дозволяє використовувати Lua як інтерпретатор скриптів у Unix-подібних системах.

Типи даних

У Lua є такі типи даних:

Nil порожньо boolean логічний number числовий string рядковий функція функція userdata дані користувача thread потік table асоціативний масив

Тип nil відповідає відсутності змінної значення. Цьому типу відповідає єдине значення nil.

Логічний тип має два значення: true та false .

Значення nil розглядається як false. Всі інші значення, включаючи число 0 та порожній рядок, розглядаються як логічне значення true.

Усі числа представлені як речові числа подвійної точності.

Рядки є масивами 8-бітових символів і можуть містити в собі символ з нульовим кодом. Усі рядки в Lua константні тобто. змінити вміст існуючого рядка не можна.

Функції можна надавати змінним, передавати функції як аргумент, повертати як результат з функції і зберігати в таблицях.

Тип userdata відповідає нетипізованим покажчиком, за яким можуть бути розташовані довільні дані. Програма на Lua не може безпосередньо працювати з такими даними (створювати, модифікувати їх). Цьому типу даних відповідає ніяких зумовлених операцій крім присвоєння та порівняння на рівність. У той же час, такі операції можуть бути визначені за допомогою механізму метаметодів.

Тип thread відповідає незалежно виконуваному потоку. Цей тип даних використовується механізмом співпрограм.

Тип table відповідає таблицям - асоціативним масивам, які можна індексувати будь-якими значеннями і можуть одночасно містити значення довільних типів.

Тип об'єкта, збереженого в змінній, можна з'ясувати, викликавши функцію type() . Ця функція повертає рядок, що містить канонічну назву типу: "nil", "number", "string", "boolean", "table", "function", "thread", "userdata".

Перетворення між числами та рядками відбуваються автоматично в момент їх використання у відповідному контексті.

Арифметичні операції мають на увазі числові аргументи і спроба виконати таку операцію над рядками призведе до перетворення їх у числа. Рядкові операції, що проводяться над числами, призводять до їх перетворення в рядок за допомогою деякого фіксованого форматного перетворення.

Можна також явно перетворити об'єкт у рядок за допомогою функції tostring() або число за допомогою функції tonumber() . Для більшого контролю за процесом перетворення чисел у рядки слід використовувати функцію форматного перетворення.

Змінні

У Lua змінні не потрібно описувати. Змінна з'являється на момент її першого використання. Якщо використовується змінна, яка була попередньо ініціалізована, вона має значення nil . Змінні не мають статичного типутип змінної визначається її поточним значенням.

Змінна вважається глобальною, якщо вона явно не оголошена як локальна. Оголошення локальних змінних може бути розташоване в будь-якому місці блоку і може бути поєднане з їх ініціалізацією:

Local x, y, z local a, b, c = 1, 2, 3 local x = x

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

Для локальних змінних інтерпретатор використовує лексичні області видимості, тобто. область дії змінної тягнеться від місця її опису (першого використання) і до кінця поточного блоку. При цьому локальна змінна видима в блоках, внутрішніх стосовно блоку, в якому вона описана. Локальна змінна зникає при виході з області видимості. Якщо локальна змінна визначена поза блоком, така змінна зникає після виконання цієї ділянки коду, оскільки ділянка коду виконується інтерпретатором як безіменна функція. Ініціалізована глобальна змінна існує постійно функціонування інтерпретатора.

Для видалення змінної їй можна просто присвоїти значення nil.

Масиви, функції та userdata є об'єктами. Усі об'єкти анонімні і неможливо знайти значенням змінної.

Змінні зберігають посилання об'єкти. При присвоюванні, передачі в функцію як аргумент і повернення з функції як результат не відбувається копіювання об'єктів, копіюються лише посилання на них.

Таблиці

Таблиці (тип table) відповідає асоціативним масивам, які можна індексувати будь-якими значеннями крім nil та які можуть одночасно містити значення довільних типів крім nil . Елементи таблиці можна індексувати і об'єктами - таблицями, функціями та об'єктами типу userdata. Елементи масиву, що не набули значення в результаті присвоювання, мають за замовчуванням значення nil .

Таблиці - основна структура даних у Lua. З їхньою допомогою видаються також структури, класи та об'єкти. І тут використовується індексування рядковим ім'ям поля структури. Оскільки елементом масиву може бути функція, у структурах допускаються також методи.

Для індексування масивів використовуються квадратні дужки: array. Запис struct.field еквівалентний наступному запису: struct["field"] . Ця синтаксична особливість дозволяє використовувати таблиці як записи з поіменованими полями.

Конструктор таблиці - це вираз, що створює та повертає нову таблицю. Кожне виконання конструктора створює нову таблицю. Конструктор таблиці є укладеним у фігурні дужки список ініціалізаторів полів (можливо порожній), розділених комою або символом ";" (крапка з комою). Для ініціалізаторів полів допустимі такі варіанти:

Exp2 table = exp2 name = exp table["name"] = exp exp table[j] = exp

У разі змінна j пробігає послідовні цілі значення, починаючи з 1 . Ініціалізатори перших двох видів не змінюють значення цього лічильника. Ось приклад конструювання таблиці:

X = (len = 12, 11, 12, = 1123)

Після виконання такого оператора поля таблиці отримають наступні значення:

X["len"] = x.len = 12 x = 11 x = 12 x = 1123

Якщо останнім елементом списку ініціалізаторів є виклик функції, значення, що повертаються функцією, послідовно поміщаються в список ініціалізаторів. Цю поведінку можна змінити, уклавши виклик функції у круглі дужки. У цьому випадку з усіх значень, що повертаються функцією, використовується тільки перше.

Після останнього ініціалізатора може йти необов'язковий символ-розділювач ініціалізаторів полів (кома або крапка з комою).

Операції

Нижче наведено основні операції:

Зміна знака + - * / арифметика ^ зведення в ступінь == ~= рівність< <= >>= порядок not and or логіка.. конкатенація рядків # отримання довжини рядка чи масиву

При застосуванні арифметичних операцій рядки, що мають числове значення, наводяться до нього. При конкатенації числових значень вони автоматично перетворюються на рядки.

При порівнянні рівність немає перетворення типів. Об'єкти різних типівзавжди вважаються різними.

Відповідно "0" ~= 0, а при індексуванні a і a["0"] відповідають різним осередкам масиву. При порівнянні на рівність/нерівність об'єктів проводиться порівняння посилань на об'єкти. Рівними виявляються змінні, що посилаються на той самий об'єкт.

При з'ясуванні порядку типи аргументів мають збігатися, тобто. числа порівнюються з числами, а рядки – з рядками.

Відносини рівності та порядку завжди дають у результаті true чи false тобто. логічне значення.

У логічних операціях nil розглядається як false , проте інші значення, включаючи нульове число і порожній рядок - як true .

При обчисленні значення використовується коротка схема – другий аргумент обчислюється лише якщо це необхідно.

Діє наступна таблиця пріоритетів та асоціативності операцій:

^ not # - (unary) * / + -< > <= >= ~= == .. and or

Логічні операції та пов'язані з ними ідіоми

Оператор not завжди повертає логічне значення, приймаючи аргумент довільного типу (при цьому лише значення nil відповідає логічному значенню false, інші ж трактуються як true). На відміну від нього оператори and та or завжди повертають один із своїх аргументів. Оператор or повертає свій перший аргумент, якщо його значення відмінне від false і nil і другий аргумент в іншому випадку. Оператор and повертає свій перший аргумент, якщо його значення дорівнює false або nil і другий аргумент в іншому випадку. Така поведінка заснована на тому, що всі значення, відмінні від nil, трактуються як true.

З цією поведінкою пов'язано кілька загальновживаних ідіом. У наступній таблиці зліва наведена ідіоматична операція, а праворуч - еквівалентний звичайний запис:

X = x or v if x == nil then x = v end x = (e and a) or b if e ~= nil then x = a else x = b end

Перша ідіома часто використовується для присвоєння неініціалізованої змінної значення, що замовчується. Друга ідіома еквівалентна C"шному оператору x = e? a, b (тут вважається, що значення змінної a відмінно від nil).

Оператори

У Lua немає виділеної функції, з якої починається виконання програми. Інтерпретатор послідовно виконує оператори, які він отримує з файлу або керуючої програми. При цьому він попередньо компілює програму в двійкову виставу, яка також може бути збережена. Будь-який блок коду виконується як анонімна функція, тому в ньому можна визначати локальні змінні та з нього можна повертати значення.

Оператори можна (але не обов'язково) розділяти символом ";" .

Допускається множинне присвоєння:

Var1, var2 = val1, val2

При цьому проводиться вирівнювання - зайві значення відкидаються, а змінним, відповідним недостатнім значенням присвоюється значення nil. Усі висловлювання, які входять у праву частину множинного присвоєння, обчислюються аж до присвоєння.

Якщо функція повертає кілька значень, то за допомогою множинного присвоювання можна отримати значення, що повертаються:

X, y, z = f(); a, b, c, d = 5, f ();

У випадку якщо в кінці списку значень, розташованого праворуч від знака присвоювання, знаходиться виклик функції, то всі значення, що повертаються функцією, дописуються в кінець списку значень. Цю поведінку можна змінити, уклавши виклик функції у круглі дужки. У цьому випадку з усіх значень, що повертаються функцією, використовується тільки перше.

Нижче наведено основні оператори:

Do ... end if ... then ... end if ... then ... else ... end if ... then ... elseif ... then ... end if ... then . .. elseif ... then ... else ... end while ... do ... end repeat ... until ... for var = start, stop do ... end for var = start, stop, step do... end return return... break

Блок do ... end перетворює послідовність операторів на один оператор і відкриває нову область видимості, у якій можна визначати локальні змінні.

У операторах if , while і repeat всі значення висловлювання, відмінні від false і nil трактуються як істинні.

Ось загальна форма запису оператора if:

If...then... (elseif...then...) end

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

Оператори return і break повинні бути останніми операторами в блоці (тобто повинні бути або останніми операторами в блоці коду, або розташовуватися безпосередньо перед словами end, else, elseif, until). Всередині блоку необхідно використовувати ідіому do return end або do break end.

Оператор for виконується всім значень змінної циклу починаючи від стартового значення і закінчуючи фінішним значенням включно. Третє значення, якщо воно встановлено, використовується як крок зміни змінної циклу. Всі ці значення мають бути числовими. Вони обчислюються лише один раз перед виконанням циклу. Змінна циклу локальна в цьому циклі і не доступна поза його тілом. Значення змінної циклу не можна змінювати всередині тіла циклу. Ось псевдокод, що демонструє виконання оператора for:

Do local var, _limit, _step = tonumber(start), tonumber(stop), tonumber(step), якщо немає (var and _limit and _step), error () end while (_step>0 and var<=_limit) or (_step<=0 and var>=_limit) do ... var = var + _step end end

Функції

Визначення функції - це вираз (конструктор функції), результатом обчислення якого є об'єкт типу функція:

F = function(...) ... end

У дужках розміщується список (можливо порожній) аргументів функції. Список аргументів функції може закінчуватися трикрапкою - у разі функція має змінне число аргументів. Між дужкою, що закривається, і оператором end розміщується тіло функції.

Для визначення функції є такі короткі форми:

Function fname(...) ... end fname = function(...) ... end local function fname(...) ... end local fname = function(...) ... end function x .fname(...) ... end x.fname = function(...) ... end function x:fname(...) ... end x.fname = function(self, ...) ... end

У момент виконання конструктора функції будується також замикання- таблиця всіх доступних у функції та зовнішніх по відношенню до неї локальних змінних. Якщо функція передається як значення, що повертається, то вона зберігає доступ до всіх змінних, що входять до її замикання. При кожному виконанні конструктора функції будується нове замикання.

Виклик функції складається з посилання на функцію та укладеного у круглі дужки списку аргументів (можливо порожнього). Посиланням на функцію може бути будь-який вираз, результатом обчислення якого є функція. Між посиланням на функцію і круглою дужкою, що відкривається, не може бути перекладу рядка.

Для виклику функцій є такі короткі форми:

F(...) f((...)) f("...") f"..." f("") f"" f([[...]]) f[[. ..]] x:f(...) x.f(x, ...)

У першому випадку єдиним аргументом є таблиця, що конструюється на льоту, а в наступних трьох - рядковий літерал. В останньому випадку x використовується як таблиця, з якої вилучається змінна f , що є посиланням на функцію, що викликається, і ця ж таблиця передається в функцію в якості першого аргументу. Ця синтаксична особливість використовується реалізації об'єктів.

При виклику функції простих типів копіюються в аргументи за значенням, а для об'єктів в аргументи копіюються посилання. При виклику функції проводиться вирівнювання числа аргументів - зайві значення відкидаються, а аргументи, що відповідають значенням, що відсутнім, отримують значення nil . Якщо наприкінці списку параметрів стоїть виклик функції, яка повертає кілька значень, всі вони додаються до списку аргументів. Цю поведінку можна змінити, уклавши виклик функції у круглі дужки. У цьому випадку з усіх значень, що повертаються функцією, використовується тільки перше.

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

Якщо функція приймає єдиний аргумент та трактує його як таблицю, елементи якої індексуються іменами формальних параметрів функції, то в цьому випадку фактично реалізується виклик механізму пойменованих аргументів:

Function rename(arg) arg.new = arg.new або arg.old .. ".bak" return os.rename(arg.old, arg.new) end rename( old = "asd.qwe" )

Повернення з функції відбувається при завершенні виконання її тіла, і під час виконання оператора return . Оператор return може повертати одне чи кілька значень. Якщо в кінці списку значень, що повертаються, стоїть виклик функції, що повертає кілька значень, то всі вони додаються до списку аргументів. Цю поведінку можна змінити, уклавши виклик функції у круглі дужки. У цьому випадку з усіх значень, що повертаються функцією, використовується тільки перше.

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

Є певна функція unpack() , яка приймає масив, елементи якого проіндексовані з 1 , а повертає всі його елементи. Ця функція може бути використана для виклику функцій, що приймають змінну кількість аргументів із динамічним формуванням списку фактичних аргументів. Зворотна операція проводиться так:

List1 = (f()) -- створити список із усіх значень, повернутих функцією f() list2 = (...) -- створити список із усіх значень, переданих у функцію зі змінною кількістю аргументів

Змінна кількість значень, що повертаються, правило доповнення списків і можливість передавати в функцію не всі формальні параметри можуть взаємодіяти нетривіальним чином. Часто функція (наприклад foo()) повертає відповідь при нормальному завершенні та nil та повідомлення про помилку при ненормальному. Функція assert(val, msg) генерує помилку з повідомленням message (викликаючи функцію error(msg)), якщо val має значення false або nil і повертає значення val в іншому випадку. Тоді оператор

V = assert(foo(), "message")

у разі успіху надає змінної v значення, що повертається функцією foo(). У цьому випадку foo() повертає одне значення, а assert() отримує параметр msg , що дорівнює nil . У разі помилки функція assert() отримує nil та повідомлення про помилку.

Ітератори

Ітератори використовуються для перерахування елементів довільних послідовностей:

For v_1, v_2, ..., v_n у explist do ... end

Число змінних у списку v_1, ..., v_n може бути довільним і не повинно відповідати числу виразів у списку explist. У ролі explist зазвичай виступає виклик функції-фабрики ітераторів. Така функція повертає функцію-ітератор, стан та початкове значення керуючої змінної циклу. Ітератор інтерпретується так:

Do local f, s, v_1 ​​= explist local v_2, ... , v_n while true do v_1, ..., v_n = f(s, v_1) if v_1 == nil then break end ... end end

На кожному кроці значення всіх змінних v_k обчислюються за допомогою функції-ітератора. Значення керуючої змінної v_1 керує завершенням циклу - цикл завершується, як тільки функція-ітератор поверне nil як значення для змінної var_1 .

Фактично ітераціями управляє змінна v_1, а інші змінні можна розглядати як «хвіст», що повертається функцією-ітератором:

Do local f, s, v = explist while true do v = f(s, v) if v == nil the break end ... end end

Оператор, у якому викликається функція-фабрика, інтерпретується як стандартний оператор присвоєння тобто. функція-фабрика може повертати довільну кількість значень.

Ітератори без внутрішнього стану

Ітератор без внутрішнього стану не зберігає жодної внутрішньої інформації, що дозволяє йому визначити своє положення в контейнері, що ітерується. Наступне значення керуючої змінної обчислюється безпосередньо за її попереднім значенням, а стан використовується для зберігання посилання на контейнер, що ітерується. Ось приклад простого ітератора без внутрішнього стану:

Function iter(a, i) i = i + 1 local v = a[i] if v the return i, v else return nil end end функція ipairs(a) return iter, a, 0 end

Ітератори, які зберігають стан у замиканні

Якщо ітератор для обходу контейнера необхідний внутрішній стан, то найпростіше зберігати його в замиканні, створюваному за контекстом функції-фабрики. Ось простий приклад:

Function ipairs(a) local i = 0 local t = a local function iter() i = i + 1 local v = t[i] if v the return i, v else return nil end end return iter end

Тут ітератор зберігає весь контекст у замиканні і не потребує стану та поточного значення керуючої змінної. Відповідно, ітератор не приймає стан і змінну, що управляє, а фабрика не повертає значення стану і стартове значення управляючої змінної.

Стандартні ітератори

Найчастіше ітератори використовуються для обходу елементів таблиць. І тому існує кілька зумовлених функцій-фабрик ітераторів. Фабрика pairs(t) повертає ітератор, що дає на кожному кроці індекс у таблиці та розміщене за цим індексом значення:

Для idx, val in pairs(tbl) do ... end

Насправді, цей ітератор легко визначити, використовуючи стандартну функцію next(tbl, idx) :

Function pairs(tbl) return next, tbl, nil end

Функція next(tbl, idx) повертає наступне значення idx індексу при деякому обході таблиці tbl (виклик next(tbl, nil) повертає початкове значення індексу; після вичерпання елементів таблиці повертається nil).

p align="justify"> Фабрика ipairs(tbl) повертає ітератор, що працює абсолютно аналогічно описаному вище, але призначений для обходу таблиць, проіндексовані цілими числами починаючи з 1 .

Мета-таблиці

Кожна таблиця та об'єкт типу userdata можуть мати мета-таблицю - звичайну таблицю, поля якої визначають поведінку вихідного об'єкта при застосуванні до нього деяких спеціальних операцій. Наприклад, коли об'єкт виявляється операндом при додаванні, інтерпретатор шукає в мета-таблиці поле з ім'ям __add і, якщо таке поле є, то використовує його значення як функцію, що виконує додавання. Мета-таблиці дозволяють визначити поведінку об'єкта при арифметичних операціях, порівняннях, конкатенації та індексування. Також можна визначити функцію, що викликається під час звільнення об'єкта типу userdata . Індекси (імена полів) у мета-таблиці називаються подіями, а відповідні значення (обробники подій) - метаметодами.

За замовчуванням новостворена таблиця не має мета-таблиці. Будь-яку таблицю mt можна зробити мета-таблицею таблиці t, викликавши функцію setmetatable(t, mt). Функція getmetatable(t) повертає мета-таблицю таблиці t або nil якщо таблиця не має мета-таблиці. Будь-яка таблиця може виконувати роль мета-таблиці для будь-якої іншої таблиці, у тому числі і для себе.

Lua визначає такі події:

Add, __sub, __mul, __div арифметичні операції __pow зведення в ступінь __unm унарний мінус __concat конкатенація __eq, __lt, __le операції порівняння __index доступ за відсутнім індексом __newindex присвоєння новому елементу таблиці __call виклик функції __tostring

Вираз a ~= b обчислюється як not (a == b). Вираз a > b обчислюється як b< a . Выражение a >= b обчислюється як b<= a . При отсутствии метаметода __le операция <= вычисляется как not (b < a) т.е. с помощью метаметода __lt .

Для бінарних операцій вибір оброблювача проводиться так: опитується перший операнд і, якщо він не визначає обробник, то опитується другий операнд. Для операцій порівняння метаметод вибирається тільки якщо порівнювані операнди мають однаковий тип і однаковий метаметод виконання цієї операції. У посібнику користувача наведено псевдокод на Lua, що демонструє контекст виклику метаметодів.

Обробник події __index може бути функцією чи таблицею. У разі функції обробник викликається і йому передається таблиця та значення індексу. Така функція має повертати результат індексування. Що стосується таблиці відбувається повторне індексування цієї таблиці тим самим індексом. Якщо є обробник події __newindex , він викликається замість присвоєння значення новому елементу таблиці. Якщо цей обробник є таблицею, то присвоєння провадиться у цій таблиці.

Метаметод __tostring дозволяє обробити перетворення об'єкта (таблиці або userdata) в рядок. Метаметод __metatable дозволяє обробити операцію отримання мета-таблиці. Якщо це поле в мета-таблиці встановлено значення, то функція getmetatable() повертатиме значення цього поля, а функція setmetatable() буде завершуватися помилкою.

Приклади використання мета-таблиць

Значення за промовчанням для полів таблиць

У наступному прикладі мета-таблиці використовуються для призначення значення за промовчанням для відсутніх елементів таблиці:

Function set_def(t, v) local mt = ( __index = function() return v end ) setmetatable(t, mt) end

Ось більш нетривіальне рішення, яке не використовує окремої мета-таблиці для кожного значення, що замовчується:

Local key = () local mt = (__index = function(t) return t end ) function set_def(t, v) t = v setmetatable(t, mt) end

Тут локальна (порожня) таблиця key використовується як наперед унікальний індекс, за яким у вихідній таблиці t зберігається значення замовчування v . Всі таблиці, для яких встановлюється значення відсутніх полів, поділяють загальну мета-таблицю mt . Мета-метод __index цієї мета-таблиці перехоплює звернення до відсутніх полів таблиці і повертає значення, збережене в таблиці за індексом key .

У цього рішення є недолік: у таблиці з'являється нова пара ключ-значення, яка виявиться при спробі обійти всі елементи таблиці.

Таблиця-проксі

У наступному прикладі порожня таблиця виконує роль проксі, що переадресує звернення до полів таблиці:

Local key = () local mt = ( __index = function(t,k) return t[k]end, __newindex = function(t,k,v) t[k] = v end ) function proxy(t) local proxy = () proxy = t setmetatable(proxy, mt) return proxy end

Тут локальна (порожня) таблиця key використовується як унікальний індекс, за яким в таблиці-проксі зберігається посилання на вихідну таблицю t . Проксі є таблицею, єдиний елемент якої має індекс key , тому звернення до будь-якого елемента проксі призведе до виклику мета-метода. Загальна всім проксі мета-таблиця визначає мета-методи __index і __newindex , які отримують вихідну таблицю з єдиного елемента проксі, індексуючи його таблицею key .

Мета-методи можуть забезпечити довільну дисципліну обробки звернень до полів вихідної таблиці. Простими прикладами є протоколювання звернень чи генерування помилки під час спроби змінити значення елемента таблиці.

«Слабкі» таблиці

Якщо об'єкт був використаний як індекс таблиці або посилання на нього було збережено в таблиці, збирач сміття не зможе утилізувати такий об'єкт. У той самий час, деяких випадках бажано мати таблицю, у якій зв'язок її елементів із ключами і/або значеннями «слабка» тобто. не перешкоджає збору сміття. Зазвичай така необхідність виникає при кешуванні результатів обчислень у таблиці та збереженні атрибутів об'єктів у таблиці, що проіндексована самими об'єктами. У першому випадку бажаний слабкий зв'язок для значень, у другому - для індексів.

Зв'язок елементів таблиці з об'єктами (значеннями та індексами) визначається значенням рядкового поля __mode її мета-таблиці. Якщо це поле містить символ "k", то слабким стає зв'язок для індексів (ключів); якщо воно містить символ "v", то слабким стає зв'язок для значень. Поле може містити обидва символи, що зробить слабкою зв'язок як індексів, так значень.

Якщо використовувати слабкі таблиці, то для розглянутої вище завдання зіставлення таблиці значення, що замовчується, для відсутніх полів можна навести наступне рішення:

Local defaults = () setmetatable(defaults, ( __mode = "k" )) local mt = ( __index = function(t) return defaults[t] end ) function set_def(t, d) defaults[t] = d setmetatable(t mt) end

Ось інше рішення, в якому слабка таблиці зберігає мета-таблиці, кількість яких збігається з числом різних значень, що замовчуються:

Local metas = () setmetatable(metas, ( __mode = "v" )) функція set_def(t, d) local mt = metas[d] if mt == nil then mt = [d] = mt end setmetatable(t, mt) end

Глобальний контекст

Усі глобальні змінні є полями звичайної таблиці, яка називається глобальним контекстом. Ця таблиця доступна через глобальну змінну _G. Оскільки всі глобальні змінні є полями контексту, _G._G == _G .

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

Val = _G _G = val

Оскільки глобальний контекст є звичайною таблицею, може відповідати мета-таблиця. У наступному прикладі змінні оточення вводяться у глобальну область видимості як глобальні змінні, доступні лише читання:

Local f = функція (t,i) return os.getenv(i) end setmetatable(_G, (__index=f))

Той самий прийом дозволяє заборонити доступ до неініціалізованих глобальних змінних.

Пакети

Пакети - основний спосіб визначати набір взаємозалежних функцій, не забруднюючи у своїй глобальну область видимості. Зазвичай пакет є окремим файлом, який у глобальній області видимості визначає єдину таблицю, що містить всі функції цього пакета:

My_package = () function my_package.foo() ... end

Також можна виконувати всі функції локальними і окремо формувати таблицю функцій, що експортуються:

Local function foo() ... end local function bar() ... end my_package = ( foo = foo, bar = bar, )

Пакет завантажується за допомогою функції require() , причому під час завантаження ім'я, передане цій функції (воно може не містити розширення, яке додається автоматично) доступне через змінну _REQUIREDNAME:

If _REQUIREDNAME == nil then run_some_internal_tests() end

Класи та об'єкти

Конструкція tbl:func() (при оголошенні функції та її виклику) надає основні можливості, дозволяють працювати з таблицею як із об'єктом. Основна проблема полягає у породженні багатьох об'єктів, що мають подібну поведінку, тобто. породжених від одного класу:

Function class() cl = () cl.__index = cl -- cl буде використовуватися як мета-таблиця return cl end function object(cl, obj) obj = obj or () -- можливо вже є заповнені поля setmetatable(obj, cl ) return obj end

Тут функція class створює порожню таблицю, підготовлену для того, щоб стати мета-таблицею об'єкта. Методи класу робляться полями цієї таблиці, тобто. клас є таблицею, що одночасно містить методи об'єкта та його мета-методи. Функція object() створює об'єкт заданого класу - таблицю, у якій як мета-таблиці встановлено заданий клас. У другому аргументі може бути передана таблиця, що містить проініціалізовані поля об'єкта.

Some_Class = class() function Some_Class:foo() ... end function Some_Class:new() return object(self, ( xxx = 12 )) end x = Some_Class:new() x:foo()

успадкування

В описаній реалізації мета-таблиця класу залишається не використаною, що робить просте завдання реалізації наслідування. Клас-спадкоємець створюється як об'єкт класу, після чого в ньому встановлює поле __index таким чином, щоб його можна було використовувати як мета-таблицю:

Function subclass(pcl) cl = pcl:new() -- створюємо екземпляр cl.__index = cl -- і робимо його класом return cl end

Тепер у отриманий клас-спадкоємець можна додати нові поля та методи:

Der_Class = subclass(Some_Class) function Der_Class:new() local obj = object(self, Some_Class:new()) obj.yyy = 13 -- додаємо нові поля return obj end function Der_Class:bar() ... end -- і нові методи y = Der_Class:new() y:foo() y:bar()

Єдиний нетривіальний момент тут полягає у використанні функції new() класу-предка з наступною заміною мета-таблиці за допомогою виклику функції object() .

При зверненні до методів об'єкта класу-спадкоємця насамперед відбувається їх пошук у мета-таблиці, тобто. у самому класі-спадкоємці. Якщо метод був успадкований, цей пошук виявиться невдалим і станеться звернення до мета-таблиці класу-спадкоємця тобто. до класу-предка.

Основний недолік наведеного загального рішення полягає у неможливості передачі параметрів у функцію new() класу-предка.

Аргументи командного рядка

Передані під час запуску аргументи командного рядка доступні як елементи масиву arg.