Unity背包系统

        实现一个拖拽移动物品,可以通过按键拆分物品的背包,和物品交换(有实力的可以跟根据我的方法增设数量判断功能)本页面不介绍拆分功能,毕竟我实现的可能比较复杂,需要者看我下一篇投稿即可,我会在那里详细介绍。

        相对上次太久没有更新了......

        这次实现的功能相对比较复杂,我也是掉了好多头发弄出来的,并不是最好的,但我会尽量尝试体贴新人的角度讲解,代码仅供参考,经过了我过数次的简化对于新手来说可能根本看不懂,所以建议按照我提供的思路去尝试一个一个实现每个功能,最后才能理解学会背包系统。

        背包系统的第一点是设计UI物品栏,能够随着格子数增加自动排序列表是第一实现点,不然一个一个的拉动排序是非常麻烦的。Unity UI中,图像的有一个属性(图像类型/)选项设置为平铺时,拉伸图片使,图片就会按照不断复制覆盖的方式填充整个图像,而背包的选项卡也是类似这个原理,在Unity UI的画布下,创建一个空物体,添加Gird Layout Group组件,然后在它的子物体下不断复制(CTRL + D)图片时,你就会发现图片就会类似背包选项卡一样平铺,这样只要不断复制第一个选项卡,就能方便快速的完成对于每个格子的排位填充。

        以下是示例图:

        完成背包的每个格子的排版后,我们只需要将每个物品挂载到对应的格子下作为子物体即可,由于这里的格子图像边界直接排布会连在一起不美观,所以我实际将格子图像作为一个空物体的子物体。想要实现物品显示数字(表示数量)),需要把一个文本组件加到物品上(不建议在格子上是因为比起挂物品上会更不好处理数量变动),为了接下来我们便于代码检测区分物品和格子,将格子(套有空物体的保持空物体标签不要改)的标签随个人设置(这里的格子标签是item),另外其一个标签识别物品(这里使用的是goods)这样就能便于后续代码操作物品了。

        图:

   想要实现物品移动物体,我们需要用到三个接口,分别是IDragHandler,IBeginDragHandler,IEndDragHandler实现它们的方法,OnBeginDragOnDragOnEndDrag这三个方法分别对应了当鼠标点击时执行一次该方法(OnBeginDrag),当鼠标拖动是执行方法一次(IBeginDragHandler),鼠标放开时执行一次(IEndDragHandler),实现接口的脚本挂载在对应的UI组件上时,该UI组件就能触发这些方法,每个方法都返回了PointerEventDataeventData对象,而对象中eventData.pointerCurrentRaycast.gameObject可以拿到鼠标当前位置指向上的对象。

        接下来就是最困难也是最核心的部分,实现拖动移动物品的逻辑。这里我还增加了按住shift移动鼠标时,每次从物品堆中分离一个物品的实现,根据你的具体需要可以选择不实现这个功能,因为会让实现难度指数增加。

        先看看需要用上的属性有什么(方便后续理解代码)

    private GameObject image;//物品栏图片组件

    private GameObject Re_gameObject;//拖动前父物体

    private bool Shift_Drag;//是否是按下shift键下拖拽,当然键位可以自定义

    private bool Shift_Drag_End;//拆分拖动结束

    private GameObject Current_image;//临时存放原组物体

        拖拽的第一步就是当鼠标点击中一个物品时,如果点击的是物品,那么就让模拟选中这个物品,,利用讲到的eventData.pointerCurrentRaycast.gameObject然后判断这个物体是否具有标签goods就能确实是否是需要的物品,设置一个GameObject,将返回的物体赋给它。这样就完成了物体的选中(你还可以改格子图像颜色等属性模拟选中效果)

        以下为代码,不需要实现shift拆分功能的不用管if中嵌套的if语句

    //点击物品,准备拖动时,执行一次
    public void OnBeginDrag(PointerEventData eventData)
    {
        //获取到需要拖动的物品
        GameObject image_info = eventData.pointerCurrentRaycast.gameObject;
        if (image_info.CompareTag("goods"))
        {
            if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift) && !Shift_Drag)//在按下shift情况下的单拖
            {
                Shift_Drag = true;//按下了shfit,但不一定视为拆分操作
            }
            //拿到移动前的父物体
            Re_gameObject = image_info.transform.parent.gameObject;
            //设置父物体使得图像始终在背包界面最上层
            image = image_info;
            image.transform.SetParent(transform);
        }
    }

        当我们拖动物品时,我们需要将选中的的位置实时改变为鼠标的位置,但是,如果你直接这么做,你会发现当第一个格子上的物品往其它格子上移动时,物体就会被格子遮挡住,这是由于图层的排序导致的,解决这个方法的思路就是把物品的父子层级改为背包图层的子物体而不是原来的格子的子物体,这样就能解决。然后我们只需要拿到鼠标的位置,修改物品的位置为鼠标的位置就可以了。以下是代码(提醒一点,鼠标拿到的Vector3中z轴的值可能会特别离谱超出摄像机的检测范围,需要代码将z轴的值手动修正为)然后还有一点,我们释放物体时,希望物品不要挡住我们的识别(不然eventData.pointerCurrentRaycast.gameObject返回的始终会是你的物品)在物品上挂载Canvas Group接口,有一个Blocks Raycasts选项,默认勾选,当这个值为false即关闭时,物品就不会参与射线检测,这样就解决了这个问题。

        以下为代码实现,同样如果不需要实现shift拆分功能的不用管if(Shift_Drag)中的内容

    //在按住物品进行拖动时执行
    public void OnDrag(PointerEventData eventData)
    {
        if (image != null)//如果玩家手上有拖动中的物品
        {
            Vector3 New_Position = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            New_Position.z = 0;
            if (Shift_Drag)//进行的是拆分移动
            {
                int num = Text_Num(image);
                num--;
                if(num > 0)//数量不足够进行拆分
                {
                    //更改物品数量
                    image.GetComponentInChildren<TMP_Text>().text = num.ToString();
                    //复制一个物品
                    GameObject game = Instantiate(image, New_Position, Quaternion.identity);
                    //将物体的拆分转为移动中的物体而不是全部物体
                    game.transform.SetParent(Re_gameObject.transform);
                    game.transform.localScale = image.transform.localScale;
                    image.transform.SetParent(Re_gameObject.transform);
                    Current_image = image;
                    image = game;
                    //标记数字
                    image.GetComponentInChildren<TMP_Text>().text = "1";
                    Shift_Drag_End = true;//确认进行了拆分操作
                }
                Shift_Drag = false;
            } 
            image.GetComponent<CanvasGroup>().blocksRaycasts = false;//让物品不再被拖拽检测
            image.transform.position = New_Position;//实时改变物品的位置和鼠标保持一致
        }
    }

        接下来最最难的地方来了,当我们放下物体时,主要有6中情况,一是你放的位置不是一个有效的位置,二是你放的位置有物品但是不是同一种类型,三是是同一种类型,第四种则是你放在了空格子上,第五种也是你放在格子上但格子上有不是同一种的物品,第六就放在格子上但格子上有同一种的物品。未实名会有放在格子上还有物品这种逻辑?因为一般来说,你的物品的图像边界是小于格子的,因此有可能你将物品堆到格子而不是物品上。同理也就是为什么我说如果格子上还有一个空物体作为父物体不建议修改它的标签的原因。我们可以将以上的六种情况做简单的归并,就分为三种,一种是扔到了空格子上,一种扔到已有物品的的格子上,一种是扔到了背包外面。如果格子上有物品扔到了格子还是物体上只需要修改鼠标释放时进行一次判断,若是有物品但扔到了格子上,就把GameObject指向从格子改指为物品,这样就能复用处理逻辑,这也是我简化代码的思路。

   基本的判断逻辑代码注释已经比较详细了,我就不多解释了。No_gameobject.transform.childCount >= 1的判断逻辑是因为如果格子下无物体那么就没有子物体,但是这是我个人的背包系统情况,这有可能因为个人需要不同使得也有可能有子物体,所以使用前慎重思考。执行物品移动的逻辑就是,将物体的位置设置为这个格子,将这个格子的子物体设置为物品,就完成一次防止,如果是物品交换,只需要将格子上的物品放到原来拖动过来的物体的格子上,物品在放置到这个格子上(current_image的作用就是记录这个原物品组,虽然对Re_gameobject进行类似的父子关系操作就能代替current_image,不过我懒的再改了)

        接下来是代码部分,如果不考虑拆分操作,不需要管含有Shift_Drags()操作的内容(if语句内包含这个部分的都可以不看)

    //鼠标放开执行的操作
    public void OnEndDrag(PointerEventData eventData)
    {
        GameObject No_gameobject = eventData.pointerCurrentRaycast.gameObject;
        if (image != null && No_gameobject == null)//超出边界的情况
        {
            Goods_RePosition();
            image.GetComponent<CanvasGroup>().blocksRaycasts = true;//可以在次被检测拖拽
        }
        else if(image != null && No_gameobject != null)//如果玩家手上有拖动中的物品
        {
            if (No_gameobject.transform.CompareTag("item"))
            {
                //判断是否放开的位置是在格子上还是物品上,如果是格子看是否为空格子,是则直接将物品放入
                //不是,则把GameObject的指向的物体转换为这个物品并进行之后的操作
                if (No_gameobject.transform.childCount >= 1)//该格子上已有物品
                {
                    //将物体组件指向为该物品而不是格子
                    No_gameobject = No_gameobject.transform.GetChild(0).gameObject;
                    Goods_Change(No_gameobject);
                }
                else//对应情况为,格子上无物品,直接放入
                {
                    Position_Change(image, No_gameobject);
                    image.GetComponent<CanvasGroup>().blocksRaycasts = true;//可以在次被检测拖拽
                }
            }
            else if (No_gameobject.CompareTag("goods"))//放置的位置在物品上
            {
                Goods_Change(No_gameobject);
            }
            else//放置的不再任何合法位置上
            {
                Goods_RePosition();
            }
        }
        Shift_Drag_End = false;
        image = null;
    }

    //将指定为子物体的位置置于父物体,并修改为父子关系
    private void Position_Change(GameObject Son_GameObject,GameObject Fat_GameOject)
    {
        Son_GameObject.transform.position = Fat_GameOject.transform.position;
        Son_GameObject.transform.SetParent(Fat_GameOject.transform);
    }

    //返回物品数量,只限于物品本身没有脚本的情况的代码,不建议
    private int Text_Num(GameObject gameObject)
    {
        string text = gameObject.GetComponentInChildren<TMP_Text>().text;
        int.TryParse(text, out int u_text);//拿到物品的数量
        return u_text;
    }

    //合并物品组
    private void Shift_Drags(GameObject gameObject)
    {
        int G_Num = Text_Num(gameObject);
        int I_Num = Text_Num(image);
        G_Num += I_Num;
        gameObject.GetComponentInChildren<TMP_Text>().text = G_Num.ToString();
        Destroy(image);
        Current_image = null;
    }

    //比较是否是同一类物体,利用对比是否是同一图片,这是不建议的方式
    //实际身上应该使用在物品上挂载脚本设置数量,最大堆叠数量,编号等等,也能为后续增加更多功能
    //所以这是一种参考方法,主要是学习实现思想
    private bool Goods_Compare(GameObject gameObject)
    {
        return gameObject.GetComponent<Image>().sprite.name == image.GetComponent<Image>().sprite.name;
    }

    private void Goods_Change(GameObject gameobject)//将物体置于新位置
    {
        if (Goods_Compare(gameobject))//对应情况为,格子上有物品为同一组
        {
            Shift_Drags(gameobject);//合并两个物品组
        }
        else if (Shift_Drag_End)//对应情况为,格子上有物品但是不是同一组,并是拆分下的物品
        {
            Shift_Drags(Current_image);//将拆分出的物品放回原物体组
        }
        else//物品不是同一组但不是拆分情况
        {
            Position_Change(image, gameobject.transform.parent.gameObject);//先将传入的物品放入这个物品的格子上
            Position_Change(gameobject, Re_gameObject);//再将这个格子上的物体放入原传入物体的位置上
            image.GetComponent<CanvasGroup>().blocksRaycasts = true;//可以在次被检测拖拽
        }
    }

    private void Goods_RePosition()//将物体回归原位置
    {
        if (Shift_Drag_End)//传过来的是拆分出的物品
        {
            Shift_Drags(Current_image);//将物品放回原组
        }
        else//传过来的不是拆分出的物品
        {
            Position_Change(image, Re_gameObject);//将物品放回格子
            image.GetComponent<CanvasGroup>().blocksRaycasts = true;//可以在次被检测拖拽
        }
    }
}

        最后再来看看实现效果:(没有做拆分的是不能实现图中的拆分的)

猜你喜欢

转载自blog.csdn.net/WEIWEI6661012/article/details/130263993