Unity UGUI 背包物品拖动实现

2,512 阅读4分钟

“这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

UGUI 背包物品拖动实现

在大多数游戏中都包含了背包系统,网格状的背包界面,会有物品的拖拽、图标交换类似的效果。

为了用UGUI实现物品拖拽并且在拖拽完成后自动吸附到目的方格中,踩了大量的坑。

在这里根据实现过程分为两大步骤

图标拖拽跟随鼠标

可以想到使用射线来检测点击在 需要拖动的对象上,但不需要自己的复杂实现,可以使用unity自带的接口
IBeginDragHandler,IDragHandler,IEndDragHandler
实现这三个接口的方法
分别对应点击时OnBeginDrag(PointerEventData eventData)
拖动时OnDrag(PointerEventData eventData)
和松开时OnEndDrag(PointerEventData eventData)

实现拖动,需要做什么,当然是让图标在拖动时跟随鼠标进行位移

public void OnBeginDrag(PointerEventData eventData)
    {
        Vector3 worldPoint;
        if (RectTransformUtility.ScreenPointToWorldPointInRectangle(GetComponent<RectTransform>(), eventData.position, Camera.main, out worldPoint))
        {
            //获取鼠标的偏移量
            offset = GetComponent<RectTransform>().position - worldPoint;
        }
        transform.SetParent(transform.parent.parent);
        
    }

    public void OnDrag(PointerEventData eventData)
    {
        Vector3 worldPoint;
        if (RectTransformUtility.ScreenPointToWorldPointInRectangle(GetComponent<RectTransform>(), eventData.position, Camera.main, out worldPoint))
        {
            //修正
            GetComponent<RectTransform>().position = offset + worldPoint;
        }
    }

❓肯定会好奇为什么会使用RectTransformUtility.ScreenPointToWorldPointInRectangle()这样一个方法。
跟随鼠标的话只需要这样写就行了把
transform.position = Input.mousePosition;
实际上如果在纯2D游戏界面中这样是可以达到效果的,但考虑到复杂的2D/3D混合,且镜头甚至会出现Rotation变化的情况下,这样做问题是非常严重的。图标不知道会飞到哪里去。
❓ScreenPointToWorldPointInRectangle()干了什么呢?

将一个屏幕空间点转换为世界空间中位于给定 RectTransform 平面上的一个位置。 cam 参数应为与此屏幕点关联的摄像机。对于设置为 Screen Space - Overlay 模式的 Canvas 中的 RectTransform,cam 参数应为 null。

当从提供 PointerEventData 对象的事件处理程序中使用 ScreenPointToWorldPointInRectangle 时,可以通过使用 PointerEventData.enterEventData(对于悬停功能)或 PointerEventData.pressEventCamera(对于单击功能)获取正确的摄像机。这会为给定事件自动使用正确的摄像机(或 null)。

啊⭕还有一个需要注意的是PointerEventData eventData参数,
PointerEventData 类有这样的属性

position当前指针位置。
pointerPressRaycast返回与鼠标单击、游戏手柄按钮按下或屏幕触摸相关联的 RaycastResult。

依靠PointerEventData .position替换了Input.mousePosition
使用偏移量来进行计算的好处是鼠标指针和图标能一直保持一个相对位置,并不固定在图标中心。
当然这也不是必须的🤷‍♂️

拖动放开后自动归位

public void OnEndDrag(PointerEventData eventData)
    {
        GameObject target = eventData.pointerCurrentRaycast.gameObject;
        if(target?.tag == Tags.InventoryItemGrid)
        {
            transform.SetParent(target.transform,false);
            transform.localPosition = Vector3.zero;
        }
        else
        {
            transform.SetParent(oldParent);
            transform.localPosition = Vector3.zero;
        }
    }

PointerEventData中有个很关键的pointerCurrentRaycast访问器,返回的是RaycastResult对象
剩下的就是对射线检测的处理,放手时对得到对象的tag做判断,所以需要提前在物品框的对象上绑定相应的tag标签。
当没有检测到物品框时,就自动回复到原位。
❓这样就完成了吗?
实际使用过程后中,会出现一个问题,当拖动物品到层级高的物品框对象上时,正确无误✅
可一旦拖动到层级低的物品框对象上时,没有效果❎

image.png
返回的RaycastResult是相机与鼠标方向产生的射线,当将图标脱离当前父对象时,在图标层级上的对象会与触发射线检测,而图标下触发检测当然只有图标自己了,完美的挡住了底层级的物体。这个射线检测是没有穿透的。层级越高越优先检测。

🔥被这个问题困扰了好久,最后发现图标一般都是Image
image.png
ImageRaycast target 属性,可以用来控制能否被射线忽略,只需要在 OnBeginDrag() 方法中将图标下的 Image 组件 Raycast target 属性设置为 false。当然不能只完成一次性使用,所以在 OnEndDrag() 方法中还要再设置为true

完整代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class InventoryItem : MonoBehaviour, IDragHandler,IBeginDragHandler,IEndDragHandler
{
    private Vector3 offset;

    private Transform oldParent;

    public void OnBeginDrag(PointerEventData eventData)
    {
        Vector3 worldPoint;
        if (RectTransformUtility.ScreenPointToWorldPointInRectangle(GetComponent<RectTransform>(), eventData.position, Camera.main, out worldPoint))
        {
            //获取鼠标的偏移量
            offset = GetComponent<RectTransform>().position - worldPoint;
        }
        oldParent = transform.parent;
        transform.SetParent(transform.parent.parent);
        
    }

    public void OnDrag(PointerEventData eventData)
    {
        Vector3 worldPoint;
        if (RectTransformUtility.ScreenPointToWorldPointInRectangle(GetComponent<RectTransform>(), eventData.position, Camera.main, out worldPoint))
        {
            //修正
            GetComponent<RectTransform>().position = offset + worldPoint;
        }
        
        transform.GetComponent<Image>().raycastTarget = false;
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        
        GameObject target = eventData.pointerCurrentRaycast.gameObject;
        if(target?.tag == Tags.InventoryItemGrid)
        {
            transform.SetParent(target.transform,false);
            transform.localPosition = Vector3.zero;
        }
        else
        {
            transform.SetParent(oldParent);
            transform.localPosition = Vector3.zero;
        }

        transform.GetComponent<Image>().raycastTarget = true;
    }
}