Как сделать спрайт динамическим

Большинство примеров здесь будут посвящены шкафчикам для предметов, таким как огнетушители и отсеки для пожарных топоров. Они очень просты, но достаточно сложны, чтобы требовать собственного визуализатора и понимания слоёв спрайтов. Соответствующие классы здесь — ItemCabinetComponent, ItemCabinetSystem (клиентская и серверная версии), ItemCabinetVisuals (shared) и ItemCabinetVisualsComponent.

Зададим вопрос: какой самый эффективный способ обновить спрайт сущности? Если бы сервер отправлял всё состояние SpriteComponent (компонента, используемого для отрисовки сущностей), это была бы масса данных для отправки каждому клиенту! К тому же забота о том, как отрисовываются сущности, не совсем задача сервера. А что если бы сервер просто отправлял базовые «данные внешнего вида» сущности (открыта ли она, заперта ли, запитывается ли, какого цвета её растворы и т. д.), а задача клиента состояла бы в восстановлении того, как она должна выглядеть?

Как мы этого добиваемся? С помощью AppearanceComponent и VisualizerSystem, конечно!

Данные внешнего вида

В SS14 всё, что сложнее одного неизменного спрайта, отрисовывается динамически с использованием данных внешнего вида. Visualizers, или VisualizerSystem, — это чисто клиентские EntitySystem, обновляющие спрайт сущности с помощью данных внешнего вида. У систем-визуализаторов обычно есть соответствующий клиентский компонент, хранящий некоторые настраиваемые параметры. Например, клиентские ItemCabinetSystem и ItemCabinetVisualsComponent.

Почему мы так делаем? Об этом немного упоминалось ранее, но есть несколько причин:

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

Данные отправляются и принимаются с помощью AppearanceComponent, а именно функций TryGetData (на клиенте) и SetData (на сервере). Данные хранятся как Dictionary<object, object>, то есть (в основном) для ключа и значения можно использовать что угодно. На практике ключом всегда должен быть enum, чтобы обеспечить проверку типов, которой мы не получаем со строками, избежать коллизий с ключом и сделать происходящее совершенно очевидным. Фактически TryGetData и SetData поддерживают в качестве ключа только enum и строки.

Пример

Вот очень простой пример установки данных внешнего вида на стороне сервера в ItemCabinetSystem:

        private void UpdateAppearance(Entity<ItemCabinetComponent?, AppearanceComponent?> ent)
        {
            if (!Resolve(ent, ref ent.Comp1, ref ref ent.Comp2, logMissing: false))
                return;

            _appearanceSystem.SetData(ent, ItemCabinetVisuals.IsOpen, ent.Comp1.Opened, ent.Comp2);
            _appearanceSystem.SetData(ent, ItemCabinetVisuals.ContainsItem, ent.Comp1.CabinetSlot.HasItem, ent.Comp2);
        }

Ключи — это значения enum из ItemCabinetVisuals, чтобы было очевидно, для чего они используются, а данные в обоих случаях — bool. AppearanceComponent и AppearanceSystem автоматически реплицируют эти данные клиенту!


Любая клиентская система сущностей может быть VisualizerSystem.

Посмотрим на клиентскую ItemCabinetSystem, чтобы увидеть, как она извлекает и использует данные внешнего вида:

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))
        {
            var state = isOpen ? component.OpenState : component.ClosedState;
            sprite.LayerSetState(ItemCabinetVisualLayers.Door, state);
            sprite.LayerSetVisible(ItemCabinetVisualLayers.ContainsItem, contains);
        }
    }
}

Во-первых, системы-визуализаторы определяются как наследники класса VisualizerSystem<T>, где T — соответствующий компонент, содержащий данные для визуализации. Это не строго обязательно — всё это можно сделать и с помощью обычной системы сущностей, — но так удобнее.

Визуализатор переопределяет OnAppearanceChange — метод, специфичный для систем-визуализаторов, который вызывается при изменении данных внешнего вида на клиенте, — и пытается получить компонент спрайта, чтобы изменить его с использованием новых данных.

Затем он использует TryGetData, чтобы получить вышеупомянутые визуальные данные шкафчика для предметов. После этого он использует эти данные для изменения спрайта шкафчика для предметов. Просто и понятно!

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

Дополнение: обобщённые визуализаторы

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

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

     - type: Appearance
     - type: GenericVisualizer
       visuals:
         enum.ItemCabinetVisuals.IsOpen: # <- Ключ данных внешнего вида. Либо enum, либо обычная строка.
           enum.ItemCabinetVisualLayers.Door: # <- Ключ слоя спрайта. Либо enum, либо обычная строка.
            True: # <- Значение данных внешнего вида
              state: open # <- Данные слоя спрайта, которые следует использовать для этого значения внешнего вида
            False: { state: closed } # <- Можно также записать YAML в одну строку, что уменьшает отступы и улучшает читаемость.
         # и затем то же самое для другой записи внешнего вида:
         enum.ItemCabinetVisuals.ContainsItem:
           enum.ItemCabinetVisualLayers.ContainsItem:
             True: { visible: true}
             False: { visible: false}

Данные слоя спрайта могут задавать sprite, state, texture, shader, scale, rotation, offset, visible и color. Обратите внимание, что YAML для значений внешнего вида — это просто результаты ToString() значений данных внешнего вида. Так, bool становятся “True”/“False”, а enum вроде VentPumpState.Off просто становится “Off”.

Спрайты, слои и состояния

Сущностям в Robust нужен SpriteComponent, если они хотят иметь визуальное представление. Компоненты спрайтов можно изменять на стороне клиента множеством способов, как показано выше, например задавая видимость, цвет (да, динамически окрашиваемый!) или смещение.

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

Пример

У дверей есть несколько различных наложений:

door-overlays.png

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

door-layers.png

(примечание: чрезмерно упрощено; это не все спрайты в этом rsi)


Вот как выглядит компонент спрайта базового шлюза:

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

Поле netsync — это bool, определяющий, будут ли серверные операции изменять спрайт для каждого клиента. Если вы правильно написали визуализатор (или визуализатор вообще не нужен), оно должно быть false!

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

Поле layers — это последовательность, содержащая данные для каждого слоя спрайта. Она определяет состояние, а также пару других вещей.

В Robust слои спрайтов могут быть «затенёнными» или «незатенёнными». «Затенённые» означают, что на них влияет освещение, и они могут сильно меняться в зависимости от окружающего света. «Незатенённые» просто означают, что на них не влияет освещение или отбрасываемые тени, то есть они отображаются над тенями, создавая иллюзию, будто они излучают собственный свет. При этом они всё равно отображаются под FoV.

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

Анимации

TODO

Subpages