#РАЗРАБОТКА: НА ПРИМЕРАХ

1 messages · Page 1 of 1 (latest)

verbal linden
#

Этот раздел — практическая база знаний по типовым задачам в кодовой базе: добавление прототипов, компонентов, визуализаторов и UI. Предполагается знакомство с C# и YAML, но не с другой документацией проекта.


#

ECS: Сущности, Компоненты, Системы

Сущность (Entity) — любой объект в игре. Игрок, банан, дубинка — всё это сущности, представленные уникальным целым числом. Сами по себе сущности не имеют никакого поведения.

Компонент (Component) — хранит данные и помечает сущность как обладающую определённым поведением. Например, NukeComponent означает, что эта сущность ведёт себя как ядерная бомба и хранит данные о таймере.

Система (EntitySystem) — содержит логику для компонентов. На весь сервер существует один NukeSystem, который обрабатывает все сущности с NukeComponent. Системы реагируют на события или обновляются каждый тик.


#

Прототипы

Прототипы — это «пресеты» сущностей, аналог префабов в Unity. Они определяют, какие компоненты есть у сущности и какие данные в них хранятся. Записываются в YAML и размещаются в Resources/Prototypes.

- 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: EmitSoundOnUse
    sound: /Audio/Items/skub.ogg
  - type: UseDelay
    delay: 2.0

Спаунить сущности в игре можно через F5 (Entity Spawn Panel).


#

Пример: Велосипедный гудок с нуля

Разбираем создание предмета, который издаёт звук при использовании.

1. Прототип

Создаём файл clown_horn.yml в Resources/Prototypes/Entities/Objects/Fun/:

- 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

2. Компонент

Создаём Content.Server/Sound/PlaySoundOnUseComponent.cs. Имя компонента в YAML генерируется автоматически — убирается суффикс Component:

namespace Content.Server.Sound;

[RegisterComponent]
public sealed partial class PlaySoundOnUseComponent : Component
{
    [DataField]
    public string Sound = string.Empty;
}

3. Система

Создаём Content.Server/Sound/PlaySoundOnUseSystem.cs:

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)
    {
        _audio.PlayPvs(ent.Comp.Sound, ent.Owner);
    }
}

PlayPvs автоматически обрабатывает дистанционную фильтрацию — игроки вне зоны слышимости не услышат звук.


#

Динамические спрайты и AppearanceComponent

Когда спрайт должен меняться в зависимости от состояния сущности (открыт/закрыт, есть предмет/нет), используется система AppearanceComponent + VisualizerSystem.

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

На сервере — устанавливаем данные:

_appearanceSystem.SetData(ent, ItemCabinetVisuals.IsOpen, ent.Comp1.Opened);

На клиенте — читаем и применяем:

public sealed class ItemCabinetSystem : VisualizerSystem<ItemCabinetVisualsComponent>
{
    protected override void OnAppearanceChange(Entity<ItemCabinetVisualsComponent> ent, ref AppearanceChangeEvent args)
    {
        if (TryComp(ent, out SpriteComponent? sprite)
            && args.Component.TryGetData(ItemCabinetVisuals.IsOpen, out bool isOpen)
            && args.Component.TryGetData(ItemCabinetVisuals.ContainsItem, out bool contains))
        {
            sprite.LayerSetState(ItemCabinetVisualLayers.Door, isOpen ? ent.Comp.OpenState : ent.Comp.ClosedState);
            sprite.LayerSetVisible(ItemCabinetVisualLayers.ContainsItem, contains);
        }
    }
}

Если логика проста — только переключение состояний спрайта — используй GenericVisualizerSystem вместо написания своего:

- type: Appearance
- type: GenericVisualizer
  visuals:
    enum.ItemCabinetVisuals.IsOpen:
      enum.ItemCabinetVisualLayers.Door:
        True: { state: open }
        False: { state: closed }

Спрайт-слои

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

- type: Sprite
  sprite: Structures/Doors/Airlocks/Standard/basic.rsi
  layers:
  - state: closed
    map: ["enum.DoorVisualLayers.Base"]
  - state: bolted_unlit
    shader: unshaded
    map: ["enum.DoorVisualLayers.BaseBolted"]
  - state: welded
    map: ["enum.DoorVisualLayers.BaseWelded"]

Слои с shader: unshaded не затрагиваются освещением — создают иллюзию собственного свечения.


#

Сетевое взаимодействие

Синхронизация компонентов

Для автоматической синхронизации полей компонента используй атрибуты:

[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class IdCardComponent : Component
{
    [DataField, AutoNetworkedField]
    public string? FullName;

    [DataField, AutoNetworkedField]
    public string? JobTitle;
}

При изменении поля вызови Dirty(uid, comp) — сервер отправит обновление клиенту. Для компонентов с множеством независимо меняющихся полей используй DirtyField вместо Dirty, чтобы слать только изменившееся поле.

Сетевые события

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

[Serializable, NetSerializable]
public sealed class BwoinkTextMessage : EntityEventArgs
{
    public NetUserId ChannelId { get; }
    public string Text { get; }
    // ...
}

Важно: всегда валидируй данные, полученные от клиента на сервере. Клиент может прислать что угодно.


#

Предсказание (Prediction)

Предсказание позволяет клиенту реагировать на ввод мгновенно, не дожидаясь ответа сервера. Делает игру отзывчивой при высоком пинге.

Чеклист для реализации предсказания:

  • Переноси компоненты и системы из Content.Server в Content.Shared
  • Добавляй [NetworkedComponent] и [AutoGenerateComponentState] к компонентам
  • Помечай изменяемые поля атрибутом [AutoNetworkedField]
  • Вызывай Dirty(ent) при каждом изменении поля
  • Используй предсказываемые методы: PlayPredicted, PopupPredicted, PredictedSpawnAtPosition
  • Не используй RobustRandom в общем коде — сервер и клиент получат разные результаты

Тестирование: команда sudo cvar net.fakelagmin 0.5 увеличивает искусственную задержку. Если предсказание работает, задержка не ощущается.


#

Локализация (Fluent)

Весь текст, видимый игроку, хранится в .ftl-файлах в Resources/Locale/<код языка>/. В коде никакого захардкоженного текста быть не должно.

Простое сообщение:

comp-stack-already-full = Stack is already full.
Loc.GetString("comp-stack-already-full")

Сообщение с переменной:

traitor-user-was-a-traitor = {$user} was a traitor.
Loc.GetString("traitor-user-was-a-traitor", ("user", "Bob"))
// → "Bob was a traitor."

Род, склонение, местоимения — через встроенные функции:

humanoid-character-profile-summary =
    This is {$ent}. {SUBJECT($ent)} {CONJUGATE-BE($ent)} {$age} years old.

Функции: SUBJECT, OBJECT, POSS-ADJ, REFLEXIVE, THE, INDEFINITE, CAPITALIZE, CONJUGATE-BE, CONJUGATE-HAVE.

Локализация прототипов — через атрибуты Fluent:

- type: entity
  id: RedOxygenTank
  # name и description не указываем в YAML
ent-RedOxygenTank = oxygen tank
    .desc = A tank of oxygen. This one is red.

Правила: отступы только пробелами (не табами), ID всегда в kebab-case, префикс по контексту (comp-stack-, traitor- и т.д.).


#

UI

Базовые правила

Всегда используй FancyWindow вместо DefaultWindow:

<ui:FancyWindow xmlns="https://spacestation14.io"
                xmlns:ui="clr-namespace:Content.Client.UserInterface">
    <!-- содержимое -->
</ui:FancyWindow>

Используй стиль-классы из StyleClass.cs вместо хардкода цветов. Несколько классов на одном элементе:

<Button Text="delete">
    <Button.StyleClasses>
        <sys:String>ButtonSmall</sys:String>
        <sys:String>negative</sys:String>
    </Button.StyleClasses>
</Button>

Для статусных цветов (красный/жёлтый/зелёный):

Palettes.Status.GetStatusColor(0.0f); // красный
Palettes.Status.GetStatusColor(0.5f); // жёлтый
Palettes.Status.GetStatusColor(1.0f); // зелёный

Кастомная кнопка с hover-эффектом

Иногда стандартных кнопок не хватает. Вот паттерн для кнопки с полностью кастомной визуализацией при наведении.

Сначала создаём класс-обёртку над Button, которая сообщает об изменении режима отрисовки:

namespace Content.Client.MyNamespace;

public sealed class DrawButton : Button
{
    public event Action? OnDrawModeChanged;

    protected override void DrawModeChanged()
    {
        OnDrawModeChanged?.Invoke();
    }
}

XAML для кнопки — невидимая DrawButton поверх PanelContainer:

<Control xmlns="https://spacestation14.io"
         xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
         xmlns:local="clr-namespace:Content.Client.MyNamespace">
    <BoxContainer Orientation="Vertical" SetSize="80 80">
        <PanelContainer VerticalExpand="True" HorizontalExpand="True" Name="Panel">
            <PanelContainer.PanelOverride>
                <graphics:StyleBoxFlat BackgroundColor="#162031"
                                       BorderColor="#4972A1"
                                       BorderThickness="2"/>
            </PanelContainer.PanelOverride>
            <local:DrawButton Name="Button"
                              Access="Public"
                              ModulateSelfOverride="#00000000"
                              HorizontalExpand="True"/>
            <!-- сюда добавляем содержимое: текст, иконку и т.д. -->
        </PanelContainer>
    </BoxContainer>
</Control>

Code-behind — логика смены цвета при наведении:

[GenerateTypedNameReferences]
public sealed partial class MyFancyButton : LayoutContainer
{
    public Action<BaseButton.ButtonEventArgs>? OnPressed;

    public static readonly Color DefaultColor = Color.FromHex("#141F2F");
    public static readonly Color DefaultBorderColor = Color.FromHex("#4972A1");
    public static readonly Color DefaultHoveredColor = Color.FromHex("#4972A1");
    private static readonly Color DisabledColor = Color.FromHex("#999999");

    public Color Color = DefaultColor;
    public Color BorderColor = DefaultBorderColor;
    public Color HoveredColor = DefaultHoveredColor;

    public MyFancyButton()
    {
        RobustXamlLoader.Load(this);
        Button.OnPressed += Pressed;
        Button.OnDrawModeChanged += UpdateColor;
        UpdateColor();
    }

    private void UpdateColor()
    {
        var panel = (StyleBoxFlat) Panel.PanelOverride!;
        panel.BackgroundColor = Button.Disabled ? DisabledColor
            : Button.IsHovered ? HoveredColor : Color;
        panel.BorderColor = BorderColor;
    }

    protected override void ExitedTree()
    {
        base.ExitedTree();
        Button.OnPressed -= Pressed;
    }

    private void Pressed(BaseButton.ButtonEventArgs args)
    {
        OnPressed?.Invoke(args);
    }
}
#

тут будет больше если @plucky violet дополнит

#

уээ

spark trellis
#

можно же просто алибаба кодинг

#

писать промпты

verbal linden