← Назад к вопросам

Реализовать drag and drop для UI элементов

2.0 Middle🔥 251 комментариев
#C# и ООП#UI#Unity Core#Анимация

Условие

Реализуйте систему drag and drop для UI элементов в Unity.

Требования

  1. Элементы можно перетаскивать мышью или тачем
  2. Визуальная обратная связь при перетаскивании
  3. Элементы возвращаются на место если drop невалидный
  4. Можно менять элементы местами
  5. События OnBeginDrag, OnDrag, OnEndDrag

Применение

  • Инвентарь
  • Построитель билдов
  • Головоломки

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Система Drag and Drop для UI элементов

Эта задача требует создания гибкой системы перетаскивания с валидацией, визуальной обратной связью и событиями. Расскажу о полной архитектуре.

Архитектура системы

Основные компоненты:

  1. DraggableItem — базовый компонент для перетаскиваемых элементов
  2. DropZone — место, куда можно бросить элемент
  3. DragDropManager — управление всей системой
  4. ItemData — данные элемента

DraggableItem - Компонент перетаскивания

public interface IDraggable
{
    void OnBeginDrag(PointerEventData eventData);
    void OnDrag(PointerEventData eventData);
    void OnEndDrag(PointerEventData eventData);
    ItemData GetItemData();
    bool CanBeDragged();
}

public class DraggableItem : MonoBehaviour, IDraggable, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] private ItemData itemData;
    [SerializeField] private CanvasGroup canvasGroup;
    [SerializeField] private Image itemImage;
    [SerializeField] private float dragAlpha = 0.6f;
    [SerializeField] private float dragScale = 1.1f;
    
    private Vector3 originalPosition;
    private LayoutElement layoutElement;
    private Canvas canvas;
    private RectTransform rectTransform;
    private RectTransform dragPreview;
    private DropZone currentDropZone;
    private bool isDragging = false;
    
    public event System.Action<DraggableItem> OnBeginDragEvent;
    public event System.Action<DraggableItem> OnEndDragEvent;
    public event System.Action<DraggableItem, DropZone> OnDropEvent;
    
    public ItemData ItemData => itemData;
    public bool IsDragging => isDragging;
    public DropZone CurrentDropZone => currentDropZone;
    
    private void Start()
    {
        rectTransform = GetComponent<RectTransform>();
        canvasGroup = GetComponent<CanvasGroup>();
        if (canvasGroup == null)
        {
            canvasGroup = gameObject.AddComponent<CanvasGroup>();
        }
        
        canvas = GetComponentInParent<Canvas>();
        originalPosition = rectTransform.anchoredPosition;
        
        layoutElement = GetComponent<LayoutElement>();
    }
    
    public void OnBeginDrag(PointerEventData eventData)
    {
        if (!CanBeDragged())
            return;
        
        isDragging = true;
        OnBeginDragEvent?.Invoke(this);
        DragDropManager.Instance?.OnItemDragBegin(this);
        
        // Сохраняем оригинальную позицию
        originalPosition = rectTransform.anchoredPosition;
        
        // Визуальная обратная связь
        SetDragVisuals(true);
        
        // Создаём preview изображение
        CreateDragPreview();
    }
    
    public void OnDrag(PointerEventData eventData)
    {
        if (!isDragging)
            return;
        
        // Обновляем позицию preview
        if (dragPreview != null)
        {
            dragPreview.anchoredPosition = eventData.position;
        }
    }
    
    public void OnEndDrag(PointerEventData eventData)
    {
        if (!isDragging)
            return;
        
        isDragging = false;
        
        // Находим drop zone под курсором
        DropZone targetDropZone = GetDropZoneAtPointer(eventData.position);
        
        if (targetDropZone != null && targetDropZone.CanAcceptItem(this))
        {
            // Валидный drop
            ExecuteDrop(targetDropZone);
        }
        else
        {
            // Невалидный drop - возвращаем на место
            ReturnToOriginalPosition();
        }
        
        // Очищаем визуалы
        SetDragVisuals(false);
        DestroyDragPreview();
        
        OnEndDragEvent?.Invoke(this);
        DragDropManager.Instance?.OnItemDragEnd(this, targetDropZone);
    }
    
    private void SetDragVisuals(bool isDragging)
    {
        if (canvasGroup != null)
        {
            canvasGroup.alpha = isDragging ? dragAlpha : 1f;
        }
        
        if (isDragging)
        {
            // Увеличиваем масштаб
            transform.localScale = Vector3.one * dragScale;
        }
        else
        {
            // Возвращаем оригинальный масштаб
            transform.localScale = Vector3.one;
        }
    }
    
    private void CreateDragPreview()
    {
        GameObject previewObject = new GameObject("DragPreview");
        dragPreview = previewObject.AddComponent<RectTransform>();
        dragPreview.SetParent(canvas.transform, false);
        dragPreview.sizeDelta = rectTransform.sizeDelta;
        
        Image previewImage = previewObject.AddComponent<Image>();
        if (itemImage != null)
        {
            previewImage.sprite = itemImage.sprite;
            previewImage.color = itemImage.color;
        }
        
        CanvasGroup previewCanvasGroup = previewObject.AddComponent<CanvasGroup>();
        previewCanvasGroup.alpha = 0.8f;
        
        // Устанавливаем на самый передний слой
        Canvas previewCanvas = previewObject.AddComponent<Canvas>();
        previewCanvas.overrideSorting = true;
        previewCanvas.sortingOrder = 10000;
    }
    
    private void DestroyDragPreview()
    {
        if (dragPreview != null)
        {
            Destroy(dragPreview.gameObject);
            dragPreview = null;
        }
    }
    
    private DropZone GetDropZoneAtPointer(Vector2 pointerPosition)
    {
        List<RaycastResult> results = new List<RaycastResult>();
        GraphicRaycaster raycaster = canvas.GetComponent<GraphicRaycaster>();
        
        PointerEventData pointerEventData = new PointerEventData(EventSystem.current)
        {
            position = pointerPosition
        };
        
        raycaster.Raycast(pointerEventData, results);
        
        foreach (RaycastResult result in results)
        {
            DropZone dropZone = result.gameObject.GetComponent<DropZone>();
            if (dropZone != null)
            {
                return dropZone;
            }
        }
        
        return null;
    }
    
    private void ExecuteDrop(DropZone targetDropZone)
    {
        currentDropZone = targetDropZone;
        targetDropZone.OnItemDropped(this);
        OnDropEvent?.Invoke(this, targetDropZone);
    }
    
    private void ReturnToOriginalPosition()
    {
        StartCoroutine(AnimateReturnToPosition());
    }
    
    private IEnumerator AnimateReturnToPosition()
    {
        float duration = 0.3f;
        float elapsed = 0f;
        Vector3 currentPos = rectTransform.anchoredPosition;
        
        while (elapsed < duration)
        {
            elapsed += Time.deltaTime;
            rectTransform.anchoredPosition = Vector3.Lerp(currentPos, originalPosition, elapsed / duration);
            yield return null;
        }
        
        rectTransform.anchoredPosition = originalPosition;
    }
    
    public bool CanBeDragged()
    {
        return itemData != null && gameObject.activeInHierarchy;
    }
    
    public ItemData GetItemData()
    {
        return itemData;
    }
    
    public void SetItemData(ItemData newData)
    {
        itemData = newData;
        UpdateVisuals();
    }
    
    private void UpdateVisuals()
    {
        if (itemImage != null && itemData != null)
        {
            itemImage.sprite = itemData.Icon;
        }
    }
}

DropZone - Зоны приёма

public interface IDropZone
{
    bool CanAcceptItem(DraggableItem item);
    void OnItemDropped(DraggableItem item);
}

public class DropZone : MonoBehaviour, IDropZone
{
    [SerializeField] private ItemType acceptedItemType = ItemType.Any;
    [SerializeField] private int maxItems = 1;
    [SerializeField] private Color validDropColor = Color.green;
    [SerializeField] private Color invalidDropColor = Color.red;
    [SerializeField] private Image dropZoneImage;
    
    private List<DraggableItem> containedItems = new List<DraggableItem>();
    private Color originalColor;
    private RectTransform rectTransform;
    
    public event System.Action<DraggableItem> OnItemDroppedEvent;
    
    public List<DraggableItem> ContainedItems => containedItems;
    public bool IsFull => containedItems.Count >= maxItems;
    
    private void Start()
    {
        rectTransform = GetComponent<RectTransform>();
        if (dropZoneImage == null)
        {
            dropZoneImage = GetComponent<Image>();
        }
        
        if (dropZoneImage != null)
        {
            originalColor = dropZoneImage.color;
        }
    }
    
    public bool CanAcceptItem(DraggableItem item)
    {
        if (IsFull)
            return false;
        
        if (acceptedItemType != ItemType.Any && item.ItemData.ItemType != acceptedItemType)
            return false;
        
        return true;
    }
    
    public void OnItemDropped(DraggableItem item)
    {
        if (!CanAcceptItem(item))
            return;
        
        // Если зона уже содержит элемент - обмениваем
        if (containedItems.Count > 0 && maxItems == 1)
        {
            DraggableItem existingItem = containedItems[0];
            containedItems.Remove(existingItem);
            
            // Перемещаем существующий элемент на место откуда взяли новый
            existingItem.transform.SetParent(item.transform.parent);
        }
        
        // Добавляем новый элемент
        containedItems.Add(item);
        item.transform.SetParent(transform);
        
        RectTransform itemRect = item.GetComponent<RectTransform>();
        itemRect.anchoredPosition = Vector3.zero;
        
        OnItemDroppedEvent?.Invoke(item);
    }
    
    public void RemoveItem(DraggableItem item)
    {
        containedItems.Remove(item);
    }
    
    public void HighlightAsValid()
    {
        if (dropZoneImage != null)
        {
            dropZoneImage.color = validDropColor;
        }
    }
    
    public void HighlightAsInvalid()
    {
        if (dropZoneImage != null)
        {
            dropZoneImage.color = invalidDropColor;
        }
    }
    
    public void ResetHighlight()
    {
        if (dropZoneImage != null)
        {
            dropZoneImage.color = originalColor;
        }
    }
}

ItemData - Данные элемента

public enum ItemType
{
    Any,
    Weapon,
    Armor,
    Potion,
    Quest
}

[System.Serializable]
public class ItemData
{
    public string itemId;
    public string itemName;
    public ItemType itemType;
    public Sprite icon;
    public int quantity = 1;
    
    public Sprite Icon => icon;
    public ItemType ItemType => itemType;
}

DragDropManager - Менеджер системы

public class DragDropManager : MonoBehaviour
{
    public static DragDropManager Instance { get; private set; }
    
    [SerializeField] private bool showDebug = true;
    
    private DraggableItem currentDraggedItem;
    private DropZone lastValidDropZone;
    
    public event System.Action<DraggableItem> OnDragBegin;
    public event System.Action<DraggableItem> OnDragEnd;
    public event System.Action<DraggableItem, DropZone> OnItemDropped;
    
    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            Instance = this;
        }
    }
    
    public void OnItemDragBegin(DraggableItem item)
    {
        currentDraggedItem = item;
        OnDragBegin?.Invoke(item);
        
        if (showDebug)
            Debug.Log($"Started dragging: {item.ItemData.itemName}");
    }
    
    public void OnItemDragEnd(DraggableItem item, DropZone targetDropZone)
    {
        currentDraggedItem = null;
        OnDragEnd?.Invoke(item);
        
        if (targetDropZone != null)
        {
            OnItemDropped?.Invoke(item, targetDropZone);
            if (showDebug)
                Debug.Log($"Dropped {item.ItemData.itemName} into {targetDropZone.name}");
        }
        else
        {
            if (showDebug)
                Debug.Log($"Drag cancelled for {item.ItemData.itemName}");
        }
    }
    
    public DraggableItem GetCurrentDraggedItem() => currentDraggedItem;
}

Пример использования - Инвентарь

public class Inventory : MonoBehaviour
{
    [SerializeField] private Transform inventoryContainer;
    [SerializeField] private DraggableItem itemPrefab;
    [SerializeField] private DropZone[] inventorySlots;
    
    private void Start()
    {
        DragDropManager.Instance.OnItemDropped += OnItemDropped;
    }
    
    public void AddItem(ItemData itemData)
    {
        // Находим свободный слот
        foreach (var slot in inventorySlots)
        {
            if (!slot.IsFull)
            {
                DraggableItem item = Instantiate(itemPrefab, slot.transform);
                item.SetItemData(itemData);
                slot.OnItemDropped(item);
                break;
            }
        }
    }
    
    private void OnItemDropped(DraggableItem item, DropZone dropZone)
    {
        // Обработка drop события
        Debug.Log($"Item {item.ItemData.itemName} dropped in {dropZone.name}");
    }
}

Ключевые особенности

1. Интерфейсы — IDraggable и IDropZone для гибкости

2. События — OnBeginDrag, OnDrag, OnEndDrag, OnDrop

3. Валидация — проверка возможности drop перед выполнением

4. Визуальная обратная связь — изменение альфы, масштаба и цвета

5. Обмен элементами — поддержка перемещения между слотами

6. Preview образ — показание позиции при перетаскивании

7. Плавная анимация — возврат на место при невалидном drop

Применение

  • Инвентарь — перемещение предметов между слотами
  • Построитель билдов — расстановка модулей и улучшений
  • Головоломки — перемещение элементов для решения задач
  • Deckbuilder — расстановка карт в колоде

Эта система обеспечивает удобный и интуитивный drag and drop с полной контролем над валидацией и визуальной обратной связью.