Перейти к содержанию

Синтаксис Lua

Для понимания работы скриптов в Worms крайне желательно иметь некоторые сведения о программировании, хотя реально и так понять. Расскажем, как пишутся скрипты (точнее как их правильно писать).

Программа - набор команд. Команды должны быть понятны для исполнителя (у нас это интерпретатор). Наш интерепретатор понимает только команды, записанные определённым образом.

Каждая команда записывается на отдельной строке (хотя вроде можно и на разных, но удобнее так).

Команды выполняются последовательно одна за другой. Порядок исполнения можно менять. Далее приведены стандартные команды (операторы):

Примечание: далее по тексту есть примеры с использованием псевдокода-под этим подразумевается, что он не должен работать в червях. Это просто абстрактный код. К примеру message(a) - некоторая функция, которая каким либо образом выводит свой параметр пользователю, не стоит воспринимать это так, как будто message есть в червях!

Комментарии

Комментарии - один из самых полезных “инструментов” для программиста. Комментарии - это простой текст, который интерпретатор полностью игнорирует, но человек по ним может быстрее понять что делает программа.

В Lua комментарии записываются двумя способами

Однострочный комментарий

После двойного знака "минус" в конце строки, например:

message(3) --пример однострочного комментария.
           --текст после двух минусов игнорируется

Многострочный комментарий

После двойного знака "минус" и двух открывающих квадратных скобок, до двух закрывающих двойных скобок, например:

message(3)  --[[
                 пример многострочного комментария.
                 текст можно записывать в несколько строк,
                 и всё что внутри будет проигнорировано,
                 даже если написать какие-либо операторы
                 чтобы окончить комментарий надо поставить две скобки(см следующую строку)
              ]]

Операторы присваивания, типы данных и массивы

Переменная - особая ячейка в памяти, где могут храниться данные. Например, мы можем хранить числа, текст(строки), массивы (или таблицы, как это принято называть в lua). Ячейки памяти (переменные) имеют имена, чтобы их можно было однозначно определить. Переменные можно использовать в различных выражениях - обычных математических(+ - * /), в функциях и т.д.

Данные, хранимые в переменных могут иметь следующие типы:

Число

Запись чисел обычная (для обозначения дробных чисел используется символ .):

number1 = 12412.124
number2 = 7

Текст

Для записи текста (в программировании текст чаще называется строкой текста или просто строкой) используются кавычки, например так:

text = "Привет!"

Соединение двух строк в одну делается с помощью оператора две точки (псевдокод):

text1 = "Привет,"
text2 = " мир!"
text = text1 .. text2
message(text)

Данный код выведет на экран надпись Привет, мир!

Булево значение (логическое, или истина или ложь)

Особый тип данных, у которого всего 2 значения - ложь (false) или правда (true). Любая операция, в которой производится проверка значения (например сравнение) возвращает именно булево значение.

Примеры (псевдокод):

a = 2
b = 4

                                  -- Сравнения
message(a == b)                   --  равенство, выведет false
message(a ~= b)                   --  неравенство, выведет true
message(a > b)                    --  больше, выведет false
message(a < b)                    --  меньше, выведет true
message(a >= b)                   --  больше или равно, выведет false
message(a <= b)                   --  меньше или равно, выведет true

                                  -- Логическое отрицание
message(not true)                 --  выведет false
message(not false)                --  выведет true

                                  -- Логическое И
message(true and true)            --  выведет true
message(false and true)           --  выведет false
message(true and false)           --  выведет false
message(false and false)          --  выведет false

                                  -- Логическое ИЛИ
message(true or true)             --  выведет true
message(false or true)            --  выведет true
message(true or false)            --  выведет true
message(false or false)           --  выведет false

                                  -- Сравнения и логические операторы можно комбинировать
message((a > b) or (a + 2) == b)  --  выведет true, потому что хотя бы одно из условий выполнено
message((a > b) and (a + 2) == b) --  выведет false, потому что одно из условий не выполнено

Как видно из примера, к булевым значениям можно применять логические операторы and, or и not. Они позволяют составлять сложные условия. Запомните, как они работают по этому примеру.

Таблица

Таблицу можно описать в виде

sometable = { [индекс1] = значение1; [индекс2] = значение2; [индекс3] = значение3 <...> }

при этом индексы можно не указывать, тогда им будут даваться числовые индексы по порядку. К тому же запись вида

sometable = { ["name1"] = значение1; ["name2"] = значение2; ["name3"] = значение3 <...> }

эквивалентна следующей:

sometable = { name1 = значение1; name2 = значение2; name3 = значение3 <...> }

Функция

Это можно объяснить так - в переменной можно кроме реальных данных хранить и какое то действие, то есть когда необходимо совершить то действие, которое описано в ней. Для полноты приведём пример (псевдокод):

local somefunc
somefunc = function(a, b)
  return math.sin(a + b)   -- не пугайтесь, так записывается стандартная функция sin - синус
end

message(somefunc(1, 2))

Код примера выведет значение синуса 1 + 2. Обратите внимание, что somefunc - это переменная, а не функция. Сама функция является безымянной.

Переменные могут иметь разную область видимости-глобальные и локальные. Локальные действуют только в пределах определённого логического куска кода, например внутри функции, цикла и т.д. Глобальные действуют на самом высоком уровне. Если мы хотим воспользоваться(получить или присвоить) значение переменной с именем foo, то сначала переменная с именем foo ищется среди локальных, а потом среди глобальных.

Создание глобальной переменной, доступной отовсюду. В случае, если переменная уже существует(глобальная или локальная), то происходит изменение значения переменной

<имя переменной> = <значение>

Создание переменной с ограниченной областью видимости (к примеру только в функции)

local <имя переменной> = <значение>

Примеры (псевдокод):

a = 12
b = 17
c = a + b
message(c)

При выполнении сначала переменная a примет значение 12, и b значение 17, а зачем c примет значение a+b, то есть 12+17, поэтому message(c) выведет 29

d = 10
g = 15
message(d)
message(g)

function a()
 local d = 5
 g = 20
 message(d)
 message(g)

end
a()
message(d)
message(g)

При выполнении выведется сначала 10 и 15, потом 5 и 20, а потом 10 и 20, что говорит о том, что локальная и глобальная переменная не одно и то же. Локальная переменная пропадает, когда заканчивается работа функции. Есть особый вид присваивания переменных - параллельное присваивание. Оно позволяет в одной строке присвоить значения сразу нескольким переменным, к примеру можно поменять местами значения двух переменных (псевдокод):

x = 10
y = 20

message(x, y)   -- выведет 10, а затем 20

x, y = y, x     -- параллельное присваивание

message(x, y)   -- выведет 20, а затем 1

Этот код аналогичен следующему:

x = 10
y = 20

message(x,y)   -- выведет 10, а затем 20

t = x          -- приходится использовать дополнительную переменную
x = y
y = t

message(x,y)   -- выведет 20, а затем 1

Существует особый тип переменных - массивы или таблицы. Таблица - это набор из элементов с общим именем. Чтобы их отличать используется индекс. Таблицу в lua можно представить в виде таблицы с двумя столбцами. Пусть у нас есть таблица с именем foo

Индекс Значение Как обращаться
1 15 foo[1]
2 55 foo[2]

Создание переменной-массива, делается как и в случае обычной переменной, однако в конце надо ставить ={} что означает создание пустого массива. Его наполнение производится после этого.

Для создания массива foo, приведённого в таблице выше, можно написать следующее.

local foo = {}
foo[1] = 15
foo[2] = 55

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

local foo = {[1] = 15; [2] = 55 }

Для того, чтобы получить значение какого-либо элемента можно написать так (псевдокод):

message(foo[1])

Иногда бывает нужно узнать длину массива, для этого надо написать # перед именем массива, например:

local foo = {}
foo[1] = 15
foo[2] = 55
message(#foo) -- выведет

Чудесная возможность, которую даёт lua - это создавать так называемые ассоциативные массивы, то есть такие, где в качестве индекса выступает текстовая строка (на самом деле в качестве индекса может быть что угодно, даже другой массив, но это редко применяется). Рассмотрим пример массива car.

Индекс Значение Как обращаться
"model" "BMW" car["model"] или car.model
"speed" 350 car["speed"] или car.speed
"price" 1000000000 car["price"] или car.price

Для создания такого массива можно написать следующее:

car = {}
car["model"] = "BMW"
car["speed"] = 350
car["price"] = 1000000000

или так:

car = {}
car.model="BMW"
car.speed=350
car.price=1000000000

или даже так:

car = { model = "BMW"; speed = 350; price = 1000000000 }

Как видно, третья запись намного удобнее. При этом на массив можно смотреть, как на некоторое описание реального объекта, в нашем случае-машины с маркой BMW и большущей ценой. Для того, чтобы получить значение какого-либо элемента, например скорости можно написать так (псевдокод):

message(car["speed"])

ну и намного проще так:

message(car.speed)

Условные операторы

Эти операторы позволяют исполнять часть команд по условию

if <условие> then
  <последовательность команд, которая будет выполнена при выполнении условия>
end
if <условие> then
  <последовательность команд, которая будет выполнена при выполнении условия>
else
  <последовательность команд, которая будет выполнена, если условие не выполнено>
end
if <условие 1> then
  <последовательность команд, которая будет выполнена при выполнении условия 1>
elseif <условие 2> then
  <последовательность команд, которая будет выполнена, если прошлые условия не выполнены, и  при выполнении условия 2>
  ...
elseif <условие n> then
  <последовательность команд, которая будет выполнена, если прошлые условия не выполнены, при выполнении условия n>
else
  <последовательность команд, которая будет выполнена, если ни одно условие не выполнено>
end

Пример (псевдокод):

number = InputNumber()
if number > 10 then
  message("Число больше 10")
elseif number == 3 then
  message("Число равно 3")
else
  message("Число меньше 11 и не равно 3")
end

Операторы цикла

Цикл - цепочка действий, которая может повторяться несколько раз. Житейский пример: вам сказали подпрыгнуть 10 раз - это цикл, в котором повторяется одно и тоже действие - прыжок - ровно 10 раз.

В языках программирования аналогично можно повторять одно и тоже действие. В качестве оператора цикла в Lua используются операторы for, while и repeat.

Цикл for-do-end позволяет повторять действие определённое количество раз, он называется цикл со счётчиком (то есть в нём задана переменная-счётчик, которая хранит номер повтора).

Цикл for устроен просто:

for <переменная-счётчик> = <начало цикла>, <конец цикла>, <шаг> do
  <цепочка действий, повторяемых для всех значений переменной-счётчика из указанного диапазона>
end

Цепочка действий, выполняемая циклом также называется телом цикла. Например (псевдокод):

for i = 1, 10 do
  message(i)      -- Выведет числа 1, 2, 3, 4, 5, 6, 7, 8, 9, 10         
end

В этом примере показана укороченная версия цикла (не пишется шаг - он необязателен). При этом значение переменной растёт с каждым шагом ровно на 1.

Можно задать шаг, как в следующем примере(псевдокод):

for i = 1, 10, 2 do
  message(i)         -- Выведет все нечётные числа 1, 3, 5, 7, 9
                     -- 11 выведено не будет, так как не попадает в заданные пределы         
end

Существует другая разновидность циклов - циклы с условием (repeat–until и while–do-end). Данный вид цикла может повторяться пока будет выполняться или не выполняться какое-либо условие.

Цикл repeat–until устроен следующим образом:

repeat
  <цепочка действий, которая будет выполнена хотя бы 1 раз, а если выполнится условие после until, то будет повторено>
until <условие продолжения>

Цикл repeat–until оканчивается условием, идущим следом за until, поэтому в условии можно ссылаться на локальные переменные, описанные внутри цикла. Цикл обязательно выполняется хотя бы 1 раз, а затем проверяется условие, необходимо ли продолжать повтор цикла.

Пример для repeat (псевдокод):

a = 0
repeat
  message("Введите число > 0")
  a = input()                         -- Пользователь вводит число
until (a < 1)                         -- Повторяем, если введено не положительное число

Цикл while-do-end позволяет делать циклы, в котором тело может быть никогда не выполнено. Основное отличие от прошлого цикла в том, что условие стоит в начале цикла, на входе в него. То есть сначала проверяется, нужно ли выполнять цикл, а затем происходит выполнение и, если условие всё ещё выполняется, цикл повторяется.

Цикл while-do-end устроен следующим образом:

while <условие> do
  <цепочка действий, которая может быть не выполнена ни разу, выполняется, только если выполнено условие выше>
end

В качестве примера для while рассмотрим, как сделать аналогию цикла for:

local <переменная-счётчик>=<начало цикла>
while <переменная-счётчик> < <конец цикла> do
  <тело цикла>
  <переменная-счётчик> = <переменная-счётчик> + 1
end

Для управления циклами, надо уметь выходить из них по необходимости. Выйти из цикла можно с помощью оператора break.

Пример для break (псевдокод):

a = 0
repeat
  message("Введите число")
  a = input()                         -- Пользователь вводит число
  if a == 0 then
    break                             -- Если введён 0, то выходим
  end

  message(a)
until true                            -- Это означает правду, то есть цикл будет выполняться всегда, если его не остановить

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

Функции

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

  1. желания сократить код
  2. сделать его более понятным
  3. лучше контролировать работу программы, искать ошибки

Важной особенностью lua-функций является то, что они могут быть вызваны интерпретатором. Таким образом можно осуществлять обработку различных событий. К примеру таким способом интерпретатор в червяках оповещает lua скрипт о том, что ранен червь, начался ход и т.д.

В Lua функция имеет следующий формат:

function <имя функции>(<параметр 1>, <параметр 2>, <...>)
  <тело функции>
  return <результат>
end

Оператор return в функции необязателен. Его задача выйти из функции и вернуть результат. Результат может быть любым типом, который мы описали раньше. Можно писать return не указывая результата, при этом функция не будет возвращать результат. Размещать return можно где угодно внутри функции (не только в конце). Оператор return немедленно выходит из всех циклов, условий и других конструкций и завершает работу этой функции.

Вызов функции происходит в следующем виде:

<имя функции>(<значение параметра 1>, <значение параметра 2>, <...>)

Вызов функции возвращает её значение, то есть можно использовать в различных выражениях, например:

<переменная> = <имя функции>(<значение параметра 1>, <значение параметра 2>, <...>)

В самом lua уже есть несколько предопределённых функций. Они, как правило разделены на библиотеки (то есть при вызове функции sin надо указывать её библиотеку, то есть math.sin() ) и конкретная версия интерпретатора может не включать в себя некоторые из них. Список функций можно узнать на этой странице

В прошлых примерах мы рассматривали псевдо-функцию message. В worms её можно реализовать так:

function message(text)
  SetData("Text.TestComment", text)
  SetData("CommentaryPanel.Comment", "Text.TestComment")
  SendMessage("CommentaryPanel.ScriptText")
end

Подробней о работе этого кода мы расскажем позже, а сейчас важно понять, что тут определена функция с именем message, имеющая 1 входной параметр, выполняющая задачу вывода текста на экран в червяках. Она не возвращает результата (нет оператора return), но в ней вызываются несколько функций, которые определены интерпретатором червяков.

Функция становится доступной после её определения, то есть нет смысла писать строку message("Привет") если функция ещё не определена.

Есть достаточно лёгкое для понимания, но порой сложное для реализации понятие под названием рекурсия. Рекурсия - это вызов из функции этой же функции. Вот пример (вычисляет факториал числа):

function factorial(number)
  if (number > 1) then
    return number * factorial(number - 1)
  else
    return 1
  end
end

message(factorial(6))

Выведет 720. Работа этого кода довольно проста. Сначала мы вызываем factorial(6), она возвращает 6 * factorial(5), factorial(5) возвращает 5 * factorial(4), и так далее до factorial(1), который вернёт 1. Получим, что factorial(6) = 6 * 5 * 4 * 3 * 2 * 1 = 720.

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

function add_mul(a, b)
  return a + b, a * b
end

x = 10
y = 20

s, m = add_mul(x, y)

message("Сумма x и y = " .. s)
message("Произведение x и y = " .. m)