Реализовать drag and drop для UI элементов
Условие
Реализуйте систему drag and drop для UI элементов в Unity.
Требования
- Элементы можно перетаскивать мышью или тачем
- Визуальная обратная связь при перетаскивании
- Элементы возвращаются на место если drop невалидный
- Можно менять элементы местами
- События OnBeginDrag, OnDrag, OnEndDrag
Применение
- Инвентарь
- Построитель билдов
- Головоломки
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Система Drag and Drop для UI элементов
Эта задача требует создания гибкой системы перетаскивания с валидацией, визуальной обратной связью и событиями. Расскажу о полной архитектуре.
Архитектура системы
Основные компоненты:
- DraggableItem — базовый компонент для перетаскиваемых элементов
- DropZone — место, куда можно бросить элемент
- DragDropManager — управление всей системой
- 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 с полной контролем над валидацией и визуальной обратной связью.