unity3d制作背包系统(3)–UI部分
UI这块比较大,花了挺长时间从项目中抠代码,也发现了原来项目中有这么多垃圾代码。这部分既要写代码,又要在unity3d中调整UI。
注:这篇文章下面的所有“格子”都代表UI显示上的格子,“物品”仍代表我们第一章定义的物品(itemunit)
0.总览
本文分为三个部分,第一部分主题为定义格子,第二部分为管理格子,第三部分为总的UI管理
红框内为背包系统UI在unity中的结构,黄框为背包UI的背景(与当前主题无关)
给packgrid添加Grid Layout Group来将格子排列整齐
每个格子由以下三部分组成
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的功能,开关背景以及给格子获取鼠标手持的功能。
背包系统的其他笔记
文章正在完善中,如有错误或不合理的地方还请谅解