unity3d制作背包系统(3)--UI部分

unity3d制作背包系统(3)–UI部分

UI这块比较大,花了挺长时间从项目中抠代码,也发现了原来项目中有这么多垃圾代码。这部分既要写代码,又要在unity3d中调整UI。

注:这篇文章下面的所有“格子”都代表UI显示上的格子,“物品”仍代表我们第一章定义的物品(itemunit)

0.总览

本文分为三个部分,第一部分主题为定义格子,第二部分为管理格子,第三部分为总的UI管理
UI布局
红框内为背包系统UI在unity中的结构,黄框为背包UI的背景(与当前主题无关)
在这里插入图片描述
在这里插入图片描述
给packgrid添加Grid Layout Group来将格子排列整齐
packgrid
slot
每个格子由以下三部分组成
slotheriachy
slot是格子的背景兼按钮,rawimage负责显示物品的贴图,text显示物品数量
灵魂绘图

1.定义格子与“手”

为了将UI的格子与存储部分中的物品槽一一对应,我们像定义物品一样定义UI格子。同时我们将描述“左键单击拿取格子中物品到鼠标”这样的逻辑放在格子上,这样可以使管理格子的UI无需处理复杂的逻辑。

“手”

鼠标拿取时,物品的图片会跟随鼠标,这就要求得有一个稍微特别的“格子”跟着鼠标移动,因此我们定义这个特别的“格子”叫mousehand
在这里插入图片描述
可以看到mousehand和格子slot的结构基本一样,下面所有的“鼠标持有”就代表了这个mousehand的内容物品itemunit
跟随鼠标移动

RectTransform rect;
void Update()
    {
        rect.position=Input.mousePosition; //跟随鼠标,当canvas的rendermode不是overlay的时候不可用
    }

“手”所承载的物品及获取、设置方法

public itemunit holding=new itemunit();
bool taking = false;
public itemunit getholding()
    {
        return holding;
    }
public void setholding(itemunit item)
    {
        if (!item.isempty())
        {
            if (holding.id != item.id)//因itemunit类是引用所以要拿个新的itemunit
                holding =  idtoitemunit.idtoitem(item.id, item.subid);
            holding.copyinfo(item);
        }
        else
        {
            holding = new itemunit();
        }
        handler.setpicture(holding);//需要一个静态的ID,texture对应表
        if (!holding.isempty()) taking = true;
        else taking = false;
    }

初始化方法

numtexthandler handler;
void Start () 
    {
        rect = GetComponent<RectTransform>();     
            handler = GetComponentInChildren<numtexthandler>();
            handler.setpicture(holding);//需要一个静态的ID,texture对应表     
    }

刷新显示方法

public void flush()//
    {
        handler.setpicture(holding);//将显示的图片设置为承载的物品对应的图片        
    }

对承载物品进行加减数量方法

public int addnum(int num)
    {
        int temp= holding.addnum(num);
        flush();
        return temp;
    }
public int subnum(int num)
    {
        int temp = holding.subnum(num);
        flush();
        return temp;
    }

直接设置承载物品数量的方法

public void setnumber(int numb)
    {
        holding.num=numb;
        flush();
    }
格子

实现格子功能的类被称为slotbutton

1.下面代码中的idtoitemunit.idtoitem功能是根据给定的物品id获取对应物品的itemunit实例.
2.uicontroller代表管理格子的UI
3candrain代表该格子内的物品能否被拿完,是被创造模式物品栏使用的功能,本来应该另外做一个类继承slotbutton来实现这个功能,这是一个失败的地方
4.takeonly是该格子内的物品是否只允许从中拿取而不能放下(参见我的世界工作台UI的产出格及火炉UI的产出格),属于延伸功能,对讲解背包系统无用,同样应该用继承实现,现在此功能与原本格子的逻辑耦合在一起,难以分开。
5.格子代码中的tex,其setholding方法的功能是将给入的itemunit显示成图片及数量,可以理解为刷新UI显示。

①首先实现格子的一些显而易见的功能:
设置内容物品:

public itemunit holding;
public void setholding(itemunit ite)//设置存储物品,不会触发UI变化事件
    {
        if (candrain)//如果可拿完
        {
            if (ite == null)
            {
                holding = new itemunit();
            }
            else
            {
                if (!ite.isempty())
                {
                    if (holding.id != ite.id)//因itemunit类是引用所以要拿个新的itemunit
                        holding = idtoitemunit.idtoitem(ite.id, ite.subid);
                    holding.copyinfo(ite);
                }
                else holding = new itemunit();
            }
        }
        tex.setpicture(holding);
    }

外部获取格子内容物品

public itemunit getholding() { return holding; }

鼠标进入或移出格子的处理方法:

bool isover;
 public void OnPointerExit(PointerEventData eventData)
    {//当鼠标光标移出该对象时触发
        isover = false;
        showiteminfo.showinfo(null);//取消显示物品信息到UI上
    }
public void OnPointerEnter(PointerEventData eventData)
    {//当鼠标光标移入该对象时触发
        isover = true;
        showiteminfo.showinfo(holding);//显示物品信息到UI上
    }
void Update()
    {
        if (isover&&Input.GetKeyDown(KeyCode.Mouse1))
            rightevent();//当鼠标在格子上且按下右键       
    }

被鼠标左击或右击,内容将在后面定义

public void leftevent(){}
public void rightevent(){}

以及用于标识该格子是隶属的UI的哪个格子的ID字段

public uicontroller uicon;//隶属于的ui
public int id;

一些初始化代码

void Start () {//hand将在下面定义
        hand = backpackmanager.getmouse();//必须在start调用,因为getmouse是在awake阶段注册的
        if (holding == null)
        {
            holding = new itemunit();
        }
        tex.setpicture(holding);
    }
    
public void setid(int ids, uicontroller uic)//在管理UI初始化时被调用
    {
        uicon = uic;
        id = ids;
        tex = transform.GetChild(0).GetComponent<numtexthandler>();
    }

②然后我们梳理一下要实现的逻辑:

先描述左键的逻辑:
1.当鼠标持有空,被点击格子装有某物品时,将格子物品与鼠标持有交换
2.当鼠标持有某物品,被点击格子空时,将格子物品与鼠标持有交换
3.当鼠标持有某物品,被点击格子装有某物品时,若两种持有的物品为同种物品,则将鼠标持有物叠加到格子上,直到格子内物品的数量等于最大堆叠数;若两种持有的物品非同种物品,则将格子物品与鼠标持有交换
右键的逻辑:
1.当鼠标持有空,被点击格子装有某物品时,鼠标持有拿取一半的格子内物品
2.当鼠标持有某物品,被点击格子空时,鼠标持有放置数量为一的物品到格子上
3.当鼠标持有某物品,被点击格子装有某物品时,若两种持有的物品为同种物品,则鼠标持有放置数量为一的物品到格子上;若两种持有的物品非同种物品,则将格子物品与鼠标持有交换

可以看到我们要实现的功能有:
0.首先为了实现下面的功能,需要格子知道鼠标手持

mousehand hand;

1.交换鼠标持有与被点击格子的内容物品

 void exchange(itemunit ite)//交换,只能和手持换,已确保手持与格子不同
    {       
            if (candrain)
            {
                if (!takeonly)//不是只能拿取则交换
                {
                    if (ite.isempty())
                    {//手空
                        hand.setholding(holding);
                        setholding(new itemunit());
                    }
                    else
                    {//手不空
                        hand.setholding(holding);
                        setholding(ite);
                    }
                }
                else//只能拿取
                {
                    if (ite.isempty())
                    {
                        hand.setholding(holding);
                        setholding(ite);
                    }
                }
            }
            else //无限物品
            {
                if (ite.isempty())//手持为空
                    hand.setholding(holding);//手持等于格子
                else if (ite.equal(holding))//手持和格子相等
                {
                    hand.addnum(holding.num);//手持加格子数量
                }
            else {
                hand.setholding(new itemunit());//手持等于格子
                }
            }
    }

2.鼠标持有物叠加到格子上

 void adder(itemunit num)//默认从手持拿东西加到格子,要保证是同种物品
    {
        if(num.equal(holding)&&!num.isempty())
        { if (candrain)
            {
                if (!takeonly)//加格子
                {
                    int temp = holding.addnum(num.num);
                    hand.setnumber(temp);
                }
                else//加手持
                {
                    int temp = hand.addnum(holding.num);
                    holding.num = temp;
                    flush();
                }
            }
            else hand.addnum(holding.num);
        }
    }

3.鼠标持有拿取一半的格子内物品

 void takehalf(itemunit ite)//格子减半,只能和手持换,已确保手持空
    {       
            if (candrain)
            {
                int numbt = (holding.num + 1) / 2;
                hand.setholding(holding);
                hand.setnumber(numbt);//手数量加n/2
                holding.subnum(numbt);//格子内数量减n/2
            }
            else
            {
                int numbt = (holding.num + 1) / 2;
                hand.setholding(holding);
                hand.setnumber(numbt);//手数量加n/2  
            }
    }

4.鼠标持有放置数量为一的物品到格子上

void addone(itemunit ite)//格子加一,只能和手持换,已确保手持与格子同
    {//已确保手持非空
        if (ite.equal(holding))//如果手持等于格子
        {
            if (takeonly) { }
            else if(candrain)
            {
               if(holding.addnum(1)<=0)//添加成功
                    ite.subnum(1);                
            }
        }
        else//如果手持不等于格子
        {
            if (takeonly) { }
            else if (candrain)
            {
                if (holding.isempty())//如格子空
                {
                    setholding(idtoitemunit.idtoitem(ite.id, ite.subid));
                    holding.copyinfo(ite);
                    holding.num = 1;//格子数量加1
                    hand.subnum(1);
                }              
            }
        }
    }

由于格子完整代码太长(近300行),所以完整代码将以文件的形式随demo放出.

2.管理格子的UI

有了格子,自然需要管理格子的UI,格子和管理UI的关系,就像存储部分描述的,物品与存储它们的存储区之间的关系。这样我们的格子不需要自己去跟存储区通信,而是通过接收管理格子之UI的调用来响应变化。
在这里插入图片描述

public GameObject opener;//打开ui的物体,
public GameObject slotbut;//用于扩容的格子

public string uiname;//用来给backpackmanager建立name和uicontroll对应关系
public slotbutton[] slots;//下属的格子
 backpackmanager manager;//backpackmanager是管理uicontroller的类

1.为了与存储部分通信,我们需要定义一个事件类来描述UI上发生的变化,称这个事件类为uievent:

public class uievent  {
//因为这个背包系统没有单独功能的按钮及滚动条,因此uieventtype
//只有slotchange是有意义的
    public int id=0;//slotbutton的id,或buttpress的ID
    public uieventtype type;
    public bool left=true;//是被左击还是右击
   public itemunit item;//发生变化的格子中的物品
    public uievent()
    {
        id = 0;
        type = uieventtype.slotchange;
    }
}
public enum uieventtype
{
    slotchange,buttpress,processdone,scrollmove
}

2.当UI被打开时,需要与打开它的GameObject建立事件通道及获取存储区物品用于显示

bool isopen;//当前UI是否打开状态
public virtual void onopen(GameObject open)
    {
        if(opener!=null)
        opener.GetComponent<backpackbase>().unlisten(flush);//解除上次打开UI建立的监听
        opener = open;
        open.GetComponent<backpackbase>().listen(flush);

        itemunit[] storageitem = open.GetComponent<backpackbase>().backpack;//同步UI与存储区
        for (int i = 0; i < Mathf.Min(storageitem.Length,slots.Length); i++)
        {
            slots[i].setholding( storageitem[i]);
        }
        
        isopen = true;
        gameObject.SetActive(true);
    }

3.对来自存储区的backpackevent的接收方法

public virtual void flush(backpackevent bev)
    {        
       if (bev.type == beventtype.slotchange)
        {
            if (slots.Length > 0)           
                slots[bev.id].setholding(bev.item);
        }
    }

4.UI下属格子有左击和右击事件发生,需要接收它们的事件(注意这是uicontroller的左右击响应方法不是slotbutton的左右击响应方法)

protected uievent eventclass=new uievent();//ui事件类
public virtual void leftevent(int ids,itemunit hold)//左键事件,被下属slotbutton调用
    {
        if (eventclass == null) eventclass = new uievent();
        eventclass.left = true;
        eventclass.id = ids;
        eventclass.type = uieventtype.slotchange;
        eventclass.item = hold;
        if (opener != null) opener.GetComponent<backpackbase>().uionchange(eventclass);//发送UI改变事件给存储区
    }
public virtual void rightevent(int ids, itemunit hold)
    {
        if (eventclass == null) eventclass = new uievent();
        eventclass.left = false;
        eventclass.id = ids;
        eventclass.type = uieventtype.slotchange;
        eventclass.item = hold;
       if(opener!=null) opener.GetComponent<backpackbase>().uionchange(eventclass);//发送UI改变事件给存储区
    }

5.UI强行关闭方法及获取UI开或关状态的方法

public void onclose()
    {
        isopen = false;
        gameObject.SetActive(false);
    }
public bool getopen()
    {
        return isopen;
    }

初始化方法

public void Awake()
    {
        slotbut = Resources.Load("prefabs/slot")as GameObject;
    }
public virtual void init()//在backpackmanager的awake被调用
    {
        for (int u = 0; u < slots.Length; u++)//开背包调用的是这个
        {
            slots[u].setid(u, this);//给slots分配id和uicontroller
        }
    }

为了文章的不至于太长,完整代码同样以文件形式在将来给出

3.管理与背包相关的UI

在正式的游戏中我们不可能只有一个背包界面的UI,我们会有合成界面,场景物体交互界面等,不同的GameOjbect会打开相同或不同的UI,因此我们需要一个管理所有上部分中“管理UI”的脚本,这里称之为backpackmanage

首先UI是被场景中物品开启的(比如箱子被打开、玩家按下开背包键等)
一般我们并不是道谁何时会打开UI,因此直接在backpackmanager定义一个静态delegate供其调用

public delegate uicontroller uiopen(string name,uitype type, GameObject opener);
public delegate mousehand getmousehand();

public class backpackmanager : MonoBehaviour {

    public static uiopen openui;//让开启UI随时可调用
    public static getmousehand getmouse;//给格子获得鼠标手持
}

backpackmanager内的一些数据

public uicontroller[] ui ;//被管理的UI
public mousehand hand;//鼠标手持
public GameObject bg;//打开任何UI后的背景
public int openuilayer = 0;//开了几层UI

将注册到getmouse静态delegate的方法

public mousehand gethands()
    {
        return hand;
    }

将注册到openui静态delegate的方法

 public uicontroller onopen(string name, uitype type, GameObject opener)//按照名字开启UI
    {//开启成功返回UICONTROLLER,失败返回null

        uicontroller cont;      
        cont = finduibyname(name);//要加判断该gameobj是否具有开启条件
        if(!cont.getopen())
        cont.onopen(opener);
        else cont.onclose();
        openuibg(cont.getopen());//开关背景
        switch (type)
        {
            case uitype.backpack:
            if (name == "背包" && !cont.getopen())//给予背包关闭则全部UI关闭的权限
            {
                 for (int i = 0; i < ui.Length; i++)
                {
                    if (ui[i] != null)//防止关空UI或再次调用背包的openUI造成死循环
                    {
                            if (ui[i].getopen())
                            {
                                ui[i].onclose();
                                openuibg(ui[i].getopen());//开关背景
                            }
                    }
                }           
            }
                break;          
        }
       
        return cont;        
    }

openuibg方法,当openuilayer大于0时开启bg否则关闭bg

 public void openuibg(bool isopen)
    {        
        if (isopen)
        {
            openuilayer++;
        }
        else
            openuilayer--;

        if (openuilayer <= 0)
        {           
            bg.SetActive(false);
           openuilayer = 0;
        }
        else if (openuilayer >= 1)
        {
            bg.SetActive(true);
        }     
    }     

根据UI名字寻找uicontroller的功能,仅在backpackmanager内使用

uicontroller finduibyname(string name)
    {//现在所有uicontroller以数组形存储,可以考虑换成字典来实现
        for(int p=0;p<ui.Length;p++)
        {
            if (ui[p] != null)
            {
                if (ui[p].uiname == name)
                {
                    return ui[p];
                }
            }
        }              
        return null;
    }   

初始化方法

void Awake () {
        openui = new uiopen(onopen);
        if(hand==null)//找到鼠标手持
        {
            hand = GameObject.Find("mousehand").GetComponent<mousehand>();            
        }             
        getmouse = new getmousehand(gethands);//获取鼠标手设为静态

         ui = GetComponentsInChildren<uicontroller>();       
        for (int i = 0; i < ui.Length; i++)
        {
            ui[i].init();
        }
        bg.SetActive(false);
    }

总的来说backpackmanager提供了按名字开启UI的功能,开关背景以及给格子获取鼠标手持的功能。

背包系统的其他笔记

整体结构
上一篇:存储部分
[下一篇]

文章正在完善中,如有错误或不合理的地方还请谅解

猜你喜欢

转载自blog.csdn.net/ardepomy/article/details/88746764