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

Entities, components и systems

Хотя Space Station 14 написан на C#, объектно-ориентированном языке программирования, он использует другую модель данных для представления предметов в игре. Эта модель данных называется entity component system (ECS). (Почему мы это делаем? См. ECS)

Entities

Каждый предмет в игре представлен entity. Игроки, бананы, дубинки — всё это представлено entity. Entity представлено целым числом. Никакие два entity не имеют одинакового целочисленного представления. Сами по себе entity только отличают один предмет от другого. Без компонентов entity не имеет поведения.

Components

Components имеют две основные функции:
  1. Помечать определённые entity как имеющие определённое поведение. Например, в одной конкретной игре entity, представленная целым числом 37629, содержит NukeComponent и ActivatableUIComponent. Это означает, что эта entity ведёт себя как ядерная бомба, а также имеет пользовательский интерфейс, который можно вызвать, активировав её.
  2. Хранить данные, необходимые для обработки её поведения. Например, NukeComponent может иметь поле данных Timer, которое представляет, сколько времени осталось до детонации бомбы.
Тем не менее, компоненты не содержат никакой логики для обработки этого поведения. Поведение реализуется в entity systems.

Systems

Entity system (часто сокращённо «система») содержит логику, реализующую поведения для определённых компонентов. В то время как в одной игре может быть несколько entity с NukeComponent, существует только один NukeSystem. Этот единственный NukeSystem отвечает за обработку всех entity с NukeComponent. Entity systems реализуют поведение, определяя обработчики событий (event handlers) или реализуя update метод, вызываемый каждый тик. В качестве другого примера рассмотрим FoodComponent. Программист может создать EatingSystem для обработки поедания пищи. EatingSystem слушает событие OnUseInHand — всякий раз, когда OnUseInHand срабатывает, EatingSystem проверяет, есть ли FoodComponent в объекте, который был использован. Если есть, то он уменьшает значение nutritionLeft и воспроизводит звук чавканья. Вот суть ECS. Если вы хотите узнать больше об этом, загляните в Your mind on ECS. Подход ECS действительно мощный и позволяет нам избегать спагетти-кода, несмотря на сложность SS14.
Вам не обязательно идеально понимать архитектуру ECS с самого начала. Она может пугать как новых программистов, так и тех, кто привык к традиционному ООП. Однако общее «ощущение» и преимущества архитектуры должны проясниться по мере её использования.

Как мне создать Entity и дать ей Components?

SS14 использует систему, которую мы называем prototypes. Это, по сути, «пресеты entity». Они похожи на prefabs в Unity или подтип /obj или /mob в BYOND. Прототипы entity определяют, какие компоненты находятся на entity и какие данные эти компоненты содержат. Они также определяют базовые данные, такие как имя entity, описание и ID прототипа (используется для спавна). Пример показан ниже:
- type: entity
  parent: BaseItem
  id: Skub
  name: skub
  description: Skub is the fifth Chaos God.
  components:
  - type: Sprite
    sprite: Objects/Misc/skub.rsi
    state: icon
  - type: Item
  - type: ItemCooldown
  - type: EmitSoundOnUse
    sound: /Audio/Items/skub.ogg
  - type: UseDelay
    delay: 2.0
Это написано на YAML, языке данных, похожем на JSON, и находится в папке Resources/Prototypes/Entities/Objects/Fun/skub.yml. Все прототипы должны находиться в папке Resources/Prototypes и быть организованы в соответствующую папку. Если вам нужно больше информации о YAML, посмотрите YAML Crash Course и Serialization. Показанный прототип entity — это «Skub», который выглядит в игре так: skubexample.png Как видно из YAML, у него много компонентов, включая EmitSoundOnUse и ItemCooldown. Задача кодеров — определить, какие данные содержат компоненты и как системы придают им поведение. Чтобы заспавнить предметы в игре из прототипа, вы можете нажать F5, чтобы открыть Entity Spawn Panel. Также есть способ спавнить прототипы в коде.

Итак, я хочу бибикать!

Ваша цель — сделать Clown Horn, который бибикает при использовании. Для этого нам нужен компонент на entity со звуком, который нужно воспроизвести, и система, которая воспроизводит этот звук после использования в руке (click, или активация с Z).
Обычно вам нужно поискать в кодовой базе и спросить других кодеров, существует ли уже компонент/система, которая это делает. В данном случае
EmitSoundOnUse действительно существует в основной кодовой базе SS14. Но для целей этого руководства мы представим, что его нет, и попробуем реализовать его сами! Для начала давайте создадим простой прототип клоунского клаксона. Я создам новый файл с именем clown_horn.yml и добавлю его в папку Resources\Prototypes\Entities\Objects. clownhornexample1.png Возможно, стоит организовать его в папку “Fun” позже, но организация зависит от вас и вашей кодовой базы! Теперь заполним прототип базовым клоунским клаксоном. Поскольку у нас ещё нет специального редактора прототипов SS14, многие обычно копируют похожий прототип и изменяют его под свои нужды.
- type: entity
  name: clown horn
  parent: BaseItem
  id: ClownHorn
  description: It goes honk honk!
  components:
  - type: Sprite
    sprite: Objects/Fun/bikehorn.rsi
    state: icon
Здесь у нас есть базовая entity с одним компонентом: SpriteComponent. Посмотрите спецификацию RSI, если вы не знакомы с системой RSI, но суть в том, что у нас есть два поля для SpriteComponent: путь к RSI относительно Resources/Textures (в данном случае папка называется bikehorn.rsi) и состояние иконки. Стоит отметить, что прототипы поддерживают наследование (parenting). В данном случае BaseItem является нашим родителем и содержит множество компонентов, универсальных для всех предметов. Таким образом, наш клоунский клаксон также будет иметь эти компоненты: базовые компоненты, такие как Item, Pullable и Physics. Родители не обязательны, но они полезны в определённых случаях, как здесь. Теперь давайте скомпилируем и проверим наш предмет в игре: clownhornexample2.png Он, безусловно, красив, но, похоже, мы солгали! Клаксон пока не работает. Чтобы это исправить, нам нужно создать новый компонент для хранения данных, таких как звук для воспроизведения, и EntitySystem, который будет обрабатывать фактическое воспроизведение звука.

Создание нашего компонента

Чтобы сделать наш компонент, нам нужно создать новый класс, назовём его PlaySoundOnUseComponent. Но погодите-ка… componentcreation.png Куда его поместить? Чтобы ответить на этот вопрос, нужно мыслить шире. Нам нужно подумать о клиенте и сервере.

Парадигма клиент-сервер

Если вы ещё не читали Codebase Organization, возможно, стоит прочитать. Но для этого руководства нужно понять всего две вещи:
  • СЕРВЕР и КЛИЕНТ выполняются РАЗДЕЛЬНО.
  • Сервер должен обрабатывать большую часть логики для предотвращения эксплойтов. Всё, что находится на клиенте, может быть изменено злоумышленником.
С учётом этого логика для нашего клаксона должна выглядеть так:
  • Клиент отправляет «Я использую этот предмет» на сервер.
  • Сервер получает это, проверяет, имеет ли это смысл, и отправляет «воспроизвести бибик» всем клиентам в радиусе.
  • Клиент получает это и воспроизводит «бибик».
Это звучит довольно сложно для реализации с нуля. К счастью, у нас есть готовый код, который помогает! А именно, событие UseInHandEvent, которое вызывается на сервере при использовании предмета, и функция SoundSystem.Play(), которая воспроизводит звук для клиентов в радиусе. Эти помощники можно рассматривать как обработку клиентский клик -> сервер и сервер -> клиентский звук за нас. Таким образом, всё, что нам нужно, — это компонент на сервере, который связывает одно с другим.

Базовая реализация компонента

В кодовой базе Space Station 14 Components и EntitySystems (а также другие классы) находятся в папках непосредственно внутри проектов Content.Server, Content.Shared или Content.Client. Существуют папки для Atmos, Botany, Research, Storage и многих других. Если подходящей папки не существует, создайте её! Никогда не помещайте файлы непосредственно в корневую директорию проекта.
В проекте Content.Server есть папка Sound. В этой папке находится папка Components. Это кажется хорошим местом для нашего нового компонента (и на самом деле, именно здесь находится настоящий EmitSoundOnTriggerComponent). Давайте назовём нашу версию PlaySoundOnUseComponent. Примечание: если вы просто скопируете этот код, он может не сработать, так как вам нужно импортировать различные классы. Ваша IDE может сделать это за вас. Теперь создадим самый простой компонент:
// Content.Server/Sound/PlaySoundOnUseComponent.cs

namespace Content.Server.Sound;

[RegisterComponent]
public sealed partial class PlaySoundOnUseComponent : Component
{
}
Все компоненты должны наследоваться от класса Component. Если вы хотите, чтобы ваш компонент считывался из YAML, вам нужно добавить [RegisterComponent] над классом. Кроме того, все компоненты должны быть помечены как sealed и partial по причинам, связанным с движком. Вам не нужно слишком беспокоиться о том, что это значит. В нашем прототипе выше вы можете вспомнить, что мы добавили Sprite, а не SpriteComponent к прототипу ClownHorn. Это потому, что «имена» компонентов генерируются автоматически из имени класса. В данном случае имя нашего компонента — PlaySoundOnUse, которое генерируется простым удалением Component из имени класса. Теперь давайте добавим PlaySoundOnUse в наш прототип.
Вы должны удалить часть Component из суффикса класса при использовании в yaml прототипа. Таким образом, PlaySoundOnUseComponent будет разрешён как PlaySoundOnUse в списке components: в yaml-определении.
- type: entity
  name: clown horn
  parent: BaseItem
  id: ClownHorn
  description: It goes honk honk!
  components:
  - type: Sprite
    sprite: Objects/Fun/bikehorn.rsi
    state: icon
  - type: PlaySoundOnUse
Что ж, это скучно; наш компонент не только не имеет данных, но и ничего не делает! Давайте добавим немного данных в наш компонент. Как вы, возможно, заметили выше, у компонента Sprite на нашем клаксоне есть два перечисленных поля: sprite и state. Всё, что вы поместите в эти поля, будет передано в компонент при его создании, и затем наш EntitySystem сможет использовать эти данные для выполнения каких-либо действий. В нашем случае нам, вероятно, понадобится поле с именем sound в нашем компоненте, которое будет хранить путь к звуку для воспроизведения при активации entity. Это довольно легко сделать:
// Content.Server/Sound/PlaySoundOnUseComponent.cs

namespace Content.Server.Sound;

[RegisterComponent]
public sealed partial class PlaySoundOnUseComponent : Component
{
    [DataField]
    public string Sound = string.Empty;
}
Всё, что вам нужно сделать, чтобы создать поле, которое можно изменять в YAML, — это добавить атрибут [DataField], который содержит имя поля, и задать ему значение по умолчанию, в данном случае string.Empty. Теперь мы можем добавить наш звук в прототип клаксона:
- type: entity
  name: clown horn
  parent: BaseItem
  id: ClownHorn
  description: It goes honk honk!
  components:
  - type: Sprite
    sprite: Objects/Fun/bikehorn.rsi
    state: icon
  - type: PlaySoundOnUse
    sound: /Audio/Items/bikehorn.ogg
Теперь мы продвигаемся! Стоит отметить, что путь здесь относителен к директории Resources (которую SoundSystem всегда предполагает), и мы также предполагаем, что файл Resources/Audio/Items/bikehorn.ogg существует. Если проверить, он есть! Но если нужного звука нет, вы всегда можете добавить его самостоятельно где-нибудь в папке Audio.

Создание нашего EntitySystem

Давайте наконец добавим изюминку нашему клаксону, заставив его… на самом деле бибикать. Как было сказано ранее, нам понадобится EntitySystem, который подключается к событию UseInHandEvent и вызывает оттуда некоторый код. Давайте создадим наш EntitySystem PlaySoundOnUseSystem в той же папке Content.Server/Sound:
// Content.Server/Sound/PlaySoundOnUseSystem.cs

namespace Content.Server.Sound;
    
public sealed class PlaySoundOnUseSystem : EntitySystem
{

}
Вы заметите, что здесь наша система наследуется от EntitySystem. Это автоматически регистрирует её как полноценный EntitySystem в игре и позволяет нам использовать некоторые полезные зависимости и переопределять некоторые методы для добавления поведения. Чтобы подписаться на вызываемое событие, нам нужно переопределить метод Initialize системы; этот метод вызывается при создании EntitySystem. В этом методе мы добавим вызов SubscribeLocalEvent, и я объясню детали после.
// Content.Server/Sound/PlaySoundOnUseSystem.cs

namespace Content.Server.Sound;

public sealed class PlaySoundOnUseSystem : EntitySystem
{
    public override void Initialize()
    {
        SubscribeLocalEvent<PlaySoundOnUseComponent, UseInHandEvent>(OnUseInHand);
    }
}
В этом вызове метода много всего происходит! По сути, мы говорим игре: «Всякий раз, когда событие UseInHandEvent вызывается на entity, у которой есть компонент PlaySoundOnUse, я хочу, чтобы ты вызвал мой метод OnUseInHand». Вы, вероятно, заметили, что этот код на самом деле выдаёт ошибку, поскольку метод OnUseInHand ещё не существует! Давайте добавим этот метод. Это называется обработчиком события (event handler), и обработчики событий требуют определённого набора аргументов:
  • UID (уникальный идентификатор) entity, на которой было вызвано событие
  • Компонент, указанный в подписке, чтобы вы могли получить доступ к его данным и использовать их для изменения поведения
  • Само событие, которое содержит полезные данные, такие как entity, которая активировала предмет.
Если вы используете IDE, она может позволить вам автоматически создать этот метод с помощью Alt+Enter. Вот как будет выглядеть наш класс сейчас, с нашим новым методом:
namespace Content.Server.Sound;

public sealed class PlaySoundOnUseSystem : EntitySystem
{
    [Dependency] private readonly SharedAudioSystem _audio = default!;
    
    public override void Initialize()
    {
        SubscribeLocalEvent<PlaySoundOnUseComponent, UseInHandEvent>(OnUseInHand);
    }

    private void OnUseInHand(Entity<PlaySoundOnUseComponent> ent, ref UseInHandEvent args)
    {

    }
}

Мы почти у цели. Теперь метод OnUseInHand будет вызываться, когда мы активируем предмет, и мы сможем воспроизвести там наш звук. Кроме того, мы добавили [Dependency] private readonly SharedAudioSystem в класс. Это позволит нам воспроизводить аудио современным способом (вместо использования устаревшего SoundSystem.Play) в дальнейшем.
private void OnUseInHand(Entity<PlaySoundOnUseComponent> ent, ref UseInHandEvent args)
{
    _audio.PlayPvs(ent.Comp.Sound, ent.Owner);
}
Метод PlayPvs полезен для воспроизведения звуков. Он имеет два аргумента:
  1. Звук для воспроизведения.
В данном случае мы просто передаём наше поле sound из нашего PlaySoundOnUseComponent.
  1. Исходная entity
Это необязательный аргумент, используемый для позиционного аудио. В нашем случае мы хотим, чтобы звук исходил от клаксона, поэтому мы передаём UID клаксона (который является свойством Owner entity). Если этот аргумент не указан, звук воспроизводится глобально и будет слышен всем игрокам. Если вы скомпилируете игру и заспавните наш клаксон с помощью меню F5 Entity Spawn Menu, вы можете попробовать активировать его в руке и — невероятно! Он правильно воспроизводит звук! Надеюсь! Если нет, возможно, вы что-то напутали в YAML или пропустили метод в EntitySystem. Кроме того, PlayPvs автоматически управляет фильтрацией по расстоянию, так что вам не нужно об этом беспокоиться.

На этом всё

С этим руководство завершено! Если вы хотите продолжить экспериментировать с вашим новым клаксоном, вот несколько идей:
  • Попробуйте реализовать клаксон с использованием существующих компонентов. Вы можете обратиться к skub.yml выше на этой странице.
  • Добавьте задержку нажатия, добавив ItemCooldown в ваш прототип и вызывая RefreshItemCooldownEvent.
  • Настройте громкость/вариативность воспроизводимого звука (см. аргумент audioParams функции PlayPvs()).
  • Сделайте так, чтобы звук также воспроизводился, когда на клаксон наступают.
    • Это довольно сложно и включает добавление большого количества новых данных! Посмотрите на стекло (glass shards) в качестве примера.
  • Сделайте так, чтобы клаксон наносил урон при атаке с помощью MeleeWeaponComponent.
  • Сделайте клаксон съедобным с помощью FoodComponent и SolutionContainerComponent.
  • Добавьте поддержку воспроизведения случайного звука из SoundCollection или SoundSpecifier вместо одного звука (настоящий EmitSoundOnUse делает это, если вам нужны подсказки).
  • Погрузитесь в код взрывов и дайте ему 5% шанс взорваться при каждом бибикании!
Мир — это ваш donk packet, и у вас есть раскалённая сковорода, чтобы его приготовить!
Последнее изменение 21 июня 2026 г.