Open Hexagon

Open Hexagon

Not enough ratings
Cкрипты на Lua #1 - Создание базовых паттернов
By The Sun XIX
ДИСКЛЕЙМЕР: доцентам компьютерных наук читать запрещено.
Я буду давать максимально тупые объяснения чтобы рассказать максимально просто о начальном кодинге для этой игры. Посвящать кого-либо в нюансы программирования и вообще учить чему-либо хорошему и полезному я не собираюсь.
И вообще, единственная причина почему я пишу ещё одну стену текста это подавление моего очередного всплеска графомании.

P.S. на самом деле объяснения не максимально подробные, в основном я просто показываю примеры и иногда рассказываю почему они работают, но я надеюсь на вашу догадливость :p
   
Award
Favorite
Favorited
Unfavorite
Что нужно знать перед началом
Я не могу и не хочу сейчас писать гайд, начиная от объяснения как включать компьютер, поэтому сорян тем, кто про создание уровней вообще ничего не знает.

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

Если нет, то:
1) установите что-нибудь для редактирования кода (КРОМЕ БЛОКНОТА, ЭТО ВРЕДНО ДЛЯ ПСИХОЛОГИЧЕСКОГО ЗДОРОВЬЯ), например Visual Studio Code;
2) сделайте копию любого левелпака, откройте любой файл из Pack/Styles или Pack/Scripts/Levels, меняйте там любые цифры и смотрите в игре что происходит после этого;
3) продолжайте экспериментировать пока не обретёте осознание того, что вы делайте. В крайнем случае, посмотрите документацию.[github.com]

Чтобы быстрее проверять изменения в игре после редактирования файлов включите Debug Mode в настройках и жмите F4 в меню выбора уровней.
1. Подготовка файла Lua для запуска уровня (Scripts/Levels/*.lua)
Для начала нам нужен файл для скрипта Lua. Как добавлять файлы и изменять конфигурацию уровня для подключения файлов я надеюсь вы знаете.

Для максимальной простоты пусть вначале файл будет выглядеть как-то так:
function onInit()     l_setSpeedMult(2)     l_setSpeedInc(0.25)     l_setRotationSpeed(0.2)     l_setRotationSpeedInc(0)     l_setRotationSpeedMax(99)     l_setDelayMult(1)     l_setDelayInc(0)     l_setSides(6)     l_setSidesMin(6)     l_setSidesMax(6)     l_setIncTime(15)     l_setPulseMin(70)     l_setPulseMax(70)     l_setPulseSpeed(0)     l_setPulseSpeedR(0)     l_setPulseDelayMax(0)     l_setBeatPulseMax(0)     l_setBeatPulseDelayMax(0)     l_setBeatPulseSpeedMult(0) end function onLoad() end function onStep() end function onIncrement() end function onUnload() end function onUpdate(mFrameTime) end

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

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

Пример как оно выглядит:
function onLoad() w_wall(0, 40) t_wait(60) w_wall(0, 40) t_wait(60) w_wall(0, 40) end
Эта штука при запуске уровня создаст три стены на одной позиции, между которыми будет задержка (которая создаётся два раза). После этого больше ничего происходить не будет.
В дальнейшем мы будем здесь проверять наш говнокод, который мы создаём.

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

В рамках чего-то далёкого от программирования и компьютерных наук просто считайте переменную как кусок памяти, в которой вы можете хранить данные.

Данные вы можете хранить как числовые, так и строковые:
textVariable = "Some text" numberVariable = 5

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

И да, если вы уже записали что-то в переменную, то всё равно можете изменить её значение на другое:
numberVariable = 5 -- равно 5 numberVariable = 7 -- равно 7 numberVariable = numberVariable + 3 -- равно 7 + 3 = 10
Если вы никогда не занимались кодингом вообще, то последняя строчка может сломать вам мозг. Объясняю: код выполняется последовательно сверху вниз и каждая строчка кода выполняется только один раз, поэтому если вы складываете переменную с самой переменной, то вы не получите бесконечный цикл, так как операция произойдёт всего один раз.

Если что, в Lua с помощью -- обозначаются комментарии. Это позволяет писать рандомный дезинформирующий бред, который не будет влиять на работу программы. Т.е. весь текст в строке, идущий после --, никакого влияния на работу кода не оказывает.

Переменные нам будут нужны для двух вещей:
1) выполнение арифметических операций со значениями, хранящимися в переменных;
2) передача значений в нужные нам куски кода.

Первый пункт вроде как понятен:
x = 5 y = 15 z = x + y -- равно 20

Со вторым разберёмся позже.




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

Выглядят они примерно как-то так:
function createSomeWalls() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) end

Смысл сего написания примерно таков: мы говорим, что хотим создать функцию с названием createSomeWalls без параметров () (ПРО ПАРАМЕТРЫ ПОТОМ). Это её заголовок.
Далее мы пишем какой-то код вызова стен и определяем окончание функции с помощью ключевого слова end.
То что происходит между заголовком функции и словом end является телом функции, т.е. списком действий, которая она выполняет.

При вызове этой функции мы сразу создадим три стены в трёх разных позициях. НАМ НЕ НАДО ПИСАТЬ ВЕЗДЕ ТРИ ВЫЗОВА СТЕНЫ ВМЕСТО ОДНОГО ВЫЗОВА ЭТОЙ ФУНКЦИИ.

Как мы можем это использовать:
function createSomeWalls() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) end function onLoad() createSomeWalls() t_wait(60) createSomeWalls() t_wait(60) createSomeWalls() end

Результат будет такой же, как и здесь:
function onLoad() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) t_wait(60) w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) t_wait(60) w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) end

Но это же не очень хорошо, верно?

То, что мы использовали ранее: w_wall(0, 40) и t_wait(60), ЭТО ТОЖЕ ФУНКЦИИ! только их создаём не мы, они уже есть в игре.

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

И да, onLoad() это тоже функция, содержимое которой мы определяем сами. Она вызывается самой игрой один раз при запуске уровня и на это мы повлиять не можем, однако мы можем определить что она будет делать.
3. Подробнее про функции и их сочетание с переменными
Для начала рассмотрим первую полезную нам игровую функцию с параметрами:
w_wall(side, thickness)
Мы уже использовали её ранее чтобы создавать стены. Теперь поговорим подробнее про то, что означают те магические числа, что мы писали в параметрах. Но для начала про сами параметры.

Параметр функции - значение, передаваемое из вызывающего кода внутрь функции.
Если хотите математическую аналогию, то парабола y(x) = x^2 в коде будет выглядеть примерно так:
function y(x) y = x ^ 2 return y end
Если вы не хотите математическую аналогию, то другой аналогии у меня для вас нет.

Для большего смысла мне пришлось также упомянуть ключевое слово return. Оно означает возвращаемое значение функции в точку её вызова. Это как параметр, только наоборот лол.

Пример использования данной функции будет выглядеть так:
function y(x) y = x ^ 2 return y end value1 = y(5) value2 = y(15) value3 = y(25)
То есть, мы передаём в функцию y числа 5, 15 и 25 как значение параметра x, а затем записываем её результат вычислений в переменные value1, value2, value3 после каждого вызова.

Данный код с функцией будет эквивалентен этому:
value1 = 5 ^ 2 value2 = 15 ^ 2 value3 = 25 ^ 2

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

Возвращаемся к w_wall(side, thickness)
Эта функция имеет два параметра:
1) side - определяет позицию (side) появления стены;
2) thickness - задаёт толщину (высоту?) стены.
На всякий случай уточняю, что возвращаемого значения у этой функции нет (или по крайней мере не должно быть..................)

Когда мы в начале писали вот такую штуку:
function onLoad() w_wall(0, 40) t_wait(60) w_wall(0, 40) t_wait(60) w_wall(0, 40) end
то мы с помощью значений задавали параметры расположения и толщины стен.
Также, упомянутая здесь функция t_wait(duration) создаёт задержку, равную её единственному параметру.

Теперь мы будем задавать параметры с помощью переменных, но чтобы мне было легче объяснять возьмём другую игровую функцию: e_messageAddImportantSilent(message, duration)
Она выводит на экран сообщение без звука. Параметры:
1) message - само сообщение (строка текстовая);
2) duration - длительность нахождения сообщения на экране.

Мы можем её использовать так:
function onLoad() e_messageAddImportantSilent("IM THE BEST PLAYER EVER", 180) end
что вызовет сообщение при запуске уровня.

А можем так:
function onLoad() message = "IM THE BEST PLAYER EVER" e_messageAddImportantSilent(message, 180) end

Или даже так:
function getSomeFact() return "IM THE BEST PLAYER EVER" end function onLoad() e_messageAddImportantSilent(getSomeFact(), 180) end

Таким образом мы можем передавать значения между функциями с помощью переменных либо даже самих функций через возвращаемое значение.
4. Создание элементарного паттерна (NOOB SKILL LEVEL)
Постановка проблемы: мы имеем пустой уровень с 6 сторонами. Мы хотим получить один паттерн с одной открытой стороной.


Что мы знаем про этот паттерн? Он состоит из 5 стен в шестиугольном уровне. Этого нам пока достаточно.
function onLoad() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) w_wall(3, 40) w_wall(4, 40) end

И вот мы уже получили что мы хотим.
Но что мы уже знаем? Мы должны использовать функции!

function patternWithOneOpenSide() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) w_wall(3, 40) w_wall(4, 40) end function onLoad() patternWithOneOpenSide() end

Уже лучше, но это не должно так работать. Переместим вызов функции паттерна из onLoad() в onStep() с некоторой задержкой, что позволит паттерну повторяться спустя время.
function onLoad() end function onStep() patternWithOneOpenSide() t_wait(60) end

Работает, но вы можете быть недовольны, что паттерн появляется всегда одинаково. Исправим это.
function patternWithOneOpenSide() position = math.random(0, 5) w_wall(position + 0, 40) w_wall(position + 1, 40) w_wall(position + 2, 40) w_wall(position + 3, 40) w_wall(position + 4, 40) end

math.random(param1, param2) - возвращает случайное значение в промежутке [param1, param2]

Теперь мы получили паттерн, появляющийся в случайном положении через промежутки времени. НО МОЖНО СДЕЛАТЬ ЛУЧШЕ
5. Создание элементарного паттерна. Циклы (ADVANCED NOOB SKILL LEVEL)
Что вы будете делать если нужно будет сделать паттерн не для шестиугольного уровня, а для 3-4-5-7-8 и так далее?
Будете создавать новые функции? Это необязательно.

Новая штука для изучения - циклы. Для создания паттернов в Lua мы используем цикл FOR - DO - END.

Выглядит он так:
for i = 0, 9, 1 do *do something* end

Как это работает:
1) первая строчка состоит из сразу трёх магических цифр:
    1a) i = 0 (мы создаём переменную, равную 0, это счётчик);
    1b) 9 (значение для условия выхода из цикла, как только счётчик i превысит число 9, цикл завершится);
    1c) 1 (инкремент, значение на которое увеличивается счётчик после выполнения одной итерации).
2) тело цикла от заголовка до end (так же как и в функциях), то что будет выполняться в каждой итерации.

Чаще всего люди не пишут инкремент явно и оставляют запись в виде for i = 0, 9 do. Это тоже будет работать, поскольку по умолчанию инкремент равен единице.

Изменим наш код паттерна с помощью цикла:
function patternWithOneOpenSide() position = math.random(0, 5) for i = 0, 4 do w_wall(position + i, 40) end end

На каждой итерации позиция стены будет сдвигаться на 1 за счёт счётчика. Так и должно быть. В результате мы получили те же пять стен, но без повторяющегося кода.

Что надо сделать, чтобы получить нужное количество стен в зависимости от формы уровня? Использовать l_getSides() - возвращает текущее количество сторон уровня.
function patternWithOneOpenSide() position = math.random(0, l_getSides() - 1) for i = 0, l_getSides() - 2 do w_wall(position + i, 40) end end

Почему -2?
• потому что условие <=, (i = 0, N do даст нам N + 1 итераций) для этого нам надо -1
• потому что нам нужна одна открытая сторона, это уже -2

Почему я инициализирую цикл с нуля (i = 0), а не единицы (i = 1)?
Потому что я привык в кодинге считать с нуля. Lua в этом плане язык особенный, потому что тут и цикл <=, и массивы индексируются с единицы, но в большинстве ЯП счёт идёт от нуля.
Вы можете писать [for i = 1, l_getSides() - 1 do], но меня это будет путать.

Паттерн работает? Работает. Универсальный для сторон? Вполне. Что ещё надо? Что-нибудь более осмысленное.
6. Создание серии паттернов (ELEMENTARY SKILL LEVEL)
Поскольку просто случайные стены используются редко, то рассмотрим создание обычных паттернов с определённой последовательностью сторон.

В качестве примера сделаем серию паттернов с "обратными" позициями.

Созданный ранее паттерн возьмём за основу и немного переделаем для лучшего контролирования переменных:
function patternWithOneOpenSide(side, thickness) for i = 0, l_getSides() - 2 do w_wall(side + i, thickness) end end
Изменения: начальную позицию (side) и толщину перенесли в параметры.

Сам по себе он нам не особо интересен, но на его основе мы можем создать серии:
function inversedPatternSeries(thickness, timesDo, delay) side = math.random(0, l_getSides() - 1) move = l_getSides() / 2 for i = 0, timesDo do patternWithOneOpenSide(side + move * i, thickness) t_wait(delay) end end
Переменная move будет двигать паттерн во время цикла после каждой итерации за счёт умножения на счётчик i.

Если вы будете использовать серии паттернов, то уберите вызов задержки в onStep() функции, потому что за один вызов серии будет создаваться несколько паттернов, между которыми нужно будет создавать задержку уже внутри самой функции.

Теперь подробнее о том, что происходит:
1) мы получаем случайную позицию паттерна на любой из граней;
2) начинается цикл, идёт первая итерация: появляется паттерн в случайной позиции + вызывается задержка;
3) проверяется условие i <= timesDo, если оно true, то создаётся следующая стена, но уже с другой позицией: к предыдущей позиции добавляется половина числа граней на уровне (side + move), таким образом мы получаем "инвертированный" паттерн;
4) паттерны продолжают появляться и чередовать противоположные стороны до окончания цикла (+ move * i) и последующего завершения серии.

Отдельно про добавленные параметры:
1) thickness - толщина, как обычно. Впрочем, вы можете сделать глобальную переменную в главном файле, что-то вроде thickness = 40 и потом использовать её значение в самой функции без параметра;
2) timesDo - сколько раз вы хотите создавать паттернов за одну серию. Обычно определяется с помощью math.random(a, b). Помните, что условие <=, поэтому 0 создаст один паттерн;
3) delay - длительность задержки, как правильно выбирать задержу для каждого паттерна я понятия не имею.

С помощью небольшой замены можно сделать уже другой вид паттерна:
function leftRightPatternSeries(thickness, timesDo, delay) side = math.random(0, l_getSides() - 1) for i = 0, timesDo do move = i % 2 patternWithOneOpenSide(side - move, thickness) t_wait(delay) end end
Что-то похожее на лево-право паттерн. % - остаток от деления, i % 2 будет давать +1 на нечётных значениях счётчика и +0 на чётных, что позволит чередовать соседние стороны на разных итерациях.


Как вы можете видеть смысловое отличие только в способе изменения позиции паттерна во время серии. Если хотите сделать что-нибудь ещё, то просто сделайте другой способ изменения для move, чтобы получить что-нибудь новое.
Например, умножьте move на два из leftRight паттерна и посмотрите что получится.
Просто замените move = i % 2 на move = i % 2 * 2
7. Генерация последовательности разных паттернов (INTERMEDIATE SKILL LEVEL)
Что делать в случае, когда мы хотим создавать последовательности из разных видов паттернов?
Во-первых, нужны условия, синтаксис у них примерно такой:
if условие1 then сделать_действие1 elseif условие2 then сделать_действие2 elseif условие3 then сделать_действие3 else сделать_что_нибудь end
• if then и end - обязательные части условия;
• elseif - (опционально) дополнительное условие если предыдущее условие не сработало;
• else - (опционально) действие если никакие условия не сработали.

• код после then выполняется только если предшествующая проверка условия равна true;
• условие для проверки создаётся с помощью логических операций ==, !=, >, >=, <, <=. Например, a == b - возвращает true если значения a и b одинаковые;
• выполняется код только первого условия с проверкой true, после него остальные игнорируются.

Условия нам нужны для создания функции выбора паттерна, которая будет вызывать нужную функцию паттерна в зависимости от значения ключа:
function addPattern(key) if key == 1 then inversedPatternSeries(40, math.random(2, 3), l_getDelayMult() * 30) elseif key == 2 then leftRightPatternSeries(40, math.random(3, 4), l_getDelayMult() * 25) end end
l_getDelayMult() - возвращает текущий множитель задержки уровня.

Теперь нам нужен список ключей паттернов, который мы хотим использовать:
keys = { 1, 1, 2, 2, 2 }

Создаётся он в виде массива (переменная, хранящая несколько значений). Нумерация элементов идёт от 1, доступ через индекс keys[value]. Расположение элементов в массиве выглядит так:
keys = { 1, 1, 2, 2, 2 } keys[0] -- null keys[1] -- 1 keys[2] -- 1 keys[3] -- 2 keys[4] -- 1 keys[5] -- 2 keys[6] -- null

Массив ключей мы можем использовать непосредственно для вызова паттернов:
keys = { 1, 1, 2, 2, 2 } index = 1 function onStep() addPattern(keys[index]) index = index + 1 if index - 1 == #keys then index = 1 end end

Подобный код используется в подавляющем большинстве уровней, включая официальные. Объясняю, что здесь происходит:
1) вызывается паттерн через addPattern(keys[index]); поскольку index = 1, keys[index] = 1, то addPattern(key) вызовет inversedPatternSeries(...) через условие key == 1;
2) увеличивается индекс на единицу;
3) index = 2, keys[index] = 1, снова вызывается inversedPatternSeries(...) через условие key == 1;
...
N) #keys - количество элементов в массиве, в данном случае #keys = 5. Когда индекс становится больше чем количество элементов в массиве (index - 1 == #keys), то его значение становится снова 1 и последовательность повторяется заново.

Есть недостаток: порядок ключей всегда одинаков, будет повторяться исходная последовательность. Для разнообразия нужно "перемешивать" порядок ключей в массиве, для этого используется функция shuffle(x):
function shuffle(x)
for i = #x, 2, -1 do
local j = u_rndIntUpper(i)
x[i], x[j] = x[j], x[i]
end
end
Работает она следующим образом: вы используете массив ключей как параметр при вызове этой функции, функция меняет расположение значений в переданном массиве. Причём глобально, нет необходимости использовать возвращаемое значение.

Полный код для генерации последовательностей паттернов будет таким:
function shuffle(x)
for i = #x, 2, -1 do
local j = u_rndIntUpper(i)
x[i], x[j] = x[j], x[i]
end
end function addPattern(key) if key == 1 then inversedPatternSeries(40, math.random(2, 3), l_getDelayMult() * 30) elseif key == 2 then leftRightPatternSeries(40, math.random(3, 4), l_getDelayMult() * 25) end end keys = { 1, 1, 2, 2, 2 } index = 1 shuffle(keys) function onStep() addPattern(keys[index]) index = index + 1 if index - 1 == #keys then index = 1 shuffle(keys) end end

keys = { 1, 1, 2, 2, 2 } + shuffle(keys) = {2, 2, 1, 2, 1} или что-нибудь ещё
Итоговый рабочий код для тестов
function patternWithOneOpenSide(side, thickness)
for i = 0, l_getSides() - 2 do
w_wall(side + i, thickness)
end
end

function inversedPatternSeries(thickness, timesDo, delay)
side = math.random(0, l_getSides() - 1)
move = l_getSides() / 2

for i = 0, timesDo do
patternWithOneOpenSide(side + move * i, thickness)
t_wait(delay)
end
end

function leftRightPatternSeries(thickness, timesDo, delay)
side = math.random(0, l_getSides() - 1)

for i = 0, timesDo do
move = i % 2
patternWithOneOpenSide(side - move, thickness)
t_wait(delay)
end
end

function shuffle(x)
for i = #x, 2, -1 do
local j = u_rndIntUpper(i)
x[i], x[j] = x[j], x[i]
end
end

function addPattern(key)
if key == 1 then inversedPatternSeries(40, math.random(2, 3), l_getDelayMult() * 30)
elseif key == 2 then leftRightPatternSeries(40, math.random(3, 4), l_getDelayMult() * 25)
end
end

keys = { 1, 1, 2, 2, 2 }
shuffle(keys)
index = 1

function onInit()
l_setSpeedMult(2)
l_setSpeedInc(0.25)

l_setRotationSpeed(0.2)
l_setRotationSpeedInc(0)
l_setRotationSpeedMax(99)

l_setDelayMult(1)
l_setDelayInc(0)

l_setSides(6)
l_setSidesMin(6)
l_setSidesMax(6)
l_setIncTime(15)

l_setPulseMin(70)
l_setPulseMax(70)
l_setPulseSpeed(0)
l_setPulseSpeedR(0)
l_setPulseDelayMax(0)

l_setBeatPulseMax(0)
l_setBeatPulseDelayMax(0)
l_setBeatPulseSpeedMult(0)
end

function onLoad()
end

function onStep()
addPattern(keys[index])
index = index + 1

if index - 1 == #keys then
index = 1
shuffle(keys)
end
end

function onIncrement()
end

function onUnload()
end

function onUpdate(mFrameTime)
end

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

Продолжение (не) следует...
Я конечно сам так написал в качестве примера для руководства просто потому что почти все так делают (и так немного проще объяснять), но вы задумайтесь: зачем?
Зачем так делать?

Неправильно:
createSomePattern() t_wait(someDelay)

Правильно:
t_wait(someDelay) createSomePattern()

Докажите, что я неправ :)
Продолжение (не) следует часть 2...
Это мыло((((
Объясните мне, пожалуйста, в чём смысл сего произведения?

Расчёт задержки на основе толщины паттернов разделённой на скорость?
И что это даёт? Что при большей скорости задержка будет изменяться даже если ты не меняешь сам параметр задержки? Что при увеличении толщины паттерна в два раза настолько же будет увеличиваться пространство между паттернами, которое обычно должно быть в несколько раз больше чем их толщина?

Можете кидаться в меня камнями за мои уровни сколько угодно, но на такую функцию задержки я никогда не соглашусь.