这篇笔记讨论的是吃鸡类型背包系统的实现。
功能需求
- 查看玩家拥有的所有物品,可以放下其中的任意物品;
- 查看已死的他人背包的物品,可以拿起其中的任意物品;
- 查看附近的物品,可以拿起其中的任意物品;
- 拿起物品时如果已达到背包容量上限,无法放进背包,并弹出提示;
- 背包里的物品数据要能json序列化,能带出场景;
- 背包里能放不同类的物品:弹匣、医疗包、手榴弹等;
数据结构设计
- 玩家从场景拿起一个物品放进背包时需要把该物品销毁,物品的数据要记入背包脚本,这个数据不在场景里存在,不能继承Monobehaviour。从背包放下物品时需要把物品实例化,能挂在场景里物体上的脚本一定继承Monobehaviour。所以一种物品一个有继承MonoBehavior和不继承MonoBehavior的脚本,分别对应场景里和背包里的情况,放入和拿出背包时要在两种脚本之间转换。
- 物品的详细信息记录在配表里,物品实例记录一个能找到自己详细信息的id,和这个物品实例独立的数据(一个弹匣里剩余的子弹数)。
- 配表和背包都面临的一个问题是:物品有不同类的,不同类的物品有共同属性,如名字、重量、图标、预制体,每类物品又有独有的属性,如医疗包的治疗值、手榴弹的伤害。对于配表,这些物品是共用一个配表还是每类一个配表?对于背包不同类物品用一个列表放还是每类一个列表?我看了一下地铁逃生的仓库面板:
物品出现的顺序是按分类出现的,说明用户数据文件就是每类物品用了一个列表存储,配表也是每类物品一个配表。虽然这个面板上用看起来一样的格子,但它们链接的数据所在的列表不同。显示面板时也确实把仓库里所有类物品的列表都读了一遍。(我突然发现这里两个三级头也各占了一个格子,也就是说这些物品右下角的数字永远是1,这个数字也就没有用)
和平精英的背包面板。无论捡起东西的顺序怎么样,物品在这个列表里是按分类排列的,而且同一类里显示的顺序是定死的。显示背包物品的时候可能是按配表的顺序读取物品的。
配表设计
使用ScriptableObject做配表
ScriptableObject可记录Assets里各种类型的资源,如GameObject、Sprite、AudioClip,无需记录预制体路径然后Resources.Load()。
需要一个资源管理器得到这个Asset。
//以预制体为中心,通过预制体路径找到预制体,再通过它身上脚本的字段找到ScriptableObject里的详细信息
class ItemData:ScriptableObject{//记录物品详细信息的
public List<ItemDetail>() itemsDetail;
}
class ItemDetail{
int id;
string name;
int load;
GameObject prefab;
Sprte icon;
}
class ItemInPack{//放在背包里的
int id;
}
//因为背包里可能装各种类型的物品,所以用总的类ItemInPack装
class MagInPack:ItemInPack{//放在背包里的弹匣
int roundNum;
}
class Item:MonoBehaviour{//挂在物品上的
int id;
}
class Mag:Item{
int roundNum;
}
使用Json做配表
ItemInPack通过一个id从json反序列化结构获得详细信息。
//以json反序列化的结构体为中心,通过id查找详细信息
public class ItemData{
public int id;
public string name;
public string prefabPath;
public string iconPath;
public int load=1;
}
class ItemInPack{
int id;
}
class MagInPack:ItemInPack{
int roundNum;
}
挂在物品上的class Item:MonoBehaviour{
int id;
}
class Mag:Item{
int roundNum;
}
父类装子类遇到json持久化的问题
弹匣、医疗包、饮料、手榴弹属于不同类物品,一开始我试图用它们的基类Item列表记录它们,列表元素为不同子类,用LitJson序列化成Json文件也没问题。问题在于把这个Json文件反序列化时LitJson不会把它们识别为对应的子类,只会读出来一个全是Item的列表,子类多出的信息全部丢失。除非自己写读Json的程序。然后这个设计就放弃了。要持久化的列表必须每个元素结构相同。
[Serializable]
public class ItemsInPack{
public List<MagInPack> magsInPack;
public List<MedicInPack> medicsInPack;
public ItemsInPack(){
magsInPack = new List<MagInPack>();
medicsInPack=new List<MedicInPack>();
}
}
附近物品列表及其维护
背包脚本声明了一个物品基类列表记录周围可拾取的物品:
Physics.OverlapSphere()检测附近物品
打开背包界面时用Physics.OverlapSphere()得到一个球形区域的碰撞体,筛选出可拾取的、没有主人的物品,加入附近物品列表。加之前要先把附近物品列表清空。
public float pickUpRange=2;
public LayerMask packCheckLayerMask;
public void CheckItemsAround(){
itemsAround.Clear();
packsAround.Clear();
Collider[] thingsAround=Physics.OverlapSphere(transform.position,pickUpRange,packCheckLayerMask);//检测周围球形区域里的物品
Item item;
Weapon gun;
MyCharacter other;
for(int i=0;i<thingsAround.Length;i++){
if(thingsAround[i].TryGetComponent(out item)){//检测到是物品
if(item is Weapon){
gun=item as Weapon;
if(gun!=myCharacter.rifleScript&&gun!=myCharacter.pistolScript&&gun.owner==null){//不是人物自己的枪&&枪没有主人
itemsAround.Add(item);
}
}
}
else if(thingsAround[i].TryGetComponent(out other)){//检测到是人
if(other.life<=0&&other.backpack){//人已经死了&&人有背包
for(int j=0;j<other.backpack.magsInPack.Count;j++){
packsAround.Add(other.backpack);
}
}
}
}
}
类的继承关系
物品格子可以按在背包里、在别人背包、在场景里分为3类,也可以按物品种类分,然后就形成了一个“类矩阵”,脚本数量会比较恐怖。
首先提取所有物品格子的最大共性,只有图标和名字。
因为同一类物品3个地方的格子有相同的详细信息,所以适合继承一个基类。
写到这里发现在其他游戏里不同类物品一般都在背包的不同页签显示,吃鸡背包存数据也必须按类型分开存,但是混在一起显示,对程序员不太友好,但是对玩家友好。
需要做的测试
1.点开背包界面,显示的物品和人物拥有的一致
2.走近其他物品,点开背包界面,其他物品出现在附近物品栏
3.点拥有的枪或物品,把它放下,此物品出现在附近物品栏;如果是弹匣这样在背包里没有实体的物品,附近应该出现它的实体
4.捡起身上没有的物品,此物品从附近物品栏消失,加入拥有的物品;如果是弹匣,此物品在场景里的实体消失
5.和附近的枪交换
6.打开背包界面查看附近的物品,按F拾取,再打开背包界面,该物品从附近物品栏消失
7.打开背包界面查看附近的物品,按F放下枪,再打开背包界面,该物品出现在附近物品栏
总结
- 玩家通过UI界面操作改变脚本里的数据时,为了保持脚本里和界面上的数据一致,UI界面的数据刷新时必须总是从脚本读取,而不能直接修改UI界面,比如从场景拿一个弹匣到背包里,如果用代码把附近物品栏的这个弹匣删掉,再在背包物品栏加上这个弹匣,实际上进行了脚本数据和UI界面的两线操作,造成了两边数据不一致的可能性。
- 不要试图用一个脚本处理很多种情况,比如玩家背包里的弹匣、场景里的弹匣、别人背包里的弹匣共用一个脚本,再在里面做不同处理,会很难读。
- 然后我发现让一个预制体的脚本里一个字段指向预制体它自己,预制体实例化后这个字段指向的也是场景里的实例它自己了。不能让一个预制体脚本里的字段指向它自己来记录预制体。
医疗系统
要在此基础上增加医疗功能,需要几个方面的修改:
物品系统
增加医疗物品的配表,物品管理器增加根据id查询医疗物品详细信息的方法。
背包系统
数据结构
我尝试过用一个物品基类列表储存背包里的各类物品,如果不需要Json持久化,就没问题。如果Json持久化,LitJson无法把Json列表里的物品反序列化成各种子类。为了能Json持久化,需要给每类物品建一个列表。
背包界面
医疗物品有使用和丢弃两种选项需要增加两个按钮,还要增加选中的物品的标记,背包界面要增加一个记录选中物品的字段selectedItem。
选中医疗物品时先把selectedItem设置成选中的,把所有物品的选中标记关闭,再把选中物品的选中标记打开。
按使用时播放治疗的动画,动画播放到后半段执行治疗效果。
动画系统
治疗的动画放在上半身层,也就是允许治疗中移动。可以从双手的各种武器状态进入治疗动画,结束后根据武器状态gunStatus决定回到哪个武器状态。治疗动画通过trigger触发。
为了从拿枪状态平滑过渡到治疗状态,触发治疗后先播放收回枪的动画,结束后进入治疗,为此增加了一个放回枪的状态,以和gunStatus变化触发的收回枪区分开。治疗结束后先进入拿出枪状态再进入端枪状态。
治疗动画的后半段放动画事件执行治疗效果,注意不要放到状态过渡的时间内,否则动画事件不执行。
治疗效果
治疗效果包括
- 人物HP增加;
- HP界面刷新;
- 背包里使用的医疗包数据移除、背包已用容量刷新;
- 如果开着背包界面,背包界面也要刷新;
人物需要知道自己用的是背包里哪个医疗物品,也就是这个物品在医疗物品列表里的索引,根据索引得到医疗物品在配表里的id,去配表查这个物品的治疗量和占用容量,以执行治疗效果。因为从触发治疗到治疗生效有一个延时,需要声明一个正在使用的物品的索引。我把这个索引放在人物脚本。
触发治疗:
public virtual void UseHeal(int medicIndex){
animator.SetTrigger(healPara);
medicKitToUsedIndex=medicIndex;
}
治疗生效(这里没有写刷新HP和背包界面的代码,在玩家人物脚本里重写时写了):
int medicKitToUsedIndex;//治疗从按下到生效有一个延时,需要记录将使用的医疗包在背包里的索引
public virtual void HealEvent(){
int healValue=ItemDataManager.Instance.GetMedicData
(backpack.itemsInPack.medicsInPack[medicKitToUsedIndex].id).healNum;
life+=healValue;
backpack.itemsInPack.medicsInPack.RemoveAt(medicKitToUsedIndex);
medicKitToUsedIndex=-1;
}
交互系统
在交互检测的拾取物品的部分增加对医疗物品类型的判断,通过场景里的医疗物品身上的脚本记录的id去物品管理器查询这种医疗物品的名字,显示在交互选项里。
执行拾取时增加对医疗物品类型的判断,在背包的医疗物品列表增加此物品的数据。
技术困难
背包系统想支持弹匣、医疗物品、手榴弹这几类物品。基于不同类物品分配表、用户数据分列表存储的设计架构,每类物品的数据准备都需要
- 配表;
- 用户背包数据;
- 用户仓库数据;
ui需要:
- 背包内物品脚本;
- 附近物品脚本;
- 他人背包物品脚本;
- 仓库界面的背包物品脚本;
- 仓库界面的仓库物品脚本。
每增加一类物品都要做大量相似但不相同的工作。
唐老狮的背包系统教程总结
总结几个要点
- 不同类物品使用同一个配表,用type记录类型,有所有物品可能的属性,允许有些属性对有些类物品无效;
- 数据管理器用以id为键的字典存配表,有一个把列表转换成字典的步骤;
- 玩家拥有的物品按类型分成几个列表存;
- 不同类物品格子共用一个脚本,用枚举标记类型,