Jump to content
Valtos

[tg HackMD] Сигналы, Компоненты и Элементы

Recommended Posts

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

Терминология

DCS, Datum component system: Немного устаревший термин, но это было первоначальное название системы в целом.
Компоненты: Изолированные обладатели функциональности. Они содержат все данные и логику, необходимые для выполнения некоторого дискретного поведения.
Элементы: То же, что и компоненты, но дешевле и более ограниченны в функциональности. Компоненты часто используются в этом руководстве для обозначения обоих как группы.
Сигналы: Способ получения сообщений компонентами. Первоначально только компоненты могли принимать сигналы, но с тех пор он был расширен, поэтому любой может принимать сигнал, если он полезен.

Компоненты/Элементы

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

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

Сигналы

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

В качестве примера представьте звук шагов. В коде обуви вы можете сделать следующее:

/obj/item/shoes
    var/turf/last_noise

/obj/item/shoes/process()
    . = ..()
    var/turf/current_turf = get_turf(src)
    if(current_turf == last_noise)
        return
    last_noise = current_turf
    play_footstep()

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

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

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

/atom/movable/Moved()
    SEND_SIGNAL(src, COMSIG_MOVABLE_MOVED)

/obj/item/shoes/equipped(mob/equipper)
    RegisterSignal(equipper, COMSIG_MOVABLE_MOVED, .proc/play_footstep)

Сигналы: Как они работают

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

#define COMSIG_MOVABLE_CROSSED "movable_crossed"

/atom/movable/Crossed(atom/movable/AM, oldloc)
	SEND_SIGNAL(src, COMSIG_MOVABLE_CROSSED, AM)

Здесь у нас есть базовая процедура Crossed, настроенная так, чтобы отправлять сигнал и больше ничего не делать. src дается в качестве аргумента, так что это сигнал с самим собой в качестве источника. Далее указывается тип сигнала, он определяется в __DEFINES/components.dm, а затем передается в сигнал. Функционально это просто строка, выступающая в качестве идентификатора. Третий аргумент в сигнале - это перемещаемое движение, которое передается тому, кто получает сигнал.

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

Теперь, когда существует этот новый сигнал, мы можем сделать что-то способным его прослушивать.

/datum/component/squeak/Initialize()
	RegisterSignal(parent, COMSIG_MOVABLE_CROSSED, .proc/play_squeak_crossed)

/datum/component/squeak/proc/play_squeak_crossed(datum/source, atom/movable/AM)
	[dostuff]

Здесь, когда объект, в данном случае компонент, создается, он регистрируется для сигнала. Теперь он «слушает» этот сигнал, и каждый раз, когда сигнал будет отправлен, компонент будет знать об этом. Любые аргументы, данные в этом сигнале, передаются слушателю. Затем это вызывает процедуру, указанную при регистрации сигнала. Вы можете вызывать любую процедуру таким образом, посмотрите как работают callbacks, чтобы увидеть, как это работает.

Откуда взялись эти данные/источник? Сигналы в дополнение к своим обычным аргументам будут каждый раз передавать отправителя сигнала в качестве первого аргумента. Это полезно, когда что-то слушает один и тот же сигнал на нескольких других объектах.

Теперь у вас есть рабочий компонент, получающий сигнал от объекта, к которому он применяется, но что это за компонент?

Изоляция плохого кода с помощью компонентов

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

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

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

Другой способ взаимодействия с компонентами - процедура GetComponent(). Эта процедура - простой и понятный - костыль. Что он делает, так это позволяет вам получить определенный тип компонента, который был применен к объекту. Такой метод взаимодействия с компонентами вредит их полезности. Если у вас есть компонент, функциональность которого зависит от внешнего кода, вы снова возвращаетесь к спагетти-коду. Вы не можете узнать, видите ли вы все возможные пути, по которым может идти логика, просто взглянув на компонент. Сначала поищите другие методы и попросите о помощи, если вы все еще чувствуете, что вам действительно нужно использовать GetComponent().

Это нужно повторить: Процедура GetComponent является костылем и вредит вашему компоненту. По возможности избегайте её.

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

/type/path/item
    var/value = 0

/type/path/item/proc/appraise()
    var/datum/component/value/comp = GetComponent(/datum/component/value)
    if(comp)
        value = comp.value
    return value

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

Есть много способов сделать это правильно, и все они включают сигналы:

/type/path/item
    var/value = 0

/type/path/item/proc/appraise()
    SEND_SIGNAL(src, COMSIG_ITEM_APPRAISING)
    return value
/datum/component/value/Initialize()
    RegisterSignal(parent, COMSIG_ITEM_APPRAISING, .proc/appraising)

/datum/component/value/proc/appraising()
    source.value = value

Мы удалили GetComponent, этот объект больше не зависит от существующего компонента. Все в предмете является общим, и компонент обрабатывает исключительные случаи. Другие компоненты также могут использовать этот же сигнал, если их эффекты связаны со стоимостью предмета.

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

Оптимизация при помощи элементов

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

Разработка новых компонентов/элементов

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

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

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

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

Оригинал: https://hackmd.io/@tgstation/SignalsComponentsElements

  • +Rep 3
Link to comment
Share on other sites

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

Так вот. Их DCS - это вариация на тему ECS. Что такое ECS? Entity-component-system, паттерн проектирования, в последние 10 лет ставший очень популярным в геймдеве. Его суть заключается в том, что сами по себе объекты - энтити, сущности - ничего из себя не представляют и не содержат какой-либо код, у них есть лишь уникальный идентификатор. Компоненты - это, по сути, тег, отражающий какую-либо характеристику, особенность, что угодно. Компонент может содержать данные, но не поведение. Как же должен работать компонент, если он имеет лишь данные? Тут в дело вступают системы. Система - код, функция, которая ответственна за один или несколько компонентов, там и находится поведение, она последовательно обрабатывает все подходящие компоненты на всех сущностях по нужной логике. Собственно, одно из двух главных преимуществ ECS заключается в том, что из-за последовательной обработки одинаковых компонентов, особенностей работы памяти и процессора код выполняется быстрее. Ну и так как компоненты это простые данные, они могут быть объявлены не в коде, а в текстовых файлах, и тебе не надо будет каждый раз компилить код для изменений не относящихся к логике.

Но паттерн с компонентами необязательно должен выглядеть так, это лишь, так сказать, каноничная реализация. В том же Юнити, например, компоненты помимо данных содержат непосредственно и логику, в итоге получается уже просто Entity-Component, без систем. Ну, в общем-то, там в новых версиях завозят уже и полноценный ECS, но до этого было так.

Теперь про бьенд. Тут не получится делать просто пустые энтити и компоненты, потому что все объекты бьенда так или иначе тащат за собой какие-то дефолтные переменные и методы. От систем тоже нет особого толка, потому что плюс к производительности в бьенде не получить и их реализация выйдет кривожопой (тут, на самом деле, могу быть не прав. но хуй знает). 
Поэтому получается DC - Datum Component. Зачем и почему в оригинальном названии есть S, которая по идее обозначает системы - неизвестно.

Собственно, даже так остается другое главное преимущество компонентов - архитектурное. Классическая ООП архитектура превращает проект уровня станции в полный ебаный абсурдный пиздец говна и хуйни, тотальную катастрофу, это всем известно. Абсолютно весь код намертво связан друг с другом, чтобы поменять даже какую-то маленькую вещь тебе надо невъебенно изъебаться. А если хочешь переписать какую-нибудь медицину? Билд просто предложит тебе либо пойти нахуй, либо потратить два года своей жизни и все нервные клетки, чтобы в итоге тоже пойти нахуй. Помимо проблем с намертво связанным кодом, опять же, определение объектов через наследование - тоже хуевая штука для такой комплексной игры как СС13. У нас есть куча объектов которые могут делать кучу действий и иметь кучу состояний, но всё это определяет лишь их родитель, либо они сами. В итоге, чтобы два абсолютно разных объекта с разным деревом родителей могли иметь какое-то одинаковое поведение, приходится копипастить кучу кода, либо находить у них общего родителя и вставлять нужный код в него, что ещё хуже копипаста. Например, у нас есть мыло и клоунский ПДА - очевидно, два совершенно разных объекта, но на обоих человек должен поскальзываться. Без компонентов здесь спасет только копипаст. Когда у тебя появляется цель сделать какой-то новый объект, который должен помимо прочего объединять логику двух и больше других несвязанных объектов, всё становится ещё хуже. И, кстати, ещё одна проблема наследования, из-за которой я и сказал, что копипаст лучше кода где-то в общем родителе - часто объекты тащат за собой кучу переменных и методов, которые им достались от родителя, но совершенно не нужны, что создает лишний оверхед и занимает место в памяти.

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

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

Edited by Shweet
  • +Rep 2
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...