数据存储的选择:尽量不要用多个字段存储多个格子,也不要用字符串,存储空间会变大。结论是使用二进制数据存储。
- 背包系统结构
关于背包 客户端与服务器端需要传输的结构,
//协议
message NBagInfo{
int32 Unlocked = 1; --解锁到第几个格子
bytes Items = 2; --背包里面有多少道具
}
message BagSaveRequest
{
NBagInfo BagInfo = 1;
}
message BagSaveResponse
{
RESULT result = 1;
string errormsg = 2;
}
利用unlock参数来决定Items长度。
服务器端:
创建背包相关表CharacterBag,
表结构为:
ID | int32
Items | Binary
Unlocked | Int
创建与Charater表的关联,关联关系为1比1,一个角色只能有一个背包。(多个背包页如何管理?)
在Character类初始化背包。
OnGameEnter→AddCharacter→Character→初始化背包。
this.Info.Bag = new NBagInfo(); this.Info.Bag.Items = this.Data.Bag.Items; this.Info.Bag.Unlocked = this.Data.Bag.Unlocked;
创建背包接收监听,
public class BagService : Singleton{ public BagService (){ MessageDistributer >.Instance.Subscribe (this.OnBagSave); } public void Init() { } void onBagSave(NetConnection sender, BagSaveRequest request) { Character character = sender.Session.Character; Log.InfoFormat("BagSaveRequest:character:{0},Unlocked{1}", character.Id, request.BagInfo.Unlocked) ; if (request.BagInfo != null) { character.Data.Bag.Items = request.BagInfo.Items; DBService.Instance.Save(); } }
在用户创建角色时给角色增加背包,在UserService→onCreateCharacter 背包初始化。
var bag = new TCharacterBag(); bag.Owner = character; bag.Items = new byte[0]; bag.Unlocked = 20; //解锁20个格子 character.Bag = DBService.Instance.Entities.CharacterBags.Add(bag);
客户端:
给客户端的物品Item的Model类增加配置表类ItemDefine,一旦产生新道具,就把配置赋予他,以便随时能得到新增加的道具的定义。
-
#Item类
public class Item { public int Id; public int Count; public ItemDefine define; public Item(NItemInfo item) { this.Id = item.Id; this.Count = item.Count; this.define = DataManager.Instance.Items[item.Id]; } }
之后设计BagItem的Model类,思考要如何设计比较好?
-
~~关于BagItem思考
考虑到背包有格子这种东西,我本来思索应该会用字典来存储Item,然后Key值为格子的位置,然后应该会用事件来加载人物所持有的数据,在UserService处创建一个网络连接,在登陆时获取到人物背包数据。
BagItem里面应该就是存储当前登陆人员的信息,物品在背包里的格子位置,物品Item的信息。
首先制作显示用的UI,创建一个TabView来管理背包页面,创建TabButton来管理单个背包按钮。将一个个背包页按钮抽象成TabButton,并且给自己添加按键监听,点击时触发TabButton的方法,下标传递给TabView做切换。
简单来说就是各做各的事情,Button管理自己的触发,和点击后图片的变换,TabView管理各个页面的切换。
TabView采用了IEnumerator Start() {}来实行他的生命周期,关于IEnumerator 。
-
#TabView类
public class BagTabView : MonoBehaviour { public BagTabButton[] ButtomList; public GameObject[] BagViewList; public int index = -1; // Use this for initialization IEnumerator Start() { for (int i = 0; i < ButtomList.Length; i++) { ButtomList[i].tabView = this; ButtomList[i].tabIndex = i; } yield return new WaitForEndOfframe(); SelectTab(0); } internal void SelectTab(int idx) { if (index != idx) { for (int i = 0; i < ButtomList.Length; i++) { ButtomList[i].Select(i == idx); //触发换页图片 BagViewList[i].SetActive(i == idx); //触发换页效果 } } } } -
#TabButton类
public class BagTabButton : MonoBehaviour { private Image tabImage; public Sprite activeImage; public Sprite normalImage; public BagTabView tabView; public int tabIndex; //下标 public bool selected = false; public void Start() { tabImage = this.GetComponent(); normalImage = tabImage.sprite; this.GetComponent
设计处理格子图片的类UIIconItem类,放置于每个格子的Image组件里。
-
#UIIconItem
class UIIconItem : MonoBehaviour { public Image mainImage; public Image secondImage; public Text mainText; internal void SetMainIcon(string icon, string text) { this.mainImage.overrideSprite = Resloader.Load(icon); this.mainText.text = text; } }
构建BagItem实体**(难点),该类主要采用Struct结构体类型,里面使用了结构布局[StructLayout(LayoutKind.Sequential,Pack=1)],**代表了结构体在内存中的存储格式。
-
结构体排序问题
[StructLayout(LayoutKind.Sequential) ]
Sequential :是指顺序布局,比如
struct S1{ int a ; int b ; }那么默认情况下在内存是先排a,再排b。
也就是如果能取到a的地址,和b的地址,则相差一个int类型的长度,4字节。
-
为什么用结构体而不是类?
使用结构体就是值类型,而类是引用类型。值类型更方便用'='互相赋值。做值的交换。
其中ItemId与Count 使用了ushort,该类型占2个字节,数据范围在0 ~ 65,535。若不够大小存储可换成int类型。
-
#BagItem
[StructLayout(LayoutKind.Sequential,Pack =1)] struct BagItem { public ushort ItemId; public ushort Count; public static BagItem zero = new BagItem {ItemId=0,Count=0}; public BagItem(int ItemId,int count) { this.ItemId = (ushort)ItemId; this.Count = (ushort)count; } //使用Operatior == 与Operatior !=重新定义符号的意义。 public static bool operator ==(BagItem lhs, BagItem rhs) { // return lhs.ItemId == rhs.ItemId && lhs.Count == rhs.Count; } public static bool operator !=(BagItem lhs, BagItem rhs) { return !(lhs == rhs); } public override bool Equals(object obj) { if (obj is BagItem) { return Equals((BagItem)obj); } return false; } public bool Equals(BagItem other) { return this == other; } public override int GetHashCode() { return ItemId.GetHashCode() *(Count.GetHashCode() <<2); } }
创建管理用的BagManager**(难点)**,单例类,初始化方法为Init(),
在UserService→onGameEnter 角色进入游戏时执行,并传入服务器发送过来的道具数据至Init()方法。
初始化 通过客户端接收并传入NBagInfo参数→通过BagItem的构造函数调用Analyze方法将NBagInfo参数解析成结构体数组(该方法是将字节数组解析成结构体数组)
-
Analyze方法解析(将字节流数组转成BagItem[]):
public BagItem[] Items; public unsafe void Analyze(byte[] items) { fixed (byte* pt = items) //指向items的指针(items数组的首地址) { for (int i = 0; i < this.Unlocked; i++) { //指的是items的首地址+i个BagItem的存储单元的偏移量。 //得到的item指针存储了经过i个BagItem存储单元的地址。 //可以理解为指向了第i个BagItem,存储了第i个BagItem的首地址。 BagItem* item = (BagItem*)(pt + (i * sizeof(BagItem))); //将该值赋予给本地数组Items[i] Items[i] = *item; } } } -
GetBagInfo方法解析(构建字节流)
private NBagInfo info; public unsafe NBagInfo GetBagInfo() { fixed (byte* pt = info.Items) { for (int i = 0; i < this.Unlocked; i++) { //数组的首地址+第i个物件的存储大小=第i个物体在数组中的位置。 BagItem* item = (BagItem*)(pt + i * sizeof(BagItem)); //将堆里该地址的值改为Items[i] *item = Items[i]; } } return this.info; }
之后利用Reset()方法对背包内大于物品叠加上限的道具进行拆分。
-
#BagManager
public class BagManager : Singleton
{ public int Unlocked; public BagItem[] Items; private NBagInfo info; internal void Init(NBagInfo info) { this.info = info; Unlocked = info.Unlocked; Items = new BagItem[Unlocked]; //开辟背包格的存储。 if (info.Items != null && info.Items.Length >= Unlocked) { Analyze(info.Items); } else { unsafe { this.info.Items = new byte[sizeof(BagItem) * Unlocked]; } Reset(); } } public void Reset() { int i = 0; foreach (var kv in ItemManager.Instance.Items) { if (kv.Value.Count <= kv.Value.define.StackLimit) { this.Items[i].ItemId = (ushort)kv.Key; this.Items[i].ItemId = (ushort)kv.Value.Count; } else { int count = kv.Value.Count; while (count> kv.Value.define.StackLimit) { this.Items[i].ItemId = (ushort)kv.Key; this.Items[i].Count = (ushort)kv.Value.define.StackLimit; i++; count -= kv.Value.define.StackLimit; } this.Items[i].ItemId = (ushort)kv.Key; this.Items[i].Count = (ushort)count; } i++; } } public unsafe void Analyze(byte[] items) { fixed (byte* pt = items) { for (int i = 0; i < this.Unlocked; i++) { BagItem* item = (BagItem*)(pt + (i * sizeof(BagItem))); Items[i] = *item; } } } public unsafe NBagInfo GetBagInfo() { fixed (byte* pt = info.Items) { for (int i = 0; i < this.Unlocked; i++) { BagItem* item = (BagItem*)(pt + i * sizeof(BagItem)); *item = Items[i]; } } return this.info; } }
UIBag
而后就是要在UIBag类里进行BagManager的初始化操作,BagManager初始化的操作需要使用协程,将初始化方法InitBags描述成IEnumerator***函数迭代器。***
该初始化的主要目的是将ItemManager里的Items数据逐个取出,并且在背包里Instantiate相应多的背包格子,然后再根据item里的define物品信息加载图标。
-
#UIBag
public class UIBag : UIWindow { public Text money; public Transform[] pages;//页数 public GameObject bagItem; //格子的数据 Listslots;//格子 // Use this for initialization void Start () { if (slots == null) { slots = new List (); } for (int page = 0; page < this.pages.Length; page++) { slots.AddRange(this.pages[page].GetComponentsInChildren (true)); //获取该页所有active状态的Image组件。 } StartCoroutine(InitBags()); //启动背包管理器初始化协程。 } public void SetTitle(string title) { this.money.text = User.Instance.CurrentCharacter.Id.ToString(); } IEnumerator InitBags() { for (int i = 0; i < BagManager.Instance.Items.Length; i++) { //根据物品遍历。 var item = BagManager.Instance.Items[i]; if (item.ItemId > 0) { //itemId不为空 GameObject go = Instantiate(bagItem, slots[i].transform);//在格子上生成物品的图标。 var ui = go.GetComponent (); var def = ItemManager.Instance.Items[item.ItemId].define; ui.SetMainIcon(def.Icon,item.Count.ToString()); } } for (int i = BagManager.Instance.Items.Length; i < slots.Count; i++) { // slots[i].color = Color.gray; } yield return null; } public void onReset() { BagManager.Instance.Reset(); } }
而后还有关于背包UI的优化:
具体有以下几点:
1,使用对象池重复利用背包里的Item对象。
2,动静分离,分离需要滑动的ScrollView至单独的Canvas中。
3,调整背包ScrollView里Content的pivot点,然后不使用Unity的自动调节组件,手动摆放Item,其中:列=该格子的列数*格子宽度,行=该格子的行数*格子高度
4,滚动时,监控content的y轴变动(如果他是往下滚动的),并通过ViewPort的长度和格子的长宽,来获取哪些格子应该被显示,哪些被隐藏。



