Расширенное использование спрайтов в Space Station 14

SS14_#4 // Создание пользовательских спрайтов, экспорт, работа с большими спрайтами // Время чтения: 60 мин

Введение

Этот гайд охватывает расширенное использование спрайтов, включая создание пользовательских спрайтов для систем, модификацию существующих спрайтов и работу с большими спрайтами. Он строится на основах базовой спрайтовой системы, описанной в `ss14-sprite-system.html`.

Создание пользовательских спрайтов для систем

1. ContentSpriteSystem - Инструмент для экспорта спрайтов

ContentSpriteSystem предоставляет инструмент для администраторов для экспорта спрайтов во всех 4 направлениях:

Использование инструмента для экспорта спрайтов

  1. Включите отладочные глаголы - используйте команду `/adminverbs`
  2. Щелкните правой кнопкой мыши любую сущность в игровом мире
  3. Выберите "Export Entity" из меню отладки
  4. Спрайты будут экспортированы в директорию `/Exports` в формате: `--.png`

Настройка поведения экспорта

csharp
// Content.Client/Sprite/ContentSpriteSystem.cs (упрощённый вариант)
public sealed class ContentSpriteSystem : EntitySystem
{
    [Dependency] private readonly IClientAdminManager _adminManager = default!;
    [Dependency] private readonly IClyde _clyde = default!;
    [Dependency] private readonly IGameTiming _timing = default!;
    [Dependency] private readonly IResourceManager _resManager = default!;
    [Dependency] private readonly IUserInterfaceManager _ui = default!;

    public static readonly ResPath Exports = new ResPath("/Exports");

    public override void Initialize()
    {
        base.Initialize();
        _resManager.UserData.CreateDir(Exports);
        _ui.RootControl.AddChild(new ContentSpriteControl());
        SubscribeLocalEvent>(GetVerbs);
    }

    public async Task Export(EntityUid entity, bool includeId = true, CancellationToken cancelToken = default)
    {
        var tasks = new Task[4];
        var i = 0;

        foreach (var dir in new Direction[] { Direction.South, Direction.East, Direction.North, Direction.West })
        {
            tasks[i++] = Export(entity, dir, includeId: includeId, cancelToken);
        }

        await Task.WhenAll(tasks);
    }

    private void GetVerbs(GetVerbsEvent ev)
    {
        if (!_adminManager.IsAdmin())
            return;

        var verb = new Verb
        {
            Text = Loc.GetString("export-entity-verb-get-data-text"),
            Category = VerbCategory.Debug,
            Act = async () => await Export(ev.Target)
        };

        ev.Verbs.Add(verb);
    }
}

2. Пользовательские спрайтовые системы в Content.Client

Пример 1: Использование RandomSpriteComponent

Движок SS14 предоставляет встроенный `RandomSpriteComponent`, который позволяет использовать случайные вариации спрайтов:

yaml
# Resources/Prototypes/Entities/MyEntity.yml
- type: entity
  name: My Random Entity
  parent: BaseItem
  id: MyRandomEntity
  components:
  - type: Sprite
    sprite: Objects/MyCategory/my_sprite.rsi
    state: default
  - type: RandomSpriteComponent
    getAllGroups: false  # Если true, использует все доступные группы, а не одну случайную
    available:
      # Группа 1: Красная вариация
      - base:
          state: red_base
        detail:
          state: red_detail
          color: "#ff0000"
      # Группа 2: Синяя вариация
      - base:
          state: blue_base
        detail:
          state: blue_detail
          color: "#0000ff"
      # Группа 3: Зелёная вариация
      - base:
          state: green_base
        detail:
          state: green_detail
          color: "#00ff00"

Пример 2: Пользовательский спрайтовый компонент для мини-игры

csharp
// Content.Client/MyMinigame/Components/MyMinigameSpriteComponent.cs
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;

namespace Content.Client.MyMinigame.Components;

[RegisterComponent]
public sealed partial class MyMinigameSpriteComponent : Component
{
    [DataField("sprite")]
    public SpriteSpecifier Sprite { get; set; } = SpriteSpecifier.Invalid;

    [DataField("animationSpeed")]
    public float AnimationSpeed { get; set; } = 0.1f;

    [DataField("maxFrames")]
    public int MaxFrames { get; set; } = 8;

    [DataField("currentFrame")]
    public int CurrentFrame { get; set; } = 0;
}

Система для контроля спрайта

csharp
// Content.Client/MyMinigame/Systems/MyMinigameSpriteSystem.cs
using Content.Client.MyMinigame.Components;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;

namespace Content.Client.MyMinigame.Systems;

public sealed class MyMinigameSpriteSystem : EntitySystem
{
    [Dependency] private readonly IGameTiming _timing = default!;
    [Dependency] private readonly SpriteSystem _sprite = default!;

    private float _timeSinceLastFrame;

    public override void Initialize()
    {
        base.Initialize();
        SubscribeLocalEvent(OnComponentInit);
    }

    private void OnComponentInit(EntityUid uid, MyMinigameSpriteComponent component, ComponentInit args)
    {
        if (component.Sprite is not SpriteSpecifier.Rsi rsi)
            return;

        // Устанавливаем начальное состояние спрайта
        if (TryComp(uid, out var sprite))
        {
            _sprite.LayerSetSprite((uid, sprite), 0, component.Sprite);
            _sprite.LayerSetRsiState((uid, sprite), 0, $"frame{component.CurrentFrame}");
        }
    }

    public override void FrameUpdate(float frameTime)
    {
        _timeSinceLastFrame += frameTime;

        // Обновляем все спрайты мини-игры
        var query = EntityQueryEnumerator();
        while (query.MoveNext(out var uid, out var comp, out var sprite))
        {
            if (_timeSinceLastFrame >= comp.AnimationSpeed)
            {
                comp.CurrentFrame = (comp.CurrentFrame + 1) % comp.MaxFrames;
                _sprite.LayerSetRsiState((uid, sprite), 0, $"frame{comp.CurrentFrame}");
                _timeSinceLastFrame = 0;
            }
        }
    }

    public void SetFrame(EntityUid uid, int frame, MyMinigameSpriteComponent? component = null, SpriteComponent? sprite = null)
    {
        if (!Resolve(uid, ref component, ref sprite))
            return;

        if (frame < 0 || frame >= component.MaxFrames)
            return;

        component.CurrentFrame = frame;
        _sprite.LayerSetRsiState((uid, sprite), 0, $"frame{frame}");
    }
}

Конфигурация прототипа

yaml
# Resources/Prototypes/Entities/MyMinigame/my_minigame_entity.yml
- type: entity
  name: My Minigame Entity
  parent: BaseItem
  id: MyMinigameEntity
  components:
  - type: Sprite
    sprite: Objects/MyMinigame/my_minigame.rsi
    state: frame0
  - type: MyMinigameSpriteComponent
    sprite: Objects/MyMinigame/my_minigame.rsi
    animationSpeed: 0.2
    maxFrames: 16

3. Система затемнения спрайтов

Движок SS14 предоставляет встроенную систему затемнения спрайтов, которая автоматически затемняет сущности, когда игрок за ними:

Использование SpriteFadeComponent

yaml
# Resources/Prototypes/Entities/MyFadingEntity.yml
- type: entity
  name: My Fading Entity
  parent: BaseItem
  id: MyFadingEntity
  components:
  - type: Sprite
    sprite: Objects/MyCategory/my_entity.rsi
    state: icon
  - type: SpriteFadeComponent  # Этот компонент включает затемнение

Настройка логики затемнения

csharp
// Content.Client/MySystem/Components/MyCustomFadeComponent.cs
using Content.Shared.Sprite;
using Robust.Shared.GameObjects;

namespace Content.Client.MySystem.Components;

[RegisterComponent]
public sealed partial class MyCustomFadeComponent : Component
{
    [DataField("fadeDistance")]
    public float FadeDistance { get; set; } = 5.0f;

    [DataField("targetAlpha")]
    public float TargetAlpha { get; set; } = 0.3f;

    [DataField("fadeSpeed")]
    public float FadeSpeed { get; set; } = 2.0f;
}

Пользовательская система затемнения

csharp
// Content.Client/MySystem/Systems/MyCustomFadeSystem.cs
using Content.Client.MySystem.Components;
using Content.Shared.Sprite;
using Robust.Client.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;

namespace Content.Client.MySystem.Systems;

public sealed class MyCustomFadeSystem : EntitySystem
{
    [Dependency] private readonly SpriteSystem _sprite = default!;
    [Dependency] private readonly SharedTransformSystem _transform = default!;
    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
    [Dependency] private readonly FixtureSystem _fixtures = default!;

    public override void Initialize()
    {
        base.Initialize();
        SubscribeLocalEvent(OnComponentInit);
        SubscribeLocalEvent(OnComponentShutdown);
    }

    private void OnComponentInit(EntityUid uid, MyCustomFadeComponent component, ComponentInit args)
    {
        // Добавляем FadingSpriteComponent для отслеживания
        if (!HasComp(uid))
        {
            var fadeComp = AddComp(uid);
            if (TryComp(uid, out var sprite))
            {
                fadeComp.OriginalAlpha = sprite.Color.A;
            }
        }
    }

    private void OnComponentShutdown(EntityUid uid, MyCustomFadeComponent component, ComponentShutdown args)
    {
        // Восстанавливаем исходную прозрачность
        if (TryComp(uid, out var fade) &&
            TryComp(uid, out var sprite))
        {
            _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(fade.OriginalAlpha));
            RemComp(uid);
        }
    }

    public override void FrameUpdate(float frameTime)
    {
        var query = EntityQueryEnumerator();
        while (query.MoveNext(out var uid, out var comp, out var sprite))
        {
            // Пользовательская логика затемнения здесь
            // Пример: Затемнение на основе расстояния от локального игрока
            var distance = GetDistanceFromPlayer(uid);
            var alpha = CalculateFadeAlpha(distance, comp);
            _sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(alpha));
        }
    }

    private float GetDistanceFromPlayer(EntityUid uid)
    {
        // Реализация расчета расстояния от локального игрока
        return 0;
    }

    private float CalculateFadeAlpha(float distance, MyCustomFadeComponent comp)
    {
        if (distance <= 0) return comp.TargetAlpha;
        if (distance >= comp.FadeDistance) return 1.0f;

        var normalized = distance / comp.FadeDistance;
        return MathHelper.Lerp(comp.TargetAlpha, 1.0f, normalized);
    }
}

4. Управление масштабом спрайтов

Движок SS14 предоставляет управление масштабом через `ScaleVisualsComponent` и `SharedScaleVisualsSystem`:

Базовая конфигурация масштаба

yaml
# Resources/Prototypes/Entities/MyScaledEntity.yml
- type: entity
  name: My Scaled Entity
  parent: BaseItem
  id: MyScaledEntity
  components:
  - type: Sprite
    sprite: Objects/MyCategory/my_entity.rsi
    state: icon
  - type: ScaleVisualsComponent
    scale: 1.5  # 150% масштаб

Серверный контроль масштаба

csharp
// Content.Shared/MySystem/Systems/SharedMySystem.cs
using Content.Shared.Sprite;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;

namespace Content.Shared.MySystem.Systems;

public abstract class SharedMySystem : EntitySystem
{
    [Dependency] private readonly SharedScaleVisualsSystem _scaleSystem = default!;

    public void SetEntityScale(EntityUid uid, Vector2 scale)
    {
        _scaleSystem.SetSpriteScale(uid, scale);
    }

    public Vector2 GetEntityScale(EntityUid uid)
    {
        return _scaleSystem.GetSpriteScale(uid);
    }
}

Клиентская обработка масштаба

csharp
// Content.Client/MySystem/Systems/MySystem.cs
using Content.Shared.MySystem.Systems;
using Content.Shared.Sprite;
using Robust.Client.GameObjects;
using Robust.Shared.Maths;

namespace Content.Client.MySystem.Systems;

public sealed class MySystem : SharedMySystem
{
    [Dependency] private readonly SpriteSystem _sprite = default!;

    public override void Initialize()
    {
        base.Initialize();
        SubscribeLocalEvent(OnScaleEntity);
    }

    private void OnScaleEntity(ScaleEntityEvent ev)
    {
        if (TryComp(ev.Uid, out var sprite))
        {
            // Опционально: Пользовательская клиентская обработка масштаба
            _sprite.SetScale((ev.Uid, sprite), ev.Scale * 1.2f); // 20% больше, чем указано на сервере
        }
    }

    public void ToggleScale(EntityUid uid)
    {
        var currentScale = GetEntityScale(uid);
        var newScale = currentScale.X == 1.0f ? new Vector2(1.5f, 1.5f) : Vector2.One;
        SetEntityScale(uid, newScale);
    }
}

5. Создание больших спрайтов

Структура RSI для больших спрайтов

Для больших спрайтов (например, транспортных средств, структур) используйте большие размеры:

json
// Resources/Textures/Objects/Vehicles/MyTank.rsi/meta.json
{
  "version": 1,
  "license": "CC-BY-SA-3.0",
  "size": { "x": 64, "y": 64 },
  "states": [
    { "name": "idle", "directions": 4 },
    { "name": "moving", "directions": 4, "delays": [[0.1, 0.1]] },
    { "name": "shooting", "directions": 4, "delays": [[0.05, 0.1, 0.15]] }
  ]
}

Обработка больших спрайтов в коде

csharp
// Content.Client/Vehicles/Components/VehicleSpriteComponent.cs
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;

namespace Content.Client.Vehicles.Components;

[RegisterComponent]
public sealed partial class VehicleSpriteComponent : Component
{
    [DataField("baseSprite")]
    public SpriteSpecifier BaseSprite { get; set; } = SpriteSpecifier.Invalid;

    [DataField("turretSprite")]
    public SpriteSpecifier TurretSprite { get; set; } = SpriteSpecifier.Invalid;

    [DataField("turretAngle")]
    public float TurretAngle { get; set; } = 0f;

    public int TurretLayer { get; set; } = -1;
}
csharp
// Content.Client/Vehicles/Systems/VehicleSpriteSystem.cs
using Content.Client.Vehicles.Components;
using Robust.Client.GameObjects;
using Robust.Shared.Maths;

namespace Content.Client.Vehicles.Systems;

public sealed class VehicleSpriteSystem : EntitySystem
{
    [Dependency] private readonly SpriteSystem _sprite = default!;

    public override void Initialize()
    {
        base.Initialize();
        SubscribeLocalEvent(OnComponentInit);
        SubscribeLocalEvent(OnComponentShutdown);
    }

    private void OnComponentInit(EntityUid uid, VehicleSpriteComponent component, ComponentInit args)
    {
        if (!TryComp(uid, out var sprite))
            return;

        // Устанавливаем основную спрайт транспортного средства
        if (component.BaseSprite is SpriteSpecifier.Rsi baseRsi)
        {
            _sprite.LayerSetSprite((uid, sprite), 0, component.BaseSprite);
            _sprite.LayerSetRsiState((uid, sprite), 0, "idle");
        }

        // Устанавливаем спрайт турели
        if (component.TurretSprite is SpriteSpecifier.Rsi turretRsi)
        {
            component.TurretLayer = _sprite.LayerAdd((uid, sprite));
            _sprite.LayerSetSprite((uid, sprite), component.TurretLayer, component.TurretSprite);
            _sprite.LayerSetRsiState((uid, sprite), component.TurretLayer, "turret");
            _sprite.LayerSetRotation((uid, sprite), component.TurretLayer, Angle.FromDegrees(component.TurretAngle));
        }
    }

    private void OnComponentShutdown(EntityUid uid, VehicleSpriteComponent component, ComponentShutdown args)
    {
        // Очищаем ресурсы при выключении компонента
    }

    public void SetTurretAngle(EntityUid uid, float angle, VehicleSpriteComponent? component = null, SpriteComponent? sprite = null)
    {
        if (!Resolve(uid, ref component, ref sprite))
            return;

        if (component.TurretLayer == -1)
            return;

        component.TurretAngle = angle;
        _sprite.LayerSetRotation((uid, sprite), component.TurretLayer, Angle.FromDegrees(angle));
    }
}

Советы по оптимизации спрайтов

Лучшие практики

  • Используйте спрайты одинакового размера для упрощения работы с атласами
  • Избегайте слишком мелких или слишком больших спрайтов
  • Оптимизируйте количество состояний спрайтов
  • Используйте сжатие PNG для уменьшения размера файлов
  • Избегайте прозрачности там, где она не нужна
  • Используйте спрайтовые атласы для группировки связанных спрайтов

Заключение

Расширенное использование спрайтов в Space Station 14 открывает широкие возможности для создания уникальных визуальных эффектов и высококачественного контента. Понимание инструментов экспорта, работы с большими спрайтами и динамической модификации спрайтов помогает разработчикам создавать более интересные и привлекательные игры.

Полезные ссылки