Unity背包系统笔记

一个物品在游戏里需要几个类?

  1. 配置数据类(详细数据);
  2. 玩家数据类(玩家拥有的这个物品的数据,剩余耐久度、数量);
  3. 格子类,只显示图标和边角的一些信息,但点击需要显示各类物品的详情,所以需要记录物品类型;
  4. 详情页类,不同类物品详情页版式可能不一样,也可能一样;

1、2使用继承,而3各类物品的格子看起来基本一样,再继承会造成类爆炸,所以不如使用一个格子类,用type枚举标记物品类型。4的种类通常少于物品类,也适合用type。

功能需求

  1. 查看玩家拥有的所有物品,可以放下其中的任意物品;
  2. 查看已死的他人背包的物品,可以拿起其中的任意物品;
  3. 查看附近的物品,可以拿起其中的任意物品;
  4. 拿起物品时如果已达到背包容量上限,无法放进背包,并弹出提示;
  5. 背包里的物品数据要能json序列化,能带出场景;
  6. 背包里能放不同类的物品:弹匣、医疗包、手榴弹等;
  7. 对于可以使用的物品,可以选择使用;

数据结构设计

  • 玩家从场景拿起一个物品放进背包时需要把该物品销毁,物品的数据要记入背包脚本,这个数据不在场景里存在,不能继承Monobehaviour。从背包放下物品时需要把物品实例化,能挂在场景里物体上的脚本一定继承Monobehaviour。所以一种物品一个有继承MonoBehavior和不继承MonoBehavior的脚本,分别对应场景里和背包里的情况,放入和拿出背包时要在两种脚本之间转换
  • 物品的详细信息记录在配表里,物品实例记录一个能找到自己详细信息的id,和这个物品实例独立的数据(一个弹匣里剩余的子弹数)。
  • 因为每类物品的详情字段不同,所以每类物品一个配表。详情页对于背包外物品,可用操作都是拾取,对背包内物品都可以丢弃,有的能使用,有的不能使用。有效物品种类可以共用详情页,有些不能。
  • 背包都面临的一个问题是:物品有不同类的,不同类的物品有共同属性,如名字、重量、图标、预制体,每类物品又有独有的属性,如医疗包的治疗值、手榴弹的伤害。对于背包不同类物品1.用一个列表放还是每类一个列表?2.格子除了按物品类分,还分为玩家背包的格子、附近物品格子、别人背包格子。用一种处理所有类物品还是制作多种格子?我看了一下地铁逃生的仓库面板:

 物品出现的顺序是按分类出现的,说明用户数据文件就是每类物品用了一个列表存储,配表也是每类物品一个配表。虽然这个面板上用看起来一样的格子,但它们链接的数据所在的列表不同。显示面板时也确实把仓库里所有类物品的列表都读了一遍。(我突然发现这里两个三级头也各占了一个格子,也就是说这些物品右下角的数字永远是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类,也可以按物品种类分,然后就形成了一个“类矩阵”,脚本数量会比较恐怖。

实用经验 87 切记继承过度滥用_过度继承-优快云博客

首先提取所有物品格子的最大共性,只有图标和名字。

因为同一类物品3个地方的格子有相同的详细信息,所以适合继承一个基类。

写到这里发现在其他游戏里不同类物品一般都在背包的不同页签显示,吃鸡背包存数据也必须按类型分开存,但是混在一起显示,对程序员不太友好,但是对玩家友好。

需要做的测试

1.点开背包界面,显示的物品和人物拥有的一致

2.走近其他物品,点开背包界面,其他物品出现在附近物品栏

3.点拥有的枪或物品,把它放下,此物品出现在附近物品栏;如果是弹匣这样在背包里没有实体的物品,附近应该出现它的实体

4.捡起身上没有的物品,此物品从附近物品栏消失,加入拥有的物品;如果是弹匣,此物品在场景里的实体消失

5.和附近的枪交换

6.打开背包界面查看附近的物品,按F拾取,再打开背包界面,该物品从附近物品栏消失

7.打开背包界面查看附近的物品,按F放下枪,再打开背包界面,该物品出现在附近物品栏

医疗系统

要在此基础上增加医疗功能,需要几个方面的修改:

物品系统

增加医疗物品的配表,物品管理器增加根据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去物品管理器查询这种医疗物品的名字,显示在交互选项里。

执行拾取时增加对医疗物品类型的判断,在背包的医疗物品列表增加此物品的数据。

手榴弹和背包的关系

根据设计,玩家可以在背包界面点手榴弹然后点使用来装备手榴弹。

然后这个手榴弹还应该在背包里吗?如果还在背包里,再次点击要么不显示使用,要么点击使用无效。如果不显示使用,那么手榴弹格子显示详情页的代码就要加判断,而且不显示使用会让玩家迷惑。如果点击使用无效,则是在点击使用的回调里判断,这个格子指向的数据对象和正在使用的手榴弹数据对象如果是同一个,就return。

如果不在背包里,使用手榴弹的时候要把这份数据移出背包,收回手榴弹时要放回背包。如果此时背包界面开着,要刷新界面。

不同类物品拥有不同属性时的设计:继承vs全包含?

背包系统想支持弹匣、医疗物品、手榴弹等几类物品。基于不同类物品分配表、用户数据分列表存储的设计架构,每类物品需要准备的数据类有

  1. 配表元素;
  2. 配表ScriptableObject;
  3. 预制体类;
  4. 用户背包、仓库数据类;

ui类有:

  1. 背包内物品脚本;
  2. 场景物品脚本;
  3. 他人背包物品脚本;
  4. 仓库界面的背包物品脚本;
  5. 仓库界面的仓库物品脚本。

需要增加的数据结构有:

  1. 配表;
  2. 背包里的物品列表;
  3. 仓库里的物品列表;
  4. 配表管理器里这类物品的配表字典;

需要增加的代码有:

  1. 配表管理器根据id得到详细信息的方法;
  2. 拾取物品时对相应类型的判定;
  3. 把这类物品放进背包的方法;
  4. 从背包取出这类物品的方法;
  5. 在显示附近物品方法里增加显示这类物品的代码;
  6. 在显示背包内物品方法里增加显示这类物品的代码;
  7. 在显示附近其他背包内物品方法里增加显示这类物品的代码;

想到原神的背包界面,按类型分成很多栏,肉眼可见工作量很大,而吃鸡虽然只有一个界面,但不同类物品的字段不同,点击行为不同,也不可能用同一个脚本处理。二者的程序工作量都很大,只是有没有用UI表现出来。

反思

为什么这套架构这么恶心,因为支持一类物品的背包系统需要的数据、代码量已经很大,又把不同类物品作为不同类,形成一个“子类矩阵”。

因为不同类物品详细信息的字段不同,配表无法共用。同理配表管理器也不行。配表部分是不能不同类物品共用的,只能期望背包和UI部分共用。

然后背包因为弹匣每个实例有独特的数据,必须单独用一个列表。那么其他每个实例都相同的物品在背包里就可以共用

public class ItemInPack{
    public int type;
    public int id;
    public int num;
}

的类存。

接下来修改放入背包的方法,输入一个ItemInPack,现在查询配表数据需要type和id两个信息,需要给配表和type定一个规则,写一个用type和id得到详细信息的方法。然后发现这个方法查到的物品可能是不同类型,那就返回它们的共同基类。

然后放入背包就要改成搜索type、id都相同的元素,num+1,没有则创建这个元素。

拿出背包:格子脚本有一个字段指向ItemInPack列表里的元素,直接把这个元素的num-1,减到0

显示背包物品,各类物品的共性是图标、名称,可能还有描述。但是详情页就根据不同类物品,有不同字段了,可采取的行动也不同。下面是逆境重生不同类物品的详情页:

几种详情页很明显是不同的预制体。现在我们只能期望格子分成两种:一个物品占一格的和一种物品占一格的。一种格子,不同类物品的格子共用同一个脚本。

不同类物品的格子能打开详情页,这些详情页不同,那么共用的格子脚本就太可能存详细信息了,除非它把所有类物品的详细信息类都存一份。那么它应该是根据type和id去相应的配表查得详细信息。

预制体上挂载的脚本也可以共用一个。因为预制体的作用就是让玩家捡起,玩家能识别这是哪种物品,还不涉及使用。

搞清了设计后尝试把之前写的医疗包和手榴弹也改成使用这个架构。需要改的部分有

  1. 背包数据结构(删掉医疗包、手榴弹列表);
  2. 人物的拾取方法(使用放入共用列表的方法);
  3. 预制体挂的脚本(使用共用脚本,记录type和id的);

如上面所说,不同类物品的详情页和可执行操作不一样,格子脚本里势必要有一个switch判定点击格子后的行为。然后我们不如把各类物品的详情页和操作按钮放在一起,做成一个页面,像上面的图一样。然后因为详情页有了按钮,不能再鼠标进入时显示,离开时消失了,否则按钮会点不到

背包格子类:精细分类还是全包含

用同一个类就需要有玩家背包物品、场景物品、他人背包物品需要的字段(玩家背包物品列表的索引、场景物品变量、玩家背包物品列表的索引),会有一些字段、代码多余,需要有一个type变量记录这个格子的来源类型。实例化格子时。写入相应字段。

用不同子类不会有多余代码,但是

  1. 格子按物品来源分(自己背包、场景、他人背包),还能按物品类分,脚本和类数量会巨多;
  2. 每种子类预制体都要有变量记录,都要拖一次预制体。

写了这么久背包,还是感觉到格子用一个大而全的类,用一个type记录类型比较好用。这里看似先进的继承反而造成很多细碎的脚本、同样多的格子预制体、同样多的记录预制体的字段。

这是只分两种格子:弹匣和其他物品时,显示详情页的代码:用is判断弹匣和其他物品,两层switch先判断格子来源,对于背包内其他物品,再根据是否有使用按钮加载相应的详情页。背包要处理的情况就是这么多,要么用这种分支判断巨长的设计,要么用子类、脚本、预制、预制体字段巨多的设计。相比之下后者更恶心一点。

public void ShowDetail(){
        HideDetail();
        if(itemUISelected is Cell_Mag){
            Cell_Mag cell=itemUISelected as Cell_Mag;
            switch(cell.type){
                case CellType.PlayerPack:
                detailPanel=Instantiate(detail_Mag_In,detailPageAnchor);
                Detail_Mag_In detail_In=detailPanel as Detail_Mag_In;
                detail_In.textNum.text=cell.ammoNum.ToString();
                detail_In.buttonDrop.onClick.AddListener(cell.Drop);
                break;
                case CellType.Scene:
                detailPanel=Instantiate(detail_Mag_Out,detailPageAnchor);
                Detail_Mag_Out detail_Out=detailPanel as Detail_Mag_Out;
                detail_Out.textNum.text=cell.ammoNum.ToString();
                detail_Out.buttonPick.onClick.AddListener(cell.PickFromScene);
                break;
                case CellType.OtherPack:
                detailPanel=Instantiate(detail_Mag_Out,detailPageAnchor);
                detail_Out=detailPanel as Detail_Mag_Out;
                detail_Out.textNum.text=cell.ammoNum.ToString();
                detail_Out.buttonPick.onClick.AddListener(cell.PickFromOtherPack);
                break;
            }
            detailPanel.textName.text=cell.magDataBin.itemName;
            detailPanel.textLoad.text=cell.magDataBin.load.ToString();
        }
        else if(itemUISelected is Cell_Item){
            Cell_Item cell= itemUISelected as Cell_Item;
            switch(cell.type){//格子来源
                case CellType.PlayerPack:
                ItemDataBin data=ItemDataManager.Instance.GetOtherItemFromSO
                (cell.otherItemInPack.type,cell.otherItemInPack.id);
                switch(cell.otherItemInPack.type){
                    case ItemDataManager.medicine:
                    detailPanel=Instantiate(detail_Item_Useful,detailPageAnchor);
                    Detail_Item_Useful detail_In=detailPanel as Detail_Item_Useful;
                    MedicineDataBin medicineData=data as MedicineDataBin;
                    detail_In.textName.text=medicineData.itemName;
                    detail_In.textDesc.text=string.Concat("回复",medicineData.healNum,"生命值");
                    detail_In.textLoad.text=medicineData.load.ToString();
                    detail_In.buttonDrop.onClick.AddListener(cell.Drop);
                    detail_In.buttonUse.onClick.AddListener(()=>{
                        cell.UseMedicine();
                        HideDetail();
                        });
                    break;
                    case ItemDataManager.grenade:
                    detailPanel=Instantiate(detail_Item_Useful,detailPageAnchor);
                    detail_In= detailPanel as Detail_Item_Useful;
                    GrenadeDataBin grenadeData=data as GrenadeDataBin;
                    detail_In.textName.text=grenadeData.itemName;
                    detail_In.textLoad.text=grenadeData.load.ToString();
                    detail_In.textDesc.text=grenadeData.desc;
                    detail_In.buttonDrop.onClick.AddListener(cell.Drop);
                    detail_In.buttonUse.onClick.AddListener(()=>{
                        if(MyInput.Instance.player.grenadeInHandData!=cell.otherItemInPack){
                            MyInput.Instance.player.UseGrenade();
                        }
                        HideDetail();
                        });
                    break;
                    case ItemDataManager.other:
                    detailPanel=Instantiate(detail_Item_Useless,detailPageAnchor);
                    Detail_Item_Useless detail2=detailPanel as Detail_Item_Useless;
                    detail2.textName.text=data.itemName;
                    detail2.textLoad.text=data.load.ToString();
                    detail2.textDesc.text=data.desc;
                    detail2.buttonDrop.onClick.AddListener(cell.Drop);
                    break;
                }
                break;
                //背包外物品格子,没有使不使用,只有拾取。也就不分能使用的药品、手榴弹和不能使用的其他
                case CellType.Scene:
                detailPanel=Instantiate(detail_Item_Out,detailPageAnchor);
                Detail_Item_Out detail=detailPanel as Detail_Item_Out;
                data=ItemDataManager.Instance.GetOtherItemFromSO
                (cell.itemInstance.type,cell.itemInstance.id);
                detail.textName.text=data.itemName;
                detail.textLoad.text=data.load.ToString();
                detail.textDesc.text=data.desc;
                detail.buttonPick.onClick.AddListener(cell.PickFromScene);
                break;
                case CellType.OtherPack:
                detailPanel=Instantiate(detail_Item_Out,detailPageAnchor);
                detail=detailPanel as Detail_Item_Out;
                data=ItemDataManager.Instance.GetOtherItemFromSO
                (cell.otherItemInPack.type,cell.otherItemInPack.id);
                detail.textName.text=data.itemName;
                detail.textLoad.text=data.load.ToString();
                detail.textDesc.text=data.desc;
                detail.buttonPick.onClick.AddListener(cell.PickFromOtherPack);
                break;
            }
        detailPanel.transform.position=itemUISelected.transform.position;
        }
    }

详情页预制体:可选操作不同,没法大而全

不同类物品可选操作按钮的数量、功能、名称不同,无法共用一个面板。

详情页预制体显示控件:精细分类还是大而全

详情页的文本显示控件可以根据物品类型拥有的字段定制,或是只包含名称和描述,甚至只用一个Text,各类物品不同的信息,药品的治疗量、弹匣容量,都通过在代码里给描述写“治疗量:”+xxx这样硬编码写。

前一种做法同样会导致详情页预制体泛滥,后一种则可以大大减少详情页预制体。

详情页脚本:精细分类还是大而全

详情页预制体已经必然每类物品一个,详情页脚本如果大而全,脚本和子类就少一点,坏处如果一些字段留空不算坏处,就没有坏处了。

改进:物品类包含所有类物品的属性,但是特定类的属性在成员对象里,可以是空的

可以让物品类有所有类物品的属性,但是把比如武器的属性放在一个类,药品属性一个类,物品拥有武器属性类字段,但如果不是武器,则武器属性字段是空。但是我们知道Unity里如果类序列化到检查器了,里面的空成员对象也会被实例化一个包含默认值的对象,不能用一类属性成员是否为空判断物品类型,还是要用种类枚举。也就是如果想在检查器方便查看背包里的物品,就不能指望通过把一些类成员设空来省内存,还是会退化成全包含设计。

经过反复折腾、查看一些项目源码,我觉得全包含设计还是比继承+多类设计好一些。

武器待拾取的预制体和人物手里的预制体

把武器分为待拾取的预制体和人物手里的预制体。待拾取的预制体有触发器、记录详情的脚本,可以被人物检测到、拾取。没有射击相关功能如发射子弹、播放枪声、播放枪口火焰、抛壳等。

人物手里的预制体有射击的脚本,声源、animator。

虽然之前一直用一种预制体也没什么问题,但考虑到枪的脚本里有Update()在一直检测射击输入,还有一大堆射击相关参数,枪在地上放着时都用不到,用两种预制体应该能明显改善性能。

然后需要考虑两种预制体衔接的问题。拾取枪时,销毁待拾取预制体,在人身上实例化人物手里的预制体。

背包类的设计

是做成独立的类还是作为人物的一部分?是否继承MonoBehaviour?

一开始我做成继承MonoBehavior,和人物分开的类,每个人物和背包都要相互引用,后来觉得每个人物都必有背包,就把背包代码作为人物的partial class。

再后来为了优化性能,想做人物死后成盒,要把人物销毁,背包数据转移到盒里,又要背包人物分开。

这时候想到,如果做成不继承MonoBehavior的类,人物和盒加一个背包字段,死时直接把这个背包实例赋值给盒,最方便。

总结起来,如果除了人物,还想做弹药箱,那么应该把背包和人物分离,继承MonoBehavior人物要多挂一个脚本,且把背包数据转移到另一个对象很麻烦,而且又看了一眼继承MonoBehavior的背包,发现它没有用任何生命周期函数、触发检测函数、协程,完全没有必要继承MonoBehavior。

弹药箱和背包的区别

  1. 背包不能装枪,弹药箱可以;
  2. 背包有主人,弹药箱没有;

总结

  • 玩家通过UI界面操作改变脚本里的数据时,为了保持脚本里和界面上的数据一致,UI界面的数据刷新时必须总是从脚本读取,而不能直接修改UI界面,比如从场景拿一个弹匣到背包里,如果用代码把附近物品栏的这个弹匣删掉,再在背包物品栏加上这个弹匣,实际上进行了脚本数据和UI界面的两线操作,造成了两边数据不一致的可能性。
  • 不要试图用一个脚本处理很多种情况,比如玩家背包里的弹匣、场景里的弹匣、别人背包里的弹匣共用一个脚本,再在里面做不同处理,会很难读。
  • 然后我发现让一个预制体的脚本里一个字段指向预制体它自己,预制体实例化后这个字段指向的也是场景里的实例它自己了。不能让一个预制体脚本里的字段指向它自己来记录预制体。

关于格子和相似面板脚本的继承问题带来的教训是:在背包系统里会有大量样子相似或一模一样,功能相似,但是又有细微差别的面板、格子,这时候很容易陷入一个误区就是面板、格子有一点不同就继承,结果多了大量脚本,这些面板、格子还需要预制体,文件又多了一倍。

反思继承的设计初衷,应该还是在类的成员、行为有较大不同时,才适合继承。

唐老狮的背包系统教程总结

总结几个要点

  1. 不同类物品使用同一个配表,用type记录类型,有所有物品可能的属性,允许有些属性对有些类物品无效;
  2. 数据管理器用以id为键的字典存配表,有一个把列表转换成字典的步骤。避免搜索列表的操作;
  3. 玩家拥有的物品按类型分成几个列表存;
  4. 不同类物品格子共用一个脚本,用枚举标记类型,

避免过度继承,尽量用同一个类支持不同类物品,使用type枚举标记,和我走过弯路后得到的结论一样。

正在使用的武器是放在拥有武器列表里,用index记录还是不放在拥有武器列表,专门用一个字段?

对于原神那样使用中的武器不可能损失的游戏,毫无疑问用index记录使用的武器,修改使用的武器只用改一个index,很方便;暗区突围那样带入武器可能损失的游戏,毫无疑问入局时使用的武器已经离开仓库了,使用的武器应该不属于仓库。这种设计修改使用的武器时

  1. 如果原来有武器,把原来使用的武器放进武器列表,把选择的武器拿出武器列表。比较简单的写法是用一个中间变量,把原来使用的武器和新武器交换。但是界面上这2把枪显示的顺序就会交换一下,体验会差一点。
  2. 如果原来没有武器,把选择的武器移出列表;

然后我们明白,原神和暗区突围的武器选择机制和它们的数据结构、游戏机制是适配的。

deepseek问答

游戏物品系统不同类物品的数据类是用type标记种类还是用继承?

在游戏物品系统的设计中,使用继承(Inheritance)是更优于使用Type标记(Tagging)的方案。这两种范式代表了面向对象编程(OOP)与数据导向设计(DOD)的核心思想碰撞。以下是深度分析和最佳实践:

---

一、核心对比:继承 vs Type标记

特性 继承方案 Type标记方案 胜出方
数据组织 类型安全,结构清晰 所有数据挤在同一类,需switch判断 ✅ 继承
内存效率 可能内存碎片化 数据连续存储,缓存友好 ✅ Type
扩展性 需修改继承树 添加新类型只需改枚举和数据表 ✅ Type
行为多态 天然支持虚函数 需手动switch或函数指针 ✅ 继承
序列化 复杂(需工厂模式) 简单(可直接反射/序列化) ✅ Type

---

二、架构示意图

1. 继承方案(OOP范式)

· 优点:类型安全,行为高内聚,符合直觉
· 缺点:物品类型固定后难以动态添加

2. Type标记方案(DOD范式)

· 优点:灵活性极高,适合数据驱动设计
· 缺点:需大量类型判断,容易破坏封装

---

三、最佳实践:混合方案(现代游戏主流)

结合两种优势,采用 “数据组件化 + 行为接口” 设计:

1. 基础物品结构

```csharp
// 基础物品数据(所有物品共有)
public struct Item {
    public uint32 GUID;
    public ItemType Type; // 枚举:Weapon, Consumable...
    public string Name;
    public IItemBehavior Behavior; // 行为接口
}

// 数据组件(按需附加)
public struct WeaponData {
    public int Damage;
    public int Durability;
}
public struct ConsumableData {
    public int HealthRestore;
}
```

2. 行为接口化

```csharp
// 行为接口
public interface IItemBehavior {
    void Use(Player user);
    void OnEquip(Player owner);
}

// 具体实现
public class WeaponBehavior : IItemBehavior {
    private WeaponData _data;
    
    public void Use(Player user) {
        user.Attack(_data.Damage);
        _data.Durability--;
    }
}
```

3. 混合方案架构

```mermaid
graph TD
    A[Item基础结构] --> B[Type字段]
    A --> C[IItemBehavior接口]
    B --> D[获取对应数据组件]
    C --> E[执行多态行为]
    
    D --> F[WeaponData]
    D --> G[ConsumableData]
    
    E --> H[WeaponBehavior]
    E --> I[ConsumableBehavior]
```

---

四、各方案性能对比

操作 继承方案 Type标记 混合方案
遍历所有物品 ❌ 缓存不友好 ✅ 最优 ✅ 优
调用Use() ✅ 虚表调用最快 ❌ switch开销 ✅ 接口调用快
内存占用 ❌ 碎片化 ✅ 紧凑 ✅ 较紧凑
添加新类型 ❌ 需重新编译 ✅ 热重载支持 ✅ 热重载支持

---

五、行业应用案例

1. 《魔兽世界》继承方案

```cpp
// 暴雪早期采用深继承树
class Item {};
class Weapon : public Item {};
class Sword : public Weapon {};
class LegendarySword : public Sword {}; // 问题:类爆炸
```

2. 《Minecraft》组件方案

```java
// 现代版采用组件化
public class ItemStack {
    private ItemType type;
    private ComponentMap components; // NBT数据
}
```

3. 《星际战甲》混合方案

```csharp
// 使用组合模式
public class Weapon {
    public WeaponData Data;
    public IWeaponBehavior[] Behaviors; // 多重行为
}
```

---

六、选择决策树

```mermaid
graph TD
    A[选择物品系统方案] --> B{项目规模};
    B -->|小型项目| C[继承方案<br/>简单直接];
    B -->|中大型项目| D{是否需要热更新?};
    D -->|是| E[Type标记方案<br/>数据驱动];
    D -->|否| F[混合方案<br/>性能与灵活性兼顾];
    E --> G[推荐: ScriptableObject];
    F --> H[推荐: ECS架构];
```

---

七、最终建议

1. 原型阶段:直接用继承快速验证(Consumable/Weapon基类)
2. 正式生产:转向混合方案(数据组件 + 行为接口)
3. 大型MMO:采用纯数据驱动(Type标记 + 脚本系统)

黄金法则:

· 如果物品行为差异大于数据差异 → 优先继承
· 如果物品数据差异大于行为差异 → 优先Type标记
· 永远不要用switch(item.Type)处理行为逻辑!

### 记录 Unity MMO 游戏背包系统开发流程笔记指南 #### 1. 需求分析 在开始编写任何代码之前,定义清晰的需求至关重要。这包括理解玩家可以携带多少物品、不同类型的物品如何存储以及交互方式。 对于MMO游戏中的背包系统来说,需求可能涉及支持多种道具类别(武器、防具、消耗品)、容量限制机制、界面显示逻辑等特性[^1]。 ```csharp // 定义物品类别的枚举类型 public enum ItemType { Weapon, Armor, Consumable } ``` #### 2. 设计架构 设计阶段应考虑模块化结构以便于维护和发展。通常会创建几个核心组件来处理不同的功能方面: - `InventoryManager`:管理整个库存的数据模型及其操作方法; - `ItemDatabase`:保存所有可用物品的信息列表; - `UIHandler`:负责绘制图形用户界面对话框并响应用户的输入事件; 这种分层的设计有助于简化复杂度,并允许团队成员专注于特定领域的工作而不干扰其他部分的功能实现[^2]. ```csharp using UnityEngine; /// <summary> /// 库存管理者脚本用于控制和更新角色当前持有的物品集合. /// </summary> public class InventoryManager : MonoBehaviour { private List<Item> items; // 存储实际拥有的项目实例 public void AddItem(Item newItem){ this.items.Add(newItem); } ... } ``` #### 3. 编码实践 当进入具体编码环节时,遵循良好的编程习惯非常重要。比如采用面向对象的思想构建可重用性强的对象体系;利用C#语言特性的优势提高性能表现;通过单元测试验证各个函数的行为正确性等等措施都可以帮助开发者更高效地完成任务目标[^3]. #### 4. 测试与优化 随着项目的推进,在每次迭代完成后都应当进行全面而细致的质量检测工作。针对可能出现的问题提前制定预案,并及时修复发现的缺陷以确保最终产品能够稳定运行。此外还需要关注用户体验方面的反馈意见从而不断改进现有设计方案直至满意为止[^4]. #### 5. 文档整理 最后一步就是将上述过程中积累下来的知识点汇总成文档形式供后续查阅参考之用了。一份详尽的技术手册不仅有利于新人快速上手新环境下的开发作业同时也便于长期维护期间查找历史变更记录等内容信息[^5].
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值