Перейти к содержанию

Зоны дропа (DropAreaBase)

Базовый класс DropAreaBase позволяет создавать кастомные зоны дропа без знания внутренностей системы. Он берёт на себя всю механику взаимодействия, а подкласс определяет только что делать с предметами.


Два паттерна использования

flowchart TD
    BASE["DropAreaBase"]
    BASE --> SIMPLE["Простое потребление"]
    BASE --> DELEG["Делегирование"]

    SIMPLE --> WDZ["WorldDropZone\nTrashZone\nSellZone"]
    DELEG --> IDA["InventoryDropArea"]

    style BASE fill:#7c4dff,color:#fff
    style SIMPLE fill:#00c853,color:#fff
    style DELEG fill:#ff6d00,color:#fff
Паттерн Суть Когда использовать
Простое потребление Переопределить CanAcceptEntry + ProcessEntry. Удаление из источника --- автоматически Зона мира, корзина, продажа --- любая цель без инвентаря
Делегирование Переопределить GetDropProcessor(), вернуть свой IDropProcessor Инвентарные области со сложной логикой (policy, swap, конвертация)

Простое потребление

Минимальный подкласс --- всего два метода:

public class TrashDropZone : DropAreaBase
{
    protected override bool CanAcceptEntry(DragEntry entry)
    {
        // Принимаем всё
        return entry.Stack != null && !entry.Stack.IsEmpty;
    }

    protected override bool ProcessEntry(ItemStack freshStack, DragEntry entry)
    {
        // Ничего не делаем --- предметы просто исчезают.
        // Удаление из источника выполнит базовый класс автоматически.
        return true;
    }
}

Как работает внутри

flowchart TD
    A["Игрок бросает предмет"] --> B["DropAreaBase начинает обработку"]
    B --> C["Для каждого entry"]
    C --> D{"CanAcceptEntry(entry)?"}
    D -->|Да| E["Скопировать стак из source slot"]
    E --> F{"ProcessEntry(freshStack, entry)"}
    F -->|true| G["Удалить предметы из источника + вызвать события"]
    G --> H{"Есть ещё entry?"}
    H -->|Да| C
    H -->|Нет| I["DropResult.Succeeded"]

Делегирование

Когда нужна сложная логика дропа (пайплайн переноса, policy, swap), переопределите GetDropProcessor():

public class CustomInventoryArea : DropAreaBase
{
    [SerializeField] private UniversalInventory _inventory;

    protected override bool TryActivateAsFocusedTarget()
    {
        if (_inventory == null) return false;
        // Своя валидация...
        DragManager.PushDropTarget(this);
        return true;
    }

    public override IDropProcessor GetDropProcessor()
    {
        // Возвращаем свой процессор вместо this
        return new InventoryDropProcessor(_inventory, DragManager?.GlobalRules);
    }

    public override BaseSlot GetTargetSlot() => null;
}

В этом случае методы CanAcceptEntry и ProcessEntry базового класса не вызываются --- вся обработка идёт через возвращённый процессор.


Что делает базовый класс

DropAreaBase наследует Selectable и реализует IDropTarget + IDropProcessor.

Автоматическая механика

Механика Описание
Pointer tracking OnPointerEnter/Exit автоматически push/pop в стек целей DragAndDropManager
State events Подписка на OnDragStarted/Cancelled/Completed/Ended для управления interactable и raycast
Raycast management raycastTarget включается только во время перетаскивания
Gamepad support Наследование от Selectable даёт навигацию и фокус из коробки
Highlight lifecycle OnBecomeActiveTarget/OnBecomeInactiveTarget вызывают OnHighlightChanged
Source removal В паттерне простого потребления --- автоматическое удаление из слота-источника с эмиссией событий

Все override-точки

public abstract class DropAreaBase : Selectable, IDropTarget, IDropProcessor
{
    // ── Простое потребление ──────────────────────
    // Переопределить для зон без инвентаря

    protected virtual bool CanAcceptEntry(DragEntry entry);
    // Может ли зона принять этот entry?
    // По умолчанию: true

    protected virtual bool ProcessEntry(ItemStack freshStack, DragEntry entry);
    // Что сделать с предметами? Удаление из источника --- автоматически.
    // По умолчанию: false (ничего не делает)

    // ── Делегирование ────────────────────────────
    // Переопределить для зон с инвентарём

    public virtual IDropProcessor GetDropProcessor();
    // По умолчанию: this (простое потребление)

    public virtual BaseSlot GetTargetSlot();
    // По умолчанию: null

    // ── Общие ────────────────────────────────────

    internal virtual bool TryActivateAsFocusedTarget();
    // Вызывается при наведении курсора во время перетаскивания.
    // По умолчанию: проверяет CanAcceptEntry на первом entry + PushDropTarget.

    protected virtual void OnTargetDeactivated();
    // Вызывается при уходе курсора. Для сброса внутреннего состояния.

    protected virtual void OnHighlightChanged(bool highlighted, bool canAccept);
    // Вызывается при смене подсветки. highlighted = активна ли зона,
    // canAccept = может ли принять текущий предмет.

    protected virtual bool RemoveFromSource { get; }
    // Удалять ли предметы из источника после ProcessEntry.
    // По умолчанию: true. Переопределить в false для зон копирования/предпросмотра.
}

Примеры подклассов

WorldDropZone --- спавн в 3D мире

Ниже пример по мотивам реализации из Demo2 Loot.

public class WorldDropZone : DropAreaBase
{
    [SerializeField] private Transform _spawnPoint;

    protected override bool CanAcceptEntry(DragEntry entry)
    {
        // Все адаптеры должны иметь 3D-префаб
        foreach (var adapter in entry.Stack.Adapters)
            if (adapter is not ItemAdapterSoWith3DAdapter with3D || with3D.WorldPrefab == null)
                return false;
        return true;
    }

    protected override bool ProcessEntry(ItemStack freshStack, DragEntry entry)
    {
        foreach (var adapter in freshStack.Adapters)
        {
            var with3D = (ItemAdapterSoWith3DAdapter)adapter;
            Instantiate(with3D.WorldPrefab, _spawnPoint.position, Quaternion.identity);
        }
        return true;
    }
}

SellDropZone --- продажа за золото

public class SellDropZone : DropAreaBase
{
    [SerializeField] private PlayerWallet _wallet;

    protected override bool CanAcceptEntry(DragEntry entry)
    {
        return entry.Stack.PrimaryAdapter is ISellable;
    }

    protected override bool ProcessEntry(ItemStack freshStack, DragEntry entry)
    {
        int total = 0;
        foreach (var adapter in freshStack.Adapters)
            total += ((ISellable)adapter).SellPrice;
        _wallet.AddGold(total);
        return true;
    }
}

PreviewDropZone --- просмотр без удаления

public class PreviewDropZone : DropAreaBase
{
    protected override bool RemoveFromSource => false;

    protected override bool CanAcceptEntry(DragEntry entry)
        => entry.Stack?.PrimaryAdapter is IPreviewable;

    protected override bool ProcessEntry(ItemStack freshStack, DragEntry entry)
    {
        ShowPreview(freshStack);
        return true;
    }
}

RemoveFromSource = false отключает автоматическое удаление из источника. Предметы остаются в исходном инвентаре.


Подсветка

Переопределите OnHighlightChanged для визуального отклика:

[SerializeField] private Image _highlight;
[SerializeField] private Color _validColor = Color.green;
[SerializeField] private Color _invalidColor = Color.red;

protected override void OnHighlightChanged(bool highlighted, bool canAccept)
{
    if (_highlight == null) return;
    _highlight.color = highlighted
        ? (canAccept ? _validColor : _invalidColor)
        : Color.clear;
}

canAccept определяется автоматически через CanAcceptEntry на первом entry при наведении.


Справочник

Класс Паттерн Описание
DropAreaBase --- Базовый класс для всех зон дропа
WorldDropZone Простое потребление Спавн 3D-объектов в мире
InventoryDropArea Делегирование Дроп в инвентарь через пайплайн переноса