Stormworks: Build and Rescue

Stormworks: Build and Rescue

97 Bewertungen
Руководство по основам языка swLUA для начинающих
Von hostbanani und 1 Helfern
Данное руководство рассчитано на людей которые уже разобрались в логике микропроцессоров и хотят получить комплексные знания о работе LUA скриптов. Также оно может быть полезно тем кто уже умеет писать скрипты, но не понимает некоторых отдельных моментов в работе программы.
2
2
   
Preis verleihen
Favorisieren
Favorisiert
Entfernen
Введение
В игре Stormworks одна из самых простых и удобных систем визуального программирования(игроки называют ее логика), но в определенный момент ее может начать не хватать. Когда вам необходимо перебирать сотни чисел, сделать систему сильно зависящую от порядка выполнения операций или вывести изображение на экран, LUA может стать незаменимым инструментом. С его помощью вы можете за 1 тик просчитать баллистическую траекторию, нарисовать на экране нужное вам изображение или записать в память любой объем информации.

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



Давайте для начала добавим LUA блок в контролер и откроем нашу среду разработки.
Сверху мы видим 2 вкладки. В первой мы сейчас находимся, а во второй можно найти подсказки от разработчиков. В левом нижнем углу можно проверить синтаксис и частично работоспособность а справа выход. если в коде присутствуют ошибки в строке снизу вам напишут в чем именно проблема.
И вот мы видим стандартный код. Разберем что же он делает.
  • серый текст которому предшествуют два дефиса называется комментарий. Он не выполняет никаких функций и сделан только для удобства человека пишущего код.
  • функции onDraw() и onTick() это те участки кода которые игра будет запускать каждый игровой тик. Что такое функция и как мы можем писать свои функции мы рассмотрим позже, а пока сойдемся на том, что в onTick() мы пишем всю логику скрипта, а в onDraw() графику.
  • value = input.getNumber(1): value это переменная. = это не тоже самое что на уроках математики в школе. Здесь это называется знак присваивания. input означает, что мы получаем значение с композита на входе lua блока, getNumber говорит о том, что канал числовой, а единица это константа говорящая, что читаем мы с канала номер один. В итоге мы получаем значение с первого числового канала композита и записываем его в переменную value.
  • output.setNumber(1, value * 10) это функция отправки значения на 1 композитный числовой канал, но как можно заметить, перед отправкой переменной value мы умножаем ее на 10
  • w = screen.getWidth() и h = screen.getHeight() передают нам ширину и высоту экрана, и записывают их в переменные w и h.
  • screen.setColor(0, 255, 0) устанавливает цвет в формате rgb которым далее будет рисовать функция.
  • screen.drawCircleF(w / 2, h / 2, 30) рисует окружность в центре экрана радиусом 30. центр находится путем деления ширины и высоты на 2.
  • end сообщает об окончании функции.
Все команды выполняются по порядку.
Выходит, что скрипт получает число умножает на 10 и отдает назад одновременно с этим рисуя зеленый круг в центре экрана.



Формат RGB состоит из 3 чисел каждое из которых отвечает за яркость своего цвета в итоговом. Первое чисто отвечает за красный Red, второе за зелёный Green, третье за синий Blue.
RedGreenBlue - RGB легко запомнить.

Домашнее задание:
Попробуйте самостоятельно изменить этот код так, чтобы значение на выходе делилось на 20, а круг в центре был синим.
Прочитайте вкладку Help и ознакомьтесь с функциями данными нам разработчиками.
Арифметические операции
В введении мы столкнулись с такой арифметической операцией как умножение и естественно это не единственная операция в языке.
Полный список выглядит так:
  • + — сложение чисел. все просто 5 + 5 = 10 думаю с этим затруднений не будет.
  • - — вычитание чисел. также как и со сложением 10 - 5 = 5.
  • * — умножение чисел. и снова все привычно 5 * 5 = 25.
  • / — деление. 10 / 2 = 5.
  • // — целочисленное деление. это уже непривычно если 5 / 2 = 2,5 то 5 // 2 = 2. эта операция — выполняет деление и отбрасывает дробную часть.
  • ^ — возведение в степень. ничего необычного 2^4 = 16.
  • % (fmod) — остаток от деления. Выдает число которое остается после деления например 5 % 2 = 1, потому что 5 содержит в себе двойку 2 раза и после вычитания этих двоек остается 1. Ещё пример 25 % 10 = 5.
Все эти операции можно записывать в виде:
x = y / 2
Используя упомянутый ранее знак присваивания.



В какой-то момент перед вами может возникнуть вопрос: "А как найти корень?"
Те, кто разбираются в математике вероятно предложили x^0.5 и они правы, но есть способ лучше который позволяет выполнять, и более сложные вычисления чем просто корень.
В Stormwork LUA нам спустили с небес модуль math.
Когда вы пытаетесь обратится к нему необходимо в начале написать "math." а далее название функции.
Например, квадратный корень можно посчитать с помощью функции math.sqrt(x).
Полный список функций можно прочитать Здесь [www.cronos.ru].



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

Домашнее задание:
Напишите программу получающую 4 числа (Ваши координаты по X, Ваши координаты по Y, координаты произвольной точке на карте по X, координаты произвольной точке на карте по Y) и возвращающую расстояние между этими точками

Подсказка: Используйте теорему Пифагора для расчета.
Оператор ветвления
Оператор ветвления — это вещь без которой скрипты были бы бессмысленны. Он представлен в единственном экземпляре, но при этом обладает достаточным функционалом.
В общем виде его синтаксис выглядит так:
if (условие) then (тело) end
  • if в переводе значит "если" что говорит само за себя.
  • условие это выражение результатом которого будет значение true - правда или false - ложь.
    Для этого применяются операторы сравнения, например: >, < и ==. Двойное равно отличается от оператора присваивания тем, что это операция сравнения, и не стоит их путать, иначе получите ошибку.
  • then ставится после условия.
  • тело выполняется в случаи если условие верно.
  • end сообщает о окончании тела.
Пример:
if x > 0 then x = 0 end
Если x больше 0, то ему присваивается значение 0.

Обратите внимание на то, что тело написано с отступом. Это называется табуляция и она позволяет не путаться в условиях когда их становится 3-4 слоя.



Это ещё не все возможности if, существуют также конструкции if-else и if-elseif.

if-else добавляет слово иначе - else и эта конструкция выглядит так:
if (условие) then (тело 1) else (тело 2) end
Если условие выполняется, тогда выполняется тело 1, иначе тело 2.


if-elseif позволяет добавлять больше условий. например:
if (условие 1) then (тело 1) elseif (условие 2) then (тело 2) end
Если условие 1, тогда выполняется тело 1, а если НЕ выполняется условие 1, но выполняется условие 2, тогда тело 2.


Домашнее задание: Напишите программу которая возвращает число 1, если на входе число больше 0, или 2 в ином случаи.
Операторы сравнения и логические операторы.
В прошлом разделе мы столкнулись с оператором сравнения, поэтому самое время рассмотреть их подробнее. Их можно использовать не только в условии оператора ветвления, но и с знаком присваивания.
x = y > 0 (это выражение запишет в переменную x значение типа bool, true или false.)
После этого переменную можно использовать как самостоятельное условие для оператора if.

Вот так if x then новички часто ошибаются и пишут if x == true then это будет работать, но лучше так не делать. Если вы хотите выполнить условия в случаи, если x - ложно, то достаточно будет написать if not x then. В данном выражении not — это логический оператор, но вернемся к ним чуть-чуть позже и рассмотрим для начала все операторы сравнения:
  • < меньше. возвращает истину если в (x < y) x меньше чем y.
  • > больше. тоже что и < только наоборот.
  • <= меньше или равно. Тоже самое, что и < только оно вернет истину ещё и в случаи когда x = y.
  • >= больше или ровно. Тоже, что и <= только наоборот.
  • == равно. Вернет истину если x=y.
  • ~= не равно. Тоже, что и == только наоборот.



Далее возникает вопрос: "А как добавить больше чем 1 условие?". Тут на помощь нам приходят логические операторы. Например, если нам необходимо чтобы x был больше 0 и меньше 5.
В самой задаче есть подсказка в виде предлога "И" на английском, это будет and, поэтому выражение будет иметь вид: x > 0 and x < 5 зная английский вы должны очень быстро запомнить все операторы, вот их полный список:
  • or (ИЛИ): если хоть с одной из сторон оператора true, то на выходе тоже true
    x
    y
    x or y
    false
    false
    false
    true
    false
    true
    false
    true
    true
    true
    true
    true

  • and (И): возвращает true, если с обоих сторон true.
    x
    y
    x and y
    false
    false
    false
    true
    false
    false
    false
    true
    false
    true
    true
    true

  • not (НЕ): инвертирует значение.
    x
    not x
    false
    true
    true
    false
Не хватает конечно оператора xor, да, можно вывести его своей функцией, но об этом позже....

Домашнее задание:
Напишите программу которая выдает на выход true, если на входе №1 число больше 0 или на входах №2 и №3 одинаковые значения.


Переменные и типы данных.
Ранее мы уже не раз с толкнулись с такой вещью как переменная. Пора разобраться, что это такое, какие они бывают, и с чем их едят.
Переменная - это область в памяти нашего скрипта в которую мы можем записать данные.
Данные бывают таких типов как: String, Number и Boolean.

Также существует тип nil к которому принадлежат все переменные, которые вы ещё не использовали. nil == false, а кроме него все числа и строки будут равны значению true.



Тип string - это строка. записывается в виде x = "hello world". если мы сделаем x = x.. " 123" то получим в переменной x строку "hello world 123".
если мы хотим убрать часть строки можно использовать метод sub например x:sub(1, 5) даст нам строку "hello" первое и второе число передаваемое в метод sub сообщает ему индекс нужного символа (порядковый номер). если сообщить отрицательное число то он отсчитает с конца строки. Также к нему можно прибавить число. Например:
x = "number: " y = 52 x = x.. y
Даст нам строку "number: 52".
Стоит учесть что если сделать:
x = "number: " y = 50 / 2 x = x.. y
То мы получим строку "number: 25.0". почему так разберемся далее.



Тип number (числовой) в языке Lua делится на 2 типа, inter(целое число) и float(число с плавающей запятой). при необходимости lua сам конвертирует одно в другое как это было в предыдущем примере где мы делили целое число.
Он не разбирается получится в итоге целое или нет. Но если мы используем целочисленное деление результатом будет тип int и .0 на конце не будет.

Чтобы преобразовать float и int используйте функцию math.floor(x). Она отбросит дробную часть и переведет значение в целочисленный тип. Можно также использовать x//1, но это костыль для маньяков.



Тип Boolean (логический). самый простой тип, способный принимать только 2 значения, true и false.



Также я бы хотел выделить тип table отдельно так как он непохож на все предыдущие. это скорее структура данных чем самостоятельный тип.
Его называют таблица или массив.Он может содержать любой тип данных и состоит из ячеек к которым можно обратится по индексу. в отличии от многих других языков нумерация в LUA начинается с единицы.
Объявим произвольный массив:

mass = {"hello world", 54, true}

Теперь к каждой ячейке массива можно обратится так: mass[индекс].
Например в ячейке mass[1] будет строка "hello world".
Используя знак # можно получить длину массива: #mass выдаст значение 3.
Но, если мы добавим элемент №5: mass[5] = 36 то #mass все также выдаст 3, так как в ячейке №4 будет значение nil и он решит, что на элементе №3 массив закончен.

Индексом в типе table не обязательно должно быть число.
объявим таблицу:

tab = {["str"] = "hello world", ["number"] = 53, [89] = true}

Да, индексами этой таблицы служат как числа, так и строки.
Теперь в элементе tab["str"] будет хранится строка "hello world".
Определить длину такого массива с помощью # не удастся.

Также в качестве элемента массива может быть другой массив а таком случаи его называют двумерный массив или матрица.
mass = {{"hello","world"}, {"hello","world"}, {"hello","world"}}
при такой записи доступ к элементам можно получить так: mass[1][2] отсюда мы получим строку "world".
Массивы также могут быть трех, четырех и сколько угодно мерными.

Массивы особенно полезны при работе с циклами которые мы рассмотрим далее.

Домашнее задание: Создайте таблицу в которой под индексом "int" будет хранится целое число 16, а под индексом "float" число 16 с плавающей запятой. После чего выведете на экран друг под другом используя только одну функцию screen.drawText(). Для переноса строки в типе string используется символ "\n".

Циклы
Часто возникает необходимость повторить один и тот же участок кода несколько раз.
Например, предположим, что вам нужно опросить все каналы и внести данные с них в массив.
Эту задачу можно решить так:
mass = {} mass[1] = input.getNumber(1) mass[2] = input.getNumber(2) mass[3] = input.getNumber(3) mass[4] = input.getNumber(4) mass[5] = input.getNumber(5) .............
Но после этого в ваш скрипт будет переполнен мусором.
Лучшим вариантом будет сделать так:

mass = {} for iter = 1, 32 do mass[iter] = input.getNumber(i) end
for — это цикл который повторяет свое тело постоянно меняя 1 переменную.
В общем виде он выглядит так:
for (переменная) = (начальное значение), (конечное значение), (шаг) do (тело цикла) end
for прибавляет к переменной шаг и выполняет тело пока не достигнет конечного значения. шаг необязательный параметр, если его не указывать он равен 1. как правило переменную принято называть i или iter это сокращение от слова iteration - итерация, так называется 1 проход цикла.
если я напишу:
st = "" for i = 0 , 10, 2 do st = st.. i end
то я получу строку "0246810".



Иногда бывает что мы не знаем заранее сколько нам понадобится итераций.
В таком случаи нам поможет цикл while который выглядит так:
while (условие) do (тело цикла) end
Он будет выполнять тело пока условие не станет ложным.
Например попробуем разбить строку "hello world" на массив символов.
mass = {} st = "hello world" while st ~= "" do --выполнять пока строка не пустая mass[#mass + 1] = st:sub(1, 1) --[[добавляем в следующий свободный элемент массива первый символ из строки]] st = st:sub(2,-1) --удаляем первый символ из строки end
и вот мы уже разбили строку.



Остался один не рассмотренный вид цикла который используется довольно редко, но бывает очень полезен.
repeat (тело) until (условие)
Если условие выполняется программа идет дальше а если нет то еще раз выполняет тело цикла. В отличии от while проверяет в начале итерации этот цикл производит проверку в конце, после каждой итерации.



Любой цикл можно досрочно прекратить командой break
x = 0 while true do x = x + 1 if x > 100 then break end end
Это позволяет прекращать цикл внутри его тела.

Домашнее задание:Используя цикл получите на входе 32 числа (числа со всех намбер каналов) и выведете на первый числовой выход сумму со всех входов. Функция input.getNumber() и оператор сложения должны встречаться в вашем коде не более одного раза.

Функции
Вот мы уже близимся к финалу. Рассмотрим не сложную, но очень полезную тему.
Во втором разделе вы уже побывали найти дистанцию между двумя точками, и вероятно получили формулу math.sqrt((x1-x2)^2+(y1-y2)^2). А теперь представим, что в вашем скрипте такая операция производится в 3 - 4 местах и хотелось бы как-нибудь упростить это выражение.
Для этого подходят такая вещь как функция. попробуем записать ее:
function Distance(x1, y1, x2, y2) return math.sqrt((x1-x2)^2+(y1-y2)^2) end
Записывается она в начале или в конце скрипта и к ней можно обратиться так:
D = Distance(x1, y1, x2, y2)
Слово return отвечает за возвращение значения функции. после выполнения этого пункта она закрывается.



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



Глобальная переменная должна быть объявлена вне всех функций, лучше всего это делать в самом начале. Например:
st = "hello" function set_text(s) st = s end
В данном случаи эта процедура будет менять переменную st.
Если вы не хотите менять глобальную переменную st, а ввести одноименную в этой функции, то стоит написать local st = s.



Функция может быть элементом массива. Например, если мы напишем mass = {"hi", set_text},
то мы сможем вызвать ее с помощью mass[2](x).



Функция может вызывать саму себя. Это называется рекурсия. Например:
function add(x) if x >= 100 then return x else return add(x+1) end end
Вернет не меньше 100.
Но если мы изначально передадим этой функции значение -1000000000, то ваш скрипт сломается.
Дело в том, что каждая новая открытая функция добавляется в стек у которого есть свои пределы и не стоит злоупотреблять методом рекурсии.



Доступные нам функции onDraw() и onTick() работают по совершенно такому же принципу, только вызывает из сама игра.
Разница в том, что onTick() вызывается раз в игровой тик, а onDraw() при каждой отрисовки экрана. Поэтому имейте ввиду, что если таймер вида x = x + 1 в функции onDraw() начнет идти быстрее при подключении двух или более экранов так как для каждого из них, она вызывается заново.

Домашнее задание: Напишите функцию вызываемую из onTick() и возвращающую массив собранный со всех числовых каналов.
Алгоритмы
Вот мы и изучили большую часть конструкций языка LUA, но как же нам теперь программировать. Сухих знаний о том как пишется if и for недостаточно, и если вы не будете практиковаться, то даже это вы очень быстро забудете.

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

Возьмем задачу: написать скрипт управления дверью через push button(нажимная кнопка), который будет открывать дверь на ограниченный отрезок времени при нажатии, а при удерживании открывать до тех пор пока не будет нажата кнопка.
У нас в памяти есть информация режима постоянного открытия двери и таймер ее короткого открытия.
Разобьем большую задачу на малые:
Для начала нужно решить задачу определения короткого - длинного нажатия.
Long_Press_Time = 60 Press_Timer = 0 function onTick() Button = input.getBool(1) if Button then Press_Timer = Press_Timer + 1 end Short_Press = not Button and Press_Timer < Long_Press_Time and Press_Timer > 0 Long_Press = Press_Timer == Long_Press_Time if not Button then Press_Timer = 0 end end
Функция onTick вызывается раз за разом и работает с начала до конца. Можно воспринимать это как бесконечный цикл.
При нажатии на кнопку у нас растет таймер, происходят проверки на условия нажатия и в случаи отпущенной кнопки таймер сбрасывается.
Короткое нажатие, если кнопку отпустили, а таймер вырос меньше, чем на время длинного нажатия.
Длинное нажатие, если таймер равен длине длинного нажатия.

Введем переменную в которой будет сохранятся информации о постоянном открытии двери и добавим ее переключение по длинному нажатию.
Long_Press_Time = 60 Press_Timer = 0 Forever_Open = false function onTick() Button = input.getBool(1) if Button then Press_Timer = Press_Timer + 1 end Short_Press = not Button and Press_Timer < Long_Press_Time and Press_Timer > 0 Long_Press = Press_Timer == Long_Press_Time if not Button then Press_Timer = 0 end if Long_Press then Forever_Open = not Forever_Open end end
в случаи длинного нажатия Переменная Forever_Open переключается.

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

Open_time = 120 Long_Press_Time = 60 Press_Timer = 0 Open_Timer = 0 Forever_Open = false function onTick() Button = input.getBool(1) if Button then Press_Timer = Press_Timer + 1 end Short_Press = not Button and Press_Timer < Long_Press_Time and Press_Timer > 0 Long_Press = Press_Timer == Long_Press_Time if not Button then Press_Timer = 0 end if Long_Press then Forever_Open = not Forever_Open end if Short_Press then if Forever_Open then Forever_Open = false else Open_Timer = Open_time end end if Open_Timer > 0 then Open_Timer = Open_Timer - 1 end Door_open = Open_Timer > 0 or Forever_Open output.setBool(1, Door_open) end
Вот так. И заодно добавили проверку должна ли быть открыта дверь и вывод этого значения.
Задаче решена!
Обратите внимания что я вынес 120 и 60 в отдельные переменные. будет очень плохо если в вашем коде будет много волшебных чисел. но мы играем в stormworks по поэтому готовитесь выбросить табуляцию и нормальные названия переменных в угоду количества символов.
по крайней мере советую при обучении писать правильно, это все нужно чтобы вам было удобнее читать свой код и модифицировать его.

Задания для самостоятельного решения:
  • Напишите программу которая заставит лампочку мигать сигналом SOS. (3 коротких вспышки - 3 длинных - 3 коротких и так по кругу)

  • Нарисуйте на экране перемещающийся круг. отталкивающийся от стенок (как DVD-скринсейвер(screensaver)

  • Напишите программу сохраняющую все значения со входа за последние 30 тиков и выводящую среднее арифметическое между ними.

  • нарисуйте на экране график функции y = sin( x / 20 )

Попробуйте задуматься и решить эти задачи. Всё намного проще, чем кажется на первый взгляд.
Эпилог
Над руководством работали:

Автор — hostbanani

Редактор — lbc



Спасибо, что прочитали наше руководство. Надеюсь у вас всё получится, и вы будете рады.
Ставьте лайки, добавляйте в избранное,оставляйте комментарии, всегда ответим. Удачи!
31 Kommentare
trinoga02 21. Mai um 1:52 
Я НЕЧО НЕПОНИМЮ ГДЕ СМЫСЛ СЛОВ Я ТУПОЙ ОБЯСНИТЕ ПОПРОЩЕЕЕ!:steamsad:
Latom 🎀𝒜𝓁𝑒𝓍🎀 10. Apr. um 12:09 
Если кто-то прям вообще не шарит и не хочет тратить время на изучение можете попробовать написать через ии [YesChat.ai] - https://www.yeschat.ai/ru
Он довольно легко это делает и не сказать что с ошибками - у меня получилось сделать со второй попытки когда я правильно описал откуда я беру информацию и куда ее хочу записать. если работаете с композитами описывайте каналы и их назначения
hostbanani  [Autor] 9. Apr. um 13:04 
Нет, все что есть это 32 цифровых и 32 бинарных канала и как передавать через них информацию твой выбор.
ilya-vsilyev 9. Apr. um 10:20 
а в swLUA есть что-то типа output.setTable
Red Lizard 24. Dez. 2024 um 6:40 
а что если я хочу написать кода для модульного двс, но не понял из этого туториала ни чего
P.s изучал ещё в школе паскаль но уже весь забыл
Serial Designation N 4. Nov. 2024 um 6:55 
достаточно полезно
hostbanani  [Autor] 3. März 2024 um 9:47 
Можно использовать math.floor(x + 0.5) костыль, но зато работает.
DiFox 2. März 2024 um 10:35 
А можно добавить про округление? Всё никак не могу нигде найти, как называется функция нормального округления, где при 0.5 и выше округляет вверх, при дробном значении < 0.5 округляет вниз. Пробовал round, игра выдаёт ошибку, что мол такого в math тупо нет, а мне нужно, чтоб без всякого лишнего мусора в виде условий
OLDSam 3. Mai 2023 um 3:07 
Прочитал раз 30 так и не понял
ВалентинаПетровн 2. Nov. 2022 um 10:00 
классный гайд, мне очень помогло!