Garry's Mod

Garry's Mod

225 ratings
Expression 2 Полное руководство по программированию
By cataph.
Полное и подробное руководство по программированию чипов Expression 2.
4
   
Award
Favorite
Favorited
Unfavorite
Предисловие
Это руководство посвящено программированию чипов Expression 2, которые входят в состав Wiremod. Чип имеет очень широкий набор функций, взаимодействующих с игровым миром, и простой синтаксис. Имея интересную идею и некоторое количество свободного времени можно творить впечатляющие вещи :)

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

Предполагается, что читатель уже знаком с основными принципами Wiremod, то есть умеет подключать различные устройства друг к другу и имеет представление о том, как они работают. Ещё раз, это очень важно! Если вы не знаете, как работать с вайрмодом, рекомендую перед этим ознакомиться с ним с помощью других руководств, поскольку в части случаев придётся связывать чип с другими устройствами.

В первых трёх главах я буду рассказывать о самых элементарных вещах, которые сами по себе практического смысла почти не имеют. Их просто необходимо запомнить, ведь на них держится всё остальное. Я очень рекомендую писать программы самому, не переписывая ничего с уже готовых программ и не делая что-то по аналогии. Чем больше вы это делаете, тем лучше и тем больше вы будете способны написать сами.

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

Ну что, начнём?


История обновлений
18.09.17 - Публикация руководства
23.09.17 - Исправление орфографических и пунктуационных ошибок, корректировка текста

В будущем планируется:
  • Добавление детальной инструкции по всем функциям окна редактора (пока что отсутствует, поскольку это достаточно хорошо разобрано в других руководствах, ищите в поиске. Если оно вам нужно, оставьте ваше мнение в комментариях)
  • Краткий обзор библиотек
  • ( а возможно и отдельные подробные руководства по библиотекам)
  • Иллюстрация каждой главы подробно разобранными примерами программ

I. Программа. Операторы. Основные принципы.
После создания новой вкладки открывается программа со стандартным шаблоном:
Каждое из слов, начинающееся с символа @ называется директивой. Текст, захваченный внутри #[]# является комментарием и не влияет на работу программы. В нашем случае, при создании окна появляется шаблон с директивами и текст, в котором разработчики пишут о нововведениях. Он не влияет на работу чипа, а значит, его можно смело стереть.

Справа от каждой директивы обычно записывают некоторое свойства, которые обычно не меняются в течение работы чипа: имени, визуальной модели чипа, используемые переменные. Давайте рассмотрим их подробнее:

Директивы

Директива
Описание
@name
Задаёт название чипа. Оно высвечивается, например, при наведении мышкой на модельку чипа.
@inputs
Используется для перечисления переменных, которые отобразятся в списке входов чипа. Необходимы для взаимодействия с остальными устройствами Wiremod. Благодаря им чип может получать информацию от других чипов и датчиков.
@outputs
Здесь перечисляются переменные, которые отобразятся в списке выходов чипа. Пригодятся для того, чтобы чип выводил информацию на другие устройства и воздействовал на его окружение (другие чипы, механизмы, экраны).
@persist
Используется для перечисления внутренних переменных. Эти переменные доступны только внутри чипа и необходимы для сохранения значений между выполнениями чипа.
@trigger
В этой директиве определяется, изменение каких переменных, перечисленных в @inputs, вызовет исполнение чипа. Есть ещё два дополнительных варианта: @trigger all и @trigger none. В первом случае при изменении любой переменной произойдёт выполнение чипа, во втором случае чип не будет реагировать на изменение ни одной из них.
@model
В кавычках указывается путь к визуальной модельке другого объекта (пропа), на который моделька чипа заменится после его установки в игре.
Замечание 1. Ни одна из директив не является обязательной. Если хочется, можно написать программу, не используя ни одну из них. В случае отсутствия @name чипу присваивается стандартное имя generic. При отсутствии @model моделька чипа определяется вариантом, установленным в меню, где выбирается инструмент Wire Expression 2. Отсутствие директивы @trigger эквивалентно наличию строчки @trigger all, т.е. чип будет исполнятся каждый раз, когда хотя бы одна из переменных, определенных в директиве @inputs изменит своё значение.

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

Что такое переменная

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

@persist Number

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

Все названия переменных записываются только с большой буквы. Цифры и нижний знак подчеркивания "_" допустимы только в том случае, если они не стоят на первом месте. Остальные специальные символы недопустимы. Более подробно объявление переменных и их типы будут разобраны в следующих разделах.

Программа и операции

Программа, которую мы пишем для исполнения в чипе - это не более чем последовательность действий, самых малых "кирпичиков", из которых она состоит. Будем называть их Операторами. Операторы в Expression2 либо записываются через строчку, либо через запятую, если они расположены в той же строке.
Например, несколько простых математических действий можно записать вот так - получится правильная программа (всё что записано после символа "#" является комментарием и не влияет на ход работы программы):
A = 10, B = 30 #сохраним в А 10, а в B 30 C = A + B #сохраним в С сумму А и В D = A - B #а в D - разность A и B
Какой бы сложной ни становилась программа, основной принцип очень прост: описывая последовательность действий с помощью операторов, мы показываем чипу какую последовательность действий нужно выполнить. В Е2 операторы либо разделяются запятыми, если находятся в одной строчке, либо разделяются переносом строки (через Enter). Помните, что когда чип исполняет записанную программу, он идёт строго слева направо и сверху вниз.

Присваивание

Наиболее простым и часто используемым действием, которое вам необходимо будет выполнять в программе является присваивание значения переменной либо извлечение её значения.

В самом деле, мы уже использовали его выше. В самом общем случае, использование оператора присваивания (=) имеет такой вид:
*название переменной* = *выражение*
Как следует из названия, этот оператор вычисляет значение выражения справа и присваивает его переменной слева.
Под выражением подразумевается обычное математическое выражение, составленное из знаков операций (+, -, *, /), чисел, и имён переменных. В последнем случае просто скопируется число из переменной, название которой было записано в выражение (само число по этой переменной не изменится) и будет использовано для того чтобы это выражение посчитать, но уже с числами. Например, мы можем записать:
@inputs A B @outputs C C = A + B
И получить элементарную, но вполне работоспособную программу, складывающую два числа, хранящиеся в A и B. Поскольку (учитывая замечание после списка директив) программа будет реагировать на изменение A и B (напомню, что их значения меняют подключенные ко входам чипа устройства). При изменениях значений чип выполнится снова, пересчитает значение A + B и сохранит его в переменной С, значение которого может извлечь другой объект из Wiremod. Можете проверить сами, подключив что-нибудь для ввода/вывода.
В Е2 доступны все основные математические операции, вроде "-", "+", "*", "/", а также возведение в степень "^".

Также можно использовать "-" перед одной переменной:
C = -A * B
что инвертирует копируемое из А значение на противоположное.

Иногда возникает ситуация когда к уже содержащемуся значению в переменной нужно прибавить некоторое значение. Например, нам нужно увеличить текущее значение в переменной А на 5. Это легко сделать таким образом:
A = A + 5

Конечно, с математической точки зрения эта запись выглядит нелепо, ведь никакое число не равно сумме его самого же с 5. Но давайте посмотрим внимательнее. Выражение справа от знака равенства означает "сумма значения переменной А и 5". Поскольку, сначала вычисляется выражение справа и лишь затем идёт присваивание, как мы видим, действие не лишено смысла.

Можно переписать и более кратко. E2 допускает использование операторов вида *оператор*=, например *=, +=.
A += 5 #эквивалентно A = A + 5 A -= 5 #эквивалентно A = A - 5 и так далее
Использование которых может заметно сократить код программы.

Помимо этого существуют операции ++ и --, они используются только вместе с одной переменной, например
A++ B--
Их действие эквивалентно A = A + 1 и B = B - 1 соответственно. Они могут оказаться весьма удобны, если вам нужно завести счётчик в программе и постоянно увеличивать или уменьшать его на единицу.
II. Сравнение и логика. Остальные операции
Операторы сравнения

В Expression 2 также существуют операции сравнения чисел. Записываются так же как и обычные математические знаки: ">" "<" ">=" "<=". Немного иначе выглядят знаки "равно" и "неравно": "==" и "!=". Сравнение берёт два выражения, находящиеся слева и справа от знака и проводит соответствующую операцию (== сравнивает на равенство, > - больше ли первое выражение чем второе и так далее). В случае, если неравенство верно, результатом такой операции является 1, в противном случае 0. Например:

@inputs A @outputs C A = 3 C = A > 3
Здесь чип считывает значение числа на вход. переменной A, а в C сохраняет 1 если значение А больше 3 и 0 в противном случае.

Не путайте "==" и "="! "=" - оператор присваивания, который сохраняет переменной слева некоторое значение выражения справа от значка "=". "==" проводит сравнение, оно не меняет переменные с которыми работает и возвращает 1 в выражение где оно расположено в случае равенства их значений (ну и 0 в противном случае).

Оператор ветвления (if - else)

Если бы все программы выполнялись последовательно без изменений хода работы, то навряд ли бы мы могли реализовать для них хорошую гибкую функциональность. Хорошо было бы иметь конструкцию, которая сможет в зависимости от выполнения определенных условий производить какие-то действия. И такая конструкция действительно есть!
if (*выражение*) { *оператор1* *оператор2* ... *операторN* }
В случае, если выражение в круглых скобках истинно (отлично от нуля), содержимое фигурных начнёт выполняться чипом. В противном случае пропустится.

Также есть расширенный вариант этого оператора:
if (*выражение*) { *блок операторов1* } else { *блок операторов2* }
Этот оператор следует тому же принципу за тем отличием, что в случае нуля в круглых скобках выполнится содержимое фигурных скобок после слова "else". Грубого говоря, вы даёте программе два варианта: если что-то верно, то нужно что-то сделать. В противном случае нужно сделать что-то другое либо не делать ничего (если вы пользуетесь первым вариантом).

Эта конструкция используется очень часто. Например, вы хотели бы изменять длину поршня (wire hydraulic) с помощью какой-нибудь клавиатуры и хотите ограничить его каким-нибудь числом, например 500, ведь в противном случае поршень уедет дальше чем нужно и что-нибудь сломает.
Разрешить проблему можно так:
Пусть IL - вход, куда подаётся число, считанное с клавиатуры, L - число, подаваемое на контроллер поршня.
@inputs IL @outputs L if (IL > 500) { L = 500 } else { L = IL }
То есть в случае, если задающее длину устройство вдруг выдаст число, большее 500, то на выходе всё равно будет 500.

Но что делать, если вы хотите сделать проверку, скажем, более сложную чем просто сравнение переменной с числом? Предположим, что вы хотите проверить входит ли число в какой-нибудь интервал. Например, по аналогии с предыдущим примером, вы хотите также проверить, что число ещё и больше 0. То есть, входит в интервал (0, 500) Конечно, можно использовать вложенно оператор ветвления
if (Distance < 500) { if (Distance > 0) { ... } }
Но что если это действие может выполняться и в ряде других случаев? А если помимо этого налагаются дополнительные условия? Придется полностью перестраивать все вложенные структуры и переорганизовывать ветвления. Хотелось бы иметь более изящный инструмент для выражения достаточно сложных условий.

Логические операции

Для разрешения проблемы, возникшей в предыдущем подразделе на помощь приходят логические операции. Всего в Expression2 их 3.

Операция "логическое И" (&&)
Слева и справа от неё записываются операнды (операндами называют то, с чем работает операция. Например, в выражении "1 + 2" + - операция, а 1,2 - операнды). Операция возвращает в выражение 0 если хотя бы один из двух операндов равен 0 и 1 если все равны 1. К примеру, запись "1 && 0" вернёт 0, поскольку не все операнды равны 1. С помощью этой операции предыдущая задача на определение, входит ли некоторое число в интервал (0, 500) решается гораздо лаконичнее:

if (A > 0 && A < 500) { ... }
Что даже вполне можно интерпретировать обычным языком: "Если А больше 0 и А меньше 500, то ...".

Операция "логическое ИЛИ" (||)
Также работает согласно названию. Если хотя бы один из двух операндов равен 1 (или оба сразу), то операция возвращает 1. Возвращает 0 только в случае одновременного равенства 0 обоих операндов. Например, с помощью этого оператора можно делать проверки на несколько значений, либо объединять условия, чтобы при выполнении хотя бы одного из них происходило некоторое действие.
Пример:
if (A == 0 || A == 1) { ...#выполняется если А = 0 или А = 1 }

Операция "логическое НЕ" (!)
Ставится перед одним операндом и возвращает инвертированное значение. То есть, 0 меняет на 1 а 1 на 0. Применяется, если в собираемом вами условии необходимо чтобы что-либо происходило наоборот, в случае невыполнения чего-либо.
Пример:
if (!A && C) { ...#выполняется если A ложно, а C истинно }

Замечание. В самом деле, эти операции могут принимать любое числовое значение - ошибки в таком случае не возникнет. Просто любое, отличное от нуля значение будет восприниматься операцией как 1.

Остальные операции
Также в Е2 включены ещё несколько операций, имеющих не такое уж и широкое применение, но которые также стоит рассмотреть.
  • -> - пишется перед переменной. Возвращает 1, если к ней подключено некоторое устройство.
  • ~ - также пишется перед переменной. При изменении входной переменной возвращает 1.
  • $ - возвращает разницу значения переменной между текущим и предыдущим выполнением. Работает не для всех переменных (подробнее в разделе про типы переменных).
  • (*условие*? *выражение1*:*выражение2* ) - тернарный оператор. ( название исходит из того факта что он имеет три операнда) Если условие верно, значение всей скобки равно первому выражению, в противном случае второму.
  • A % B - находит остаток от деления А на B.
Первая операция работает только для входов/выходов, вторая только для входов, третья для выходов и внутренних переменных. Третья операция также работает только для тех типов данных, в которых определена разность.
Первые три операции применяются слева к переменной, например ~Input, ->Out, $Value.
III. Функции
Функции
Вот мы и приступили, пожалуй, к основной части руководства. Понятие функции заимствовано из математики, но в программировании понятие функции несколько более широкое и означает, скорее, некоторую подпрограмму, которая, получив какие-то значения (которые называются аргументами), может производить довольно большую цепочку действий, а результат может, вообще говоря, и не возвращать.

Имя функции в языке Expression 2 обязано (по правилам синтаксиса) начинаться с маленькой буквы. Принимающую на вход N аргументов функцию в общем виде можно записать так:
*имя функции*(аргумент1, аргумент2, ..., аргументN) #и несколько примеров: int(X) #Функция, возвращающая целую часть числа X, то есть отбрасывает дробную часть (int(5.123) = 5) abs(X) #Функция, возвращающая абсолютное значение (модуль) X (если Х положителен, возвращает Х, иначе -Х, грубо говоря, отбрасывает возможный минус) min(X, Y, Z) #возвращает наименьшее из трёх чисел X, Y, Z
Некоторые функции вообще не принимают аргументов. Тогда содержимое их скобок остаётся пустым.
pi() # возвращает число Пи[ru.wikipedia.org] random() # возвращает случайное дробное число в промежутке от 0 до 1
Когда чип доходит до выполнения функции, он "засовывает" в неё аргументы, функция проделывает свою работу и, может быть, возвращает некоторое значение. А может и не возвращать.
Для примеров, указанных выше вы в праве использовать функцию внутри каких-либо сложных составных выражений. Да и любые выражения внутри аргументов функций. Например, если вы хотите, чтобы чип при нажатии на кнопку выбирал что-либо из 3 различных случайных вариантов вам нужно сгенерировать одно случайное целое число.
RandomInt = int(100 * random()) % 3
Смотрим.
От получившегося случайного дробного числа 100 * random(), которое находится в промежутке от -100 до 100, отсекается дробная часть с помощью функции int, а затем, уже от полученного целого числа находится остаток от деления на 3. Какими могут быть остатки? 0, 1, 2. Не может быть так, чтобы мы поделили число на 3, а остаток оказался больше либо равен 3. И действительно, переменная RandomInt после выполнения всех действий равна одному из чисел 0, 1, 2
Пример, конечно, немного надуманный, поскольку такая функция в стандартной библиотеке уже реализована (randint(N,M) возвращает случайное целое число от N до M), но в качестве демонстрации, как вы можете выражать необходимые вам вещи, пример, я думаю, вполне сойдёт.

Также отличает функцию от остальных операторов языка тот факт, что выполнение функции может влиять на окружение чипа. Её работа может проявляться за пределами вашей программы. Например, есть функции, создающие голограммы. (о них будет рассказано позже)
Яркий пример функции, которая ничего не возвращает (и потому может использоваться отдельно в строке программы как оператор) но что-то принимает - функция print. Эта функция выводит свой аргумент в чат в виде текста (виден вывод будет только игроку, который чипом владеет, то есть вам)
print(4455) print(int(100 * random()) % 3 + 1) #Выведет в чат случайное целое число от 1 до 3

В самом общем случае print() принимает аргумент в виде символьного текста (строки). Для этого типа данных есть своё название String. И если вы хотите, чтобы переменная сохраняла именно строку, то в определении переменной в директивах вам нужно дописать суффикс :string после её названия:
@persist Message:string
Для того, чтобы в программе написать выражение, представляющую из себя строку, вам нужно заключить текст в кавычки:
Message = "Hello, world!"
Тогда в переменной Message сохранится строчка "Hello, world!"
Как уже упоминалось, содержимое Message можно вывести в чат, воспользовавшись функцией print
print(Message) #либо print("Hello, world!")
Тогда print возьмёт содержимое переменной Message (или просто строку, как показано во втором примере) и выведет строку в чат.

Подробнее о String
@persist Str:string Str = "String example"
Как уже упоминалось, String - это тип данных. Если вы объявите чипу, что тип переменной является String, то чип начнёт воспринимать содержимое переменной как некоторый текст. Помните, в численную переменную текст сохранять нельзя! При попытке это сделать редактор выдаст ошибку - программа некорректна. Однако, при попытке сохранить число в строке оно будет переведено в текстовый вид и затем сохранено в переменной.

Для произвольных двух взятых строк можно использовать операции "==" (для проверки строк на равенство), а также *имя переменной*[N], где N - некоторое положительное целое число, для получения N-ного символа в этой строке.
@persist Str:string Str = "String example" print(Str[1]) #Выведет первый символ строки Str, то есть S
Помимо этого строки можно объединять с помощью операции "+". Не путайте со сложением чисел! 5 + 10 имеет своим результатом число 15, в то время как "5" + "10" = "510" как объединение двух строк. Вы также можете складывать число со строкой, в таком случае число переведется в последовательность цифр (в текстовый вид) и объединится с другой строкой, например
@inputs Ranger @outputs OutputText:string OutputText = "Distance: \n" + Ranger
&lt;здесь скриншот text screen&gt;
Вывод чипа переведен на text screen - монитор wiremod, отображающий текст

Замечание. \n означает перенос строки. Необходимость такого обозначения объясняется тем, что сам по себе перенос строки, назначенный на кнопку Enter используется в тексте программы и влияет на её структуру. Помните, что внутри текстовой переменной сочетание "\n" считается за один символ.

Также стоит ввести ещё одно замечание. Иногда, функция объявляется с помощью двоеточия и дописывается как суффикс к переменной некоторого типа.
К примеру, функция S:length() (где S - некоторая строка) возвращает количество символов этой строки S.

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

Под S1, S2 ..., т.д. я буду подразумевать переменные, обозначающие строки, под N1, N2, ... т.д. - некоторые переменные, содержащие числа.
Функция
Описание
S:upper()
Делает все символы строки заглавными.
S:lower()
Делает все символы строки строчными.
S:sub(N1, N2)
Возвращает подстроку строки S, выделенную с N1-ого по N2-ой символ.
S:find(S1) / S:find(S1, N)
Ищет в строке S подстроку S1. Возвращает номер, начиная с которого S1 входит в S, если S1 не является подстрокой S возвращает 0. Второй вариант функции проводит поиск, начиная с N-ного символа строки S.
S:replace(S1, S2)
Заменяет все подстроки S1 строки S на S2.
S:toNumber() / S:toNumber(N)
Пытается прочесть число из заданной строки (пытается перевести из строкового представления в число) и затем возвращает его. Второй вариант указывает систему счисления, в которой заданное число представлено в текстовом виде. Возвращает 0 в случае, если из строки с текстом не сможет прочесть его.
toString(N1, N2)
Переводит N1 в текстовое представление по системе счисления N2.
IV. Типы данных
Общая информация о переменных и их типах

Числа и строки не являются единственными типами данных, доступными в программе. В общем случае, когда вы хотите определить переменную некоторого типа нужно дописать после неё суффикс с двоеточием. Числа такого суффикса не требуют. Вообще, перед каждой сохраняющей число переменной можно дописать :number, но это необязательно.
Для экономии места переменные одного и того же типа можно захватить в квадратные скобки и определить тип уже после них:
@persist Counter [Name Status Message]:string
Замечание. Переменные можно и не объявлять явно, а использовать, к примеру, для хранения результата промежуточных вычислений. В таком случае вам нужно просто по ходу программы присвоить переменной с некоторым именем значение и затем пользоваться ей по ходу работы. Тип переменной определится автоматически в зависимости от того, что вы в неё сохраняете. Отличие от переменных, объявленных в директиве @persist заключается в том, что значение, сохраненное в таких переменных обнуляется как только ваш чип заканчивает выполнять программу, поэтому между выполнениями значение не сохранится.

Рассмотрим наиболее часто используемые типы данных и функции для работы с ними.

Number
Суффикс: :number либо отсутствует

Стандартные числа, операции для которых уже были рассмотрены.

Краткая таблица наиболее часто используемых функций, полезных для работы с числами:

Функция
Возвращаемое значение
Краткое описание
Общие функции
round(N) / ceil(N) / floor(N)
N
Округление к ближайшему целому / вверх / вниз.
int(N) / frac(N)
N
Возвращает целую часть числа / Дробную часть числа.
clamp(N, N1, N2)
N
"Зажимает" N между N1 и N2. Если N1 <= N2 возвращает N2; Если N1 >= N3, возвращает Arg3. В противном случае возвращает N1.
Математика
sin(N), cos(N), tan(N), tan(N), cot(N), asin(N), acos(N), atan(N)
N
Обычные тригонометрические функции и обратные тригонометрические. Для работы в радианной мере допишите "r" в конце названия функции, к примеру sinr(N)
root(N1, N2)
N
Берёт корень из N1 степени N2 и возвращает получившееся число. Для квадратного и кубического корней есть функции sqrt(N) и cbrt(N).
log(N1, N2)
N
Возвращает логарифм N1 по основанию N2. Для логарифмов по основанию 2, 10 и e есть отдельные функции log2(N), log10(N) и ln(N).
e(), pi()
N
Константы e и пи.

Vector (3D Vector)
Суффикс :vector

Является типом данных, переменные которого хранят упорядоченную тройку чисел (X, Y, Z), которые в дальнейшем будем называть компонентами (или координатами в зависимости от контекста). В дальнейшем для обозначения аргументов функции будет использоваться значок V. Для векторов определена операция суммирования и разности (V = V1 + V2, V = V1 - V2), которые просто складывают или вычитают их соответствующие компоненты а также операция умножения вектора на число V = N * V, которая каждый компонент вектора умножает на число N.
Векторы (в зависимости от того, как вы интерпретируете содержимое) разумно использовать для выражения физических величин, которые помимо самого значения имеют также и направление вроде силы, перемещения, скорости и т.д. можно им воспользоваться для сохранения цветов в виде (R, G, B), где R, G, B принимают целое значение от 0 до 255. Числу R соответствует интенсивность красного цвета, G зеленого, а B - синего.
Собрать три числа в один вектор можно с помощью функции vec(), которая, в зависимости от количества аргументов, ведёт себя по-разному.
Например
V = vec(N1, N2, N3) V = vec(N)
В первом случае функция объединяет числа N1, N2, N3 и сохраняет их как вектор в V, во втором случае возвращает вектор, все компоненты которого равны N.
Извлечь из вектора компоненту можно с помощью функций V:x(), V:y(), V:z().

Краткая таблица наиболее часто используемых функций:
Функция
Возвращаемое значение
Описание
V:length()
N
Возвращает длину вектора V.
V:setX(N) / V:setY(N) / V:setZ(N)
-
Заменяет компоненту X/Y/Z на заданное число N
V:distance(V1)
N
Возвращает расстояние от точки, координаты которой заданы вектором V1 до точки, координаты которой заданы вектором V.
V:normalized()
V
Возвращает нормированный вектор (вектор, имеющий то же направление но единичную длину.
round(V) / ceil(V) / floor(V)
V
Округление всех компонент вектора до ближайшего целого числа / Округление вверх / Округление вниз.
V:toString()
S
Возвращает вектор в виде строки формата "[X,Y,Z]". Может пригодиться для вывода отладочной информации на экран.

Двумерный и четырёхмерный векторы
Суффикс :vector2, :vector4

Векторы, содержащие 2 и 4 компонента соответственно. Для них работают все операции и функции, что были определены выше и работают по той же аналогии (для четырёхмерного вектора четвёртая координата обозначается как w). И функции для объединения чисел называются как vec2 и vec4 соответственно и имеют ту же логику работы.

Угол
Суффикс: angle

Фактически работает как вектор, однако сам тип данных предполагает, что его компонентами являются три угла Эйлера[ru.wikipedia.org]: pitch, yaw, roll, которым соответствуют три различных поворота (анимации взяты с сайта http://wikipedia.org/):
1) Pitch:

2) Yaw:

3) Roll:

В совокупности все эти три угла задают поворот тела вокруг каждой из трёх его осей. Извлекаются компоненты из типа данных соответствующими функциями :pitch(), :yaw(), :roll() и собрать три компонента в тип данных angle можно, по аналогии с векторами, функцией ang(N,N,N). Можно преобразовать вектор к углу с помощью функции ang(V).
V. Entity
Entity
Суффикс :entity

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

Далее я вкратце перечислю наиболее часто использующиеся функции для работы с entity, остальные я планирую полностью перевести в отдельном руководстве. Также есть
официальное вики с полным списком функций (на английском языке)[github.com], которое описывает вообще все возможные функции из библиотек. Если вы владеете языком, то такой вариант даже более предпочтителен, поскольку там самый полный список доступных в Е2 функций.

Договоримся, что в описании некоторой функции аргументы будут обозначены с заглавной буквы, которая совпадает с названием типа. Если функция example получает вектор и строку, то я буду для краткости записывать этот факт как example(V, S).

Получение объектов и их свойств
Краткий список функций, получающих информацию об объектах либо извлекающих какие-то их свойства.
Функция
Описание
Возвращаемый тип
entity()
Возвращает чип
Entity
owner()
Возвращает владельца чипа
Entity
E:id()
Возвращает идентификатор объекта ((!) Обратите внимание, что функция "приписывается" справа как суффикс к объекту, тип которого необходимо извлечь)
Number
E:mass()
Возвращает массу объекта
Number
noentity()
Возвращает неверный/несуществующий объект. (С ним обычно сравнивают некоторый другой объект, чтобы проверить его существование)
Entity
E:type()
Возвращает тип объекта
String
E:model()
Возвращает путь к визуальной модели чипа
String
E:owner()
Возвращает владельца объекта E
Entity
E:pos()
Возвращает глобальные координаты объекта
Vector
E:angles()
Возвращает углы поворота объекта
Angle

Игроки
findPlayerByName(S)
Ищет игрока с именем S и возвращает его в качестве Entity (имя игрока можно и не дописывать, функция вернет игрока, если ваша строка хотя бы однозначно входит в ник одного из игроков, играющих на сервере)
Entity
E:name()
Возвращает имя игрока
String
E:steamID()
Возвращает идентификатор игрока в Steam
String
E:health()
Возвращает здоровье игрока
Number
E:lastSaid()
Возвращает последнюю сказанную фразу в чате игроком E
String
E:aimEntity()
Возвращает Entity, на который смотрит игрок E
Entity
E:aimPos()
Возвращает координаты точки, на которую смотрит игрок
Vector
E:eye()
Возвращает вектор, направленный в сторону, куда смотрит игрок E. (Для пропа будет вектор, направленный из передней части объекта)
Vector
E:key#()
Возвращает 1 если игрок нажал на кнопку # (в остальное время возвращает 0). Вместо #
используются: Attack1 - левая кнопка мыши, Attack2 - правая, Zoom - приближение, Reload -
перезарядка, Use - использование, Duck - приседание, Sprint - бег (для остальных клавиш см. вики, указанную в начале раздела). Пример использования:
if (owner():keyAttack2()) { ... }
Number

Проверки на принадлежность определенному классу
Каждая возвращает 1 в случае принадлежности и 0 в обратном случае.
E:isPlayer()
Является ли E игроком?
E:isNPC()
Является ли E NPC?
E:isWorld()
Является ли E миром?
E:isAlive()
Жив ли E?
E:isOnGround()
Находится ли E на земле?
E:isVehicle()
Является ли E транспортом?
E:inVehicle()
Находится ли E в транспорте?
E:isAdmin()
Является ли игрок E администратором?
E:isFrozen()
Заморожен ли объект?

Влияние на окружающие предметы
Каждая из этих функций имеет проявление в мире игры и не имеет возвращаемого значения.
E:applyForce(V)
Применяет силу к объекту E по направлению вектора V. Длина вектора задаёт саму величину силы
E:applyTorque(V)
Задаёт вращающий момент[ru.wikipedia.org] для объекта E.
E:setMass(N)
Устанавливает объекту массу N
E:setColor(N,N,N)/
E:setColor(N,N,N,N)/
E:setColor(V)/
E:setColor(V4)
Задаёт цвет объекта в цветовой палитре RGB, каждое число от 0 до 255. В случае появления четвертой координаты (вариант с четырьмя числами либо с четырёхмерным вектором V4) задаётся ещё и прозрачность
E:setAlpha(N)
Задаёт прозрачность объекта E. N задаётся от 0 до 255.
E:setMaterial(S)
Установить материал S для предмета E
E:setTrails(N,N,N,S,V,N)
Создаёт след, который будет оставлять объект, точно такой же, как и при инструменте Trail, однако имеет много параметров для изменения. По порядку:
1. Размер следа при создании
2. Размер следа при исчезновении
3. Длина
4. Пусть к материалу
5. Цвет
6. Прозрачность
E:removeTrails()
Убирает все следы с объекта E
VI. Управляющие функции
Запуск чипа
По умолчанию, если в директиве @trigger не указано иное, чип начинает исполнять написанную в нём программу всякий раз, когда одна из входных переменных меняет своё значение. Это неплохо в плане производительности, однако вам может больше подходить другой режим работы.
Одной из функций, которая может запустить ваш чип является interval(N). В качестве аргумента она принимает некоторое число N - количество миллисекунд, по истечению которых снова вызовется работа чипа. Рассмотрим такой пример:
@outputs Out @persist Timer interval(1000) if (first()) { Timer = 60 } else { Timer-- Out = !Timer }
Здесь встречается ещё одна важная функция - first(). Она возвращает 1 в случае, если выполнение чипа является первым. Она бывает очень полезна, если вам нужно задать что-то единожды при запуске чипа.
По задумке, нужно чтобы чип представлял из себя простой таймер. Начиная с 60, чип каждую секунду (= 1000 миллисекунд) вычитает из таймера 1, и как только Timer дойдёт до нуля (а !Timer, при этом, станет равным 1) Out присвоится значение 1. При этом, заметьте, что interval вызывает только одно выполнение чипа. В данном случае, каждый вызов программы заканчивается повторным вызовом interval что зацикливает её и делает возможным создание таймера. Конечно, это можно и контролировать, для этого можно занести функцию interval в некоторое условие, которое не позволит программе выполняться дальше:
if (Timer) { interval(1000) }
Тогда чип перестанет вычитать единицу из таймера. Как только он дойдёт до нуля вызов interval остановится и Timer не станет отрицательным, сбросив значение Out на 0.

Можно также разделять ситуации, когда чип заработал от изменения входной переменной или от вызова с помощью функции interval. Функция clk() возвращает 1, если исполнение чипа началось с помощью функции interval. Можно добавлять таймеры с определенным названием с помощью функции timer(S,N). Первый аргумент задаёт имя для таймера, а второй задержку в миллисекундах. Работает она так же, за тем исключением, что у каждого таймера есть собственное имя и работают они все одновременно. Проверить истечение какого таймера вызвало выполнение чипа можно с помощью функции clk(S), где S - имя таймера, которое вы хотите проверить. Сделано это для того, чтобы вам было легче организовывать различные повторяющиеся с разным периодом кусочки программ.

Бывают ситуации, когда вам нужно чтобы чип работал с максимальной частотой. Не рекомендую использовать в таком случае interval с очень малой задержкой. Лучше использовать функцию runOnTick(N), где N это либо 1 (включение режима) либо 0 (отключение). При включении чип станет работать по тику сервера (каждые 15 милисекунд, приблизительно 66 раз в секунду). Функция для проверки, что выполнение чипа вызвано тиком тоже присутствует - tickClk().

Самой работой чипа тоже можно управлять: reset() сбрасывает и перезапускает чип, exit() полностью останавливает его работу, selfDestruct() уничтожает чип,
а selfDestructAll уничтожает также и все объекты, соединенные с этим чипом.
VII. Массивы и циклы
Часто возникает ситуация, когда вместо одного объекта вам нужно оперировать целой группой однотипных объектов некоторого размера, причём сам размер может изменяться. Для хранения подобных групп объектов в Expression 2 используются массивы: array. Для объявления переменных этого типа в директивах пишется соответствующий суффикс.
Разберёмся, как работать с массивом на примере функции players().
Функция возвращает массив игроков, в данный момент времени играющих на сервере. Все объекты, находящиеся в массиве пронумерованы. Чтобы извлечь какой-то конкретный, нужно обратиться к нему с помощью номера. Номер, по которому идёт обращение к какому-либо объекту массива обычно называют индексом.
@persist Players:array Player:entity Players = players() Player = Players[1,entity] #извлекаем из массива Players entity, находящийся по индексу 1 print("Player " + Player:name() + " is online!") #выводим сообщение с именем игрока в чат
По той же аналогии можно извлечь объект из массива по какому угодно индексу, указывая тип объекта после запятой в квадратных скобках.
Таким же образом можно и сохранять в определенной ячейке массива информацию некоторого типа
Массив[*номер*, *тип*] = *значение* #например @persist Numbers:array Numbers[1, number] = 12 Numbers[2, number] = 39 ... и т.д.

Замечание. Указание типа при извлечении или занесении информации в опредленную ячейку предполагает, что массивы могут хранить разнородную информацию, то есть, в одной ячейке может храниться число, в другой строка, в третьей entity. Однако, не стоит этого делать за исключением крайней необходимости: при таком подходе повышается (зачастую необоснованно) сложность программы и вероятность сделать трудно обнаруживаемые ошибки.

Краткий список функций, с помощью которых можно работать с массивами
Под R подразумевается массив
Функция
Описание
Возвращаемое значение
array()
Создаёт пустой массив
Array
R:clone()
Создаёт независимую копию R.
Array
R:count()
Возвращает количество элементов в массиве
Number
R:concat()
Объединяет все значения в массиве в виде строки. Для сложных типов вроде angle, vector возникает неопределенное поведение, поэтому использовать в таком случае эту функцию не стоит.
String
S1:explode(S2)
Разбивает строку S1 по строке S2 и возвращает массив из отдельных кусков строк. Например, если S1 - некоторое предложение, то S1:explode(" ") разобьёт строку на слова и вернет в виде массива.
Array

Циклы

С появлением массивов можно столкнуться с ситуацией, когда количество обрабатываемой информации на момент написания программы неизвестно и узнается только непосредственно во время её запуска. Пропы вокруг чипа, количество игроков на сервере - вариантов масса. Подобные (да и вообще любые сопряженные с большим количеством действий) ситуации очень легко описываются с помощью цикла.
Самый простой пример цикла - while. Это цикл с предусловием. Эта конструкция является оператором и имеет свою запись:
while (*условие*) { *содержимое* }
Смысл достаточно прост. Сначала проверяется, верно ли условие или нет. Если нет, то фигурные скобки просто игнорируются и выполнение программы идёт дальше. Если верно, то их содержимое исполняется и условие проверяется снова. То есть действие повторяется до тех пор, пока условие, описанное в круглых скобках, не окажется неверным.

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

Если вам нужно выполнить действие лишь определенное число раз, то разумнее будет пользоваться циклом for - циклом со счётчиком, запись которого выглядит как:
for (*Переменная-счётчик* = *начальное значение*, *конечное значение*, *шаг*) { #третье значение "шаг" не является обязательным *содержимое* } #Пример: for (I = 2, 10, 2) { print(I) }

Работает этот цикл так. Допустим, указан какой-нибудь положительный шаг. (или не записан совсем, тогда он равен 1). Сначала переменной-счётчику присваивается значение, как в примере выше, это значение 2. Затем выполняется действие в фигурных скобках. После прохода через содержимое фигурных скобок к счётчику прибавляется шаг, а после происходит сравнение со вторым числом. Если счётчик меньше либо равен второму числу, то цикл проходит заново: к счётчику снова прибавляется шаг, и дальше снова происходит сравнение. Цикл обрывается сразу, как только счётчик становится больше второго числа. В примере выше счётчик I просто "пробегает" все чётные значения 2, 4, 6, 8, 10.
Если воспринять как это работает все ещё тяжело, давайте посмотрим ещё раз на более простом примере:

for (I = 1, 10) { print(I) }

Шаг здесь не указан, а значит он предполагается равным 1.
Как только исполнение программы доходит до круглых скобок она входит в цикл. Изначально I инициализируется единицей, а затем происходит исполнение функции print, и счётчик выводится в чат. Затем к счётчику прибавляется шаг (он становится, очевидно, равным 2), а поскольку он не равен 10 то происходит повторное исполнение и число "2" выводится в чат. И так далее до 10. На последнем шаге счётчик становится равным, 11 и на этапе сравнения программа просто выходит из цикла, print больше не выполняется, и следовательно напишутся только числа от 1 до 10.

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

Также есть два управляющих слова continue и break. При исполнении первого цикл возвращается в начало цикла, снова выполняя проверку и/или прибавку к счётчику (в зависимости от того в каком цикле вы его используете. Слово break же просто обрывает цикл и заставляет программу идти дальше.

Пример использования циклов для обхода массивов

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

Players = players() #Чтобы постоянно не вызывать функцию, сохраним игроков в переменной Counter = 0 #В этой переменной будет копиться количество администраторов for (I = 1, Players:count()) { if (Players[I, entity]:isAdmin()) { Counter++ print(Players[I, entity]:name() + " is an admin.") } else { print(Players[I, entity]:name() + " is a normal player.") } } print(Counter + " admin" + (Counter == 1? "" : "s") + " online!")

Здесь показан достаточно стандартный способ обхода массива. Как видите, независимо от количества человек в массиве, массив всё равно пройдется от начала до конца, поскольку в конечном значении в цикле будет количество элементов. На каждом шаге выполняется оператор if, который затем уже в зависимости от того, что вернет функция :isAdmin() для очередного игрока в цикле выбирает, прибавлять ли к счётчику единицу или нет.
Аналогичным образом можно пройти по любому другому массиву.
Конструкция в конце, а именно "слагаемое" ... + (Counter == 1? "" : "s") + ..., присоединяющееся к другим строкам просто добавляет в строчку "s" к слову "admin", если таких администраторов больше чем 1. Это тернарный оператор "(?:)". Если вы не читали о нём или забыли, как он работает, посмотрите подраздел "дополнительные операции" во второй главе.
Заключение
На этом руководство подходит к концу. Благодарю вас за чтение! Оставляйте ваше мнение в комментариях и предложения по добавлению дополнительных глав.

Спасибо за внимание!
31 Comments
[Jesus42] 14 Apr, 2023 @ 3:13pm 
@HellDaBoiii не помогло кста
Citizen 10 Dec, 2022 @ 10:32pm 
Полезно
Moki 25 Sep, 2021 @ 9:09pm 
Очень похоже на Arduino в мире Garry's Mod
HellDaBoiii 1 Aug, 2021 @ 7:57am 
@Raisel Попробуй так: if(owner():keyjump) { print "hi"}
DNS 28 Mar, 2021 @ 6:13am 
А где продолжение? Почти 4 года прошло, до сих пор нет никаких намёков на новые главы. Автору дизлайк
VERGIL 9 Mar, 2021 @ 4:54am 
почему я не могу сделать так,что бы овнеру при нажатии кнопки прыжок написалось ты прыгнул?
что тут не так?
if(owner()):keyjump(print("hi"))
i like big hairy oiled up men 30 May, 2020 @ 10:29am 
ГДЕ СКАЧАТЬ МОД?
СКИНЬТЕ ПЛИЗ ССЫЛКУ
дайте новый танк 13 May, 2020 @ 5:54am 
Кодеры Л0XИ
Speed Hucker 2 Nov, 2019 @ 3:44pm 
Ваай!! Да в E2 то оказывается прям как в C++, а я уж подумал мне привыкать к новому правописанию придётся... Огромное спасибо за данное руководство, мне помогло!:steamhappy:
Maxsspeaker 23 Sep, 2019 @ 6:35am 
Нечево не понятно но интересно!