Это руководство по программированию в BYOND для начинающих от Kawaiidesu.

Оригинал статьи находится на нашем форуме.

Другие руководства по программированию в BYOND на русском языке вы можете найти в раздале All about the code.

Основы

ООП (Объектно-Ориентированное Программирование) - основа BYOND. Это не процедурный и далеко не императивный язык, здесь всё по большей части закручено именно на объектах, так что примем объект за низшую единицу счисления. Что же такое объект? Аирлок, плата в компьютере, "око" ИИ и т.д. Объект обладает чётко выраженными свойствами(переменные) и возможностями(методы). Но что я говорю, давайте же попробуем создать объект!

 /obj/object

Зачем же здесь obj, спросите вы? Позвольте пояснить: бьёнд обладает своей базой, базовыми объектами, которая позволяет легко и просто создавать новые объекты, производные от оных (наследование). Она называется atom (Area. Turf. Obj. Mob.) и позволяет нам воспользоваться одним из четырёх готовых объектов.

Но чем же они отличаются? Зона абсолютно уникальна для определённой точки, то есть в один тайл, мельчайшую единицу измерения пространства в бьёнде, не поставить две зоны. Турф относительно уникален - если потанцевать с бубном, можно поставить несколько в один тайл. Объект может спокойно двигаться, а моб может контролироваться игроком(хоть объекты тоже можно контролировать, но моб даёт более обширные возможности).

Я надеюсь, здесь ещё всё понятно и продолжу. Создадим что-нибудь относительно простое. Пускай это будет фонарик. Какие свойства должны быть у фонарика? Мы должны знать, включён ли он. Попробуем объявить переменную.

 /obj/flashlight
   var/on

Ах, да, табуляция - определённый промежуток от начала строки, очень важна в бьёнде. Здесь строки заканчиваются не по закрывающему символу ";" или "}", но по "\n" - то есть при переходе каретки на другую строку. В коде выше мы поставили одинарную табуляцию перед "var/on", этим мы обозначили то, что этот код принадлежит к "obj/flashlight". Можете попробовать представить код в виде иерархии - более высшему(меньше табуляции) принадлежит всё более низшее(участки кода под ним, в котором больше табуляции). Однако не переборщите с ней - если вы поставите две табуляции в коде выше, он не будет работать, поскольку структура кода будет нарушена.

К примеру мы можем объявить объект по-другому:

 /obj
   object

Всё, что по табуляции ниже /obj - принадлежит ему, но наше объявление переменной можно заменить на:

 /obj/object/flashlight/var/on

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

Но чему равна наша переменная? Пока - ничему. Абсолютно ничему. "Абсолютное ничто" в бьёнде равно значению "null". Сделаем так, чтобы фонарик был изначально выключен.

 /obj/flashlight
   var/on = 0

Как можно увидеть, мы делаем переменную равной нулю. Это значение по умолчанию, оно устанавливается при создании объекта, но может быть спокойно изменено. К тому же мы присваиваем нуль не только по велению левой пятки, но и потому что в бьёнде false, ложь, может быть выражена нулём или null'ом., а всё, что не равно этому - true, истина. Надеюсь, вы знаете логику.

Но как изменить значение? Здесь нам и понадобятся методы. Методы исполняют свой код во время вызова. В бьёнде методы делятся на "verb" - действие и "proc" - процедура, которая является функцией(функция возвращает значение, процедура - нет). Объявим небольшой метод, который будет менять значение переменной:

 /obj/flashlight
   var/on = 0

   proc/TurnOn()
     on = 1

Рассмотрим объявление метода. Оно начинается с "proc", которое говорит бьёнду о том, что мы создаём новый метод, а не переопределяем старый (далее). Дальше после слеша мы видим название метода, а после пустые скобки. Зачем они? В них можно объявить переменные - входные данные, которые могут использоваться в самом методе, в нашем же случае переменных нет, но скобки всё равно требуются, иначе бы метод был бы слишком похож на переменную. Далее мы можем увидеть табуляцию, которая показывает, что строка принадлежит к данному методу и будет исполняться при вызове метода.

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

 /obj/flashlight
   var/on = 0
   var/charge = 1000

   proc/Switch()
     on = !on

   proc/Use(var/t)
     charge -= t

Теперь не обязательно указывать это значение, если оно не будет указано, то автоматически заменится на это, НО такие переменные должны быть указаны только после переменных без стандартного значения, то есть можно сделать "(var/t, var/k = 1)", но нельзя "(var/k = 1, var/t)". Будьте осторожны.

Кстати, если вы внимательны, то могли заметить, что я изменил первый метод на "Switch()". Внутри нашей переменной, отвечающей за состояние фонаря присваивается та же переменная, но отрицательная, НЕ-переменная. Как бы это не звучало, но работает это так - если в переменной стоит, допустим, 143, то мы получим 0, если стоит 0, то получим 1. Это очень удобно для проверок. Получается, что мы присваиваем переменной "on", которая равна 0, "!on", которая становится единицей. А если вызовем повторно, то уже "on", которая равна 1 "!on", которая равна нулю. Такие дела.

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

proc/Use(var/t = 3)
  charge = max(0, charge - t)

Я воспользовался системным методом, который определяет максимальное значение среди всех введённых и возвращает его. Таким образом мы проверяем, меньше ли "charge - t" нуля или нет. Если меньше - вернётся 0, иначе "charge - t".Таким образом заряд никогда не упадёт меньше нуля. Но если заряд равен нулю, то нам нужно выключить фонарь! Но даже это мы можем сделать, мы же кодеры! Добавим проверку:

proc/Use(var/t = 3)
  charge = max(0, charge - t)
  if(!charge)
    Switch()

Что-то новое! "if" это проверка, если в скобках будет истина - код под ней исполнится, обратите внимание на табуляцию. В данном случае мы можем увидеть в скобках "charge". Как я уже говорил - 0 это ложь, не 0 это истина, то есть если в коде выше заряду присвоится значение 0, то исполнится код. Отлично! Мы уже близки к тому, чтобы сделать фонарь идеальным. Стоп! А если сработает какое нибудь ЕМП и заряд потратится, пока фонарь выключен? Тогда он при заряде в нуль возьмёт и включится, а это не очень хорошо. Добавим ещё одну проверку и получим:

proc/Use(var/t = 3)
  charge = max(0, charge - t)
  if(!charge && on)
    Switch()

"on" это понятно, но что значит "&&" ? Это логический оператор И. Можно его представить как булевый (булевые значения это истина или ложь - основа компьютерной логики) знак умножения. Если мы умножим 0 на 1, получим 0, если 1 на 0, тоже 0, а если 1 на 1, то 1. То есть "Switch()" сработает только если заряд будет равен нулю И фонарь будет включён. Также есть "||" - оператор сложения. 1 и 1 будет 1, 1 и 0 будет 1 и лишь 0 и 0 вернут 0. Осталось закрыть последнюю дыру - фонарь можно включить, когда заряда нет. Но с вашими новыми знаниями это очень легко:

proc/Switch()
  if(!charge && !on)
    return
  on = !on

Что за return? Это оператор возврата значения. Если его прописать без значения - как в нашем случае, он возвращает null, хотя это мы обсудим в другом уроке. В данном случае return работает как прерыватель исполнения кода. То есть после выполнения return, исполнение данного метода дальше не идёт.

Наследование

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

Что такое метод, мы видели в предыдущем параграфе, но у него ещё множество интересных возможностей. Вспомните фонарь, он нам ещё сослужит службу. На данный момент он выглядит так:

/obj/flashlight
  var/on = 0
  var/charge = 1000

  proc/Switch()
    if(!charge && !on)
      return
    on = !on
 
  proc/Use(var/t = 3)
    charge = max(0, charge - t)
    if(!charge)
      Switch()

Сделаем для фонаря лампочку. Конечно, столь малый код проще сделать прямо в фонаре, но если мы собираемся использовать лампочку где-то ещё или просто хотим отделить одно от другого, то лучше создать новый объект. Лампочка будет обладать параметром "glower", который отвечает за состояние нити накаливания и "glass" за состояние стекла лампы. Конечно, вообще такие сложности ни к чему, но ради примера можно.

/obj/bulb
  //Status
  var/glower = 0
  var/glass = 0

Иногда метод гораздо удобнее, чем проверка каждой переменной, поэтому сделаем такой для проверки:

  proc/Broken()
    if(glower || glass)
      return 1
    return 0
  
  proc/AnotherBroken()
    return glower && glass

Здесь можно увидеть два примера - в первом переменные "складываются", во втором "умножаются". Любой из них будет работать одинаково хорошо, но я возьму первый. Как ранее говорилось, "return" отвечает за то, что возвращает метод - в нашем случае он отвечает, сломана ли лампочка. Сделаем эту лампочку тёплой и ламповой, пускай она греет людям сердца :3

  proc/Emit(var/r)
    if(Broken()) return
    for(var/mob/m in range(r)) m << "You feel some heat."

С первой строкой, я думаю, всё понятно, но во второй "return" на той же строке, что и "if", разве так можно? Да, если после системного оператора(for, if, while и прочее) вам хватает лишь одной строки, то можно поставить код прямо на той же строке, точно также мы можем написать "for(var/mob/m in range®) m << "You feel some heat."" в одну строку. Но не переусердствуйте с этим, обычно так лаконично смотрится лишь простой код.

Следующая строка с for это цикл, довольно сложная строчка, я скажу, поэтому разберём её по кусочкам с конца: range(n) это системный метод бьёнда, который возвращает связный список "list" (далее). "in" это также системный оператор, но немного другой. Он сверяет список справа с условием слева и возвращает уже фильтрованный список. Вообще данная строка читается как "Для (for) всех объектов типа /mob(var/mob/m) в (in) радиусе 3 (range(3))". Внимательные уже заметили "var" и, возможно, поняли, что мы создали переменную в цикле. Но чего мы этим добиваемся? Для этого надо рассмотреть работу "for" - он начинает листать список: берёт первый элемент списка, присваивает его укзанной и созданной специально для этого переменной, указанной в самом "for'е", исполняет код, указанный ниже него, потом повторяет, пока не кончатся объекты или не встретится оператор "break", который как "return" прерывает исполнение, но не метода, а конкретного цикла. Это самый сложный к описанию элемент и если вы его поняли - вы поймёте и дальше.

Переменной m будет присваиваться моб. "<<" - оператор вывода. В контексте бьёнда оператор вывода посылает файл, сообщение или что-то ещё прямо на текстовое поле всех клиентов, подключённых к данному объекту. К примеру, если двое человек управляют мобом, то им обоим пришлётся сообщение, указанное справа.

Теперь наша тёплая, ламповая лампочка светит тёплым, ламповым светом, но ведь её ещё нужно засунуть в фонарик, не так ли? Для этого мы будем использовать ссылку ("reference"/"ref") на объект. Ссылки создаются точно также, как и переменные. Так как в бьёнде используются нетипизированные переменные, то есть не важно, будет ли это строка, объект, число, то всё сокращается до лаконичного:

/obj/flashlight
  var/on = 0
  var/charge = 1000
  var/light ..

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

Оператор создания нового объекта в бьёнде это "new()". Зачем же скобки? В них мы можем указать нужные для создания переменные, к примеру для всех стандартных классов бьёнда нужно указывать место, где появляется объект.

  New()
    ..()
    light = new /obj/bulb(src)
    l2 = light
    l3 = light

Здесь light, l2 и l3 будут полностью идентичны, они ссылаются на один объект. К тому же здесь мы впервые используем переопределение метода, но об этом позже. Далее, после new вы можете увидеть путь к объекту, я думаю, это очевидно, а в скобках "src". "src" это ссылка на объект, в котором выполняется метод, то есть мы создали новый объект типа /obj/bulb в фонаре, в котором и присутствует данный код. Вызов методов, как многие уже знают, осуществляется через "ProcName(args)", но как нам вызвать его у лампочки? Для этого существует оператор доступа ".", который позволяет нам получить доступ к переменным, ссылкам и методам объекта.

  proc/Use(var/t = 3)
    charge = max(0, charge - t)
    
    light.Emit(3)
    
    if(!charge)
      Switch()

Но тут возникает главная проблема - бьёнд не видит данного метода, так как мы не сообщили ему, на класс какого типа ссылается объект. Исправим это:

/obj/flashlight
  var/on = 0
  var/charge = 1000
  var/obj/bulb/light

Здесь мы видим, что переменная обзавелась путём, типом объекта в середине. Почему именно так? Скорее всего потому что это действительно удобный способ, отличающийся полнотой информации и отсутствием необходимости запоминать множество классов, а так же оставляющий возможность повторяемости названий. Вообщем удобно. Соберём всё вместе: слева, в самом начале, мы указываем, что это переменная("var"), дальше мы сообщаем тип объекта. Если мы этого не сделаем - бьёнд будет обращаться с ней, как с универсальной переменной и не даст нам доступа к методам и переменным. Закрывается же всё названием переменной. Также существует оператор "мягкого" доступа, который не выдаёт ошибку, если метод отсутствует, но реальная необходимость возникает редко, а глупо используют часто, поэтому я лишь сказал про наличие такой возможности.

Наследование классов и переопределение методов, это базис ООП, который позволяет слегка подправивить или полностью изменить объект, добавить новый функционал или изменить старый. Вы уже видели первый пример переопределения с системным методом New(), которая есть у абсолютно всех классов, но я приведу и другой пример. А вот текущей листинг кода.

/obj/flashlight
  var/on = 0
  var/charge = 1000
  var/obj/bulb/light

  New()
    ..()
    light = new /obj/bulb(src)

  proc/Switch()
    if(!charge && !on)
      return
    on = !on

  proc/Use(var/t = 3)
    charge = max(0, charge - t)    
    light.Emit(3)   
    if(!charge)
      Switch()

/obj/bulb
  var/glower = 0
  var/glass = 0

  proc/Broken()
    if(glower || glass)
      return 1
    return 0

  proc/Emit(var/r)
    if(Broken()) return
    for(var/mob/m in range(r)) m << "You feel some heat."

Допустим, нам понадобился фонарик с двумя лампочками, но нам нужен и старый с одной лампочкой. Что нам поможет? Наследование! Совершим наследование от фонаря и добавим новую переменную!

/obj/flashlight/powerful
  var/obj/bulb/anotherLight
  
  New()
    ..()
    anotherLight = new /obj/bulb(src)

Что мы здесь видим? Во-первых, изменился путь/тип, но если быть точнее, то он не изменился, а расширился. Остался старый "/obj/flashlight", но к нему добавился "/powerful". Что это даёт? У данного класса остаются все старые переменные, ссылки (далее просто переменные) и методы, но мы можем добавить новые, не затрагивая старых. Что мы и делаем в следующей строке, объявляя новую лампочку уже известным способом. Ещё можно заметить, что мы снова переопределяем "New()", добавляя в него создание новой лампочки и странный метод "..()". Этот метод отвечает за вызов аналогичного метода, но в интерпретации класса-родителя. Коротко говоря, он вызывает "New()" класса /obj/flashlight. Зачем нам это? Чтобы не переписывать создание первой лампочки, разумеется.

Но что это за сильный фонарь, если он греет так же недалеко? Исправляем!

  Use(var/t = 6)
    charge = max(0, charge - t)    
    light.Emit(8)   
    if(!charge)
      Switch()

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

/mob/flashlighter_monster
  name = "FLASHLIGHTEEEEER!!!!!!"

  var/obj/flashlight/right_flashlight
  var/obj/flashlight/left_flashlight
  
  New()
    ..()
    right_flashlight = new /obj/flashlight(src)
    left_flashlight = new /obj/flashlight/powerful(src)
 
  verb/Flashlight()
    right_flashlight.Use()
    left_flashlight.Use()
/obj/flashlight
  var/on = 0
  var/charge = 1000
  var/obj/bulb/light

  New()
    ..()
    light = new /obj/bulb(src)

  proc/Switch()
    if(!charge && !on)
      return
    on = !on

  proc/Use(var/t = 3)
    charge = max(0, charge - t)    
    light.Emit(3)   
    if(!charge)
      Switch()
/obj/flashlight/powerful
  var/obj/bulb/anotherLight
  
  New()
    ..()
    anotherLight = new /obj/bulb(src)
  Use(var/t = 6)
    charge = max(0, charge - t)    
    light.Emit(8)   
    if(!charge)
      Switch()

Создадим своего первого моба - монстра-флешлайтера, мухахахаха! У него: переопределена системная переменная name, если быть точнее - изменено её значение по умолчанию, а двум фонарям-рукам присвоены фонари - правый - обычный, левый- сильный. Хотя обе переменных были объявлены как "/obj/flashlight", так как метод "Use()" переопределён, то у сильного фонаря будет вызван именно "/obj/flashlight/powerful/Use()", а не "/obj/flashlight/Use()". Если сказать по простому - он будет использовать именно ту версию метода, которая заключена в классе в ссылке, а не в каркасе класса в переменной.

И самое главное - "verb"! Это действие, которое может вызвать у объекта, у моба, сам моб, условия изменяемы. Пока что достаточно лишь знать, что это метод, который может быть вызван пользователем вручную, как, к примеру, вращение стула.

К переменным доступ происходит точно так же, как и к методам, лишь не надо ставить скобки. К примеру

  verb/Flashlight()
    right_flashlight.Use()
    left_flashlight.Use()
    if(prob(1))
      right_flashlight.light.glower = 1

Здесь мы использовали системный метод prob(x), который выдаёт 1 с вероятностью в то значение, которое вы вобьёте, в процентах. То есть если написать prob(100), всегда будет 1, если prob(0), всегда 0, а если prob(50), то 50 на 50. Далее мы обращаемся через ссылку на ссылку и уже к переменной, присваивая ей значение 1. Сломалась лампочка, такие дела.

Списки

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

Теория-теория~~~~~

Лист может хранить в себе числа, строки, ссылки на объекты и много другое. Попробуем сделать свой первый лист.

/obj/tutorial_item
  var/list/test_list = list()

Один важный ньюанс: лист является не переменной, а объектом, следовательно его необходимо создать. Есть много способов сделать это, но list() я нахожу самым удобным.

Вот мы и создали лист, воспользуемся же им.

/obj/tutorial_item
  var/list/test_list = list()

  proc/AddItem()
    test_list += 1
    test_list.Add(1)

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

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

  proc/RemoveItem()
    test_list -= 1

Здесь же мы убрали один элемент из списка, получив list(1). Заметьте, пропали не оба, а лишь один элемент, который стоял в начале списка.

Точно также можно работать и с обычными объектами:

/obj/tutorial_item
  var/list/test_list = list()
  var/obj/item

  New()
    ..()
    item = new /obj(usr)

  proc/AddItem()
    test_item += item

  proc/RemoveItem()
    test_item -= item

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

Перейдём к чему-то посложнее.

Бинарные операции с листами

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

  proc/AddItem()
    test_item |= item
    test_item |= item

  proc/RemoveItem()
    test_item &= !item

В чём их соль? Они позволяют гарантировано держать в листе лишь один экземпляр объекта, то есть в листе этот объект будет фигурировать лишь однажды. Логика их проста:

  • сложение это создать лист из текущего листа ИЛИ этого объекта.
  • вычитание это создать лист из текущего листа И всех объектов кроме данного.

Я уверен, большинство тут знает конъюнкцию, дизъюнкцию и прочие страшные слова.

  var/list/a = list(1, 2, 2)
  var/list/b = list(2, 3)

  var/list/sum = a + b   // 1, 2, 2, 2, 3
  var/list/sub = a - b   // 1, 2
  var/list/binary_sum = a | b   // 1, 2, 2, 3
  var/list/binary_sub = a & b   // 2

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

Почему же в бинарном сложении осталось две двойки? Ответ прост: новый лист записывается бинарным сложением каждого из элементов второго листа к первому листу. То есть вышеописанная операция может выглядеть как:

  var/list/binary_sum = a
  a |= 2
  a |= 3

С вычитанием всё немного по-другому. Сравниваются все элементы между листами и все повторяющиеся элементы записываются.

Что же, с математикой мы закончили, перейдём к кое-чему другому.

Во-первых, к листу можно обращаться как к массиву:

  var/list/a = list()
  a["first"] = 1
  a["second"] = 2
  a[/mob] = 3
  //a[-1] = 4 NOWAY

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

В-третьих, лист обладает длиной, что позволяет обрабатывать его как обычный массив:

  var/list/a = list(1, 3, 4)
  var/sum = 0
  for(var/i = 1; i <= a.len; i++)
    sum += a[i] //8