6.6 装备系统

6.6 装备系统

本节要点:

  • 装备需求分析
  • 装备系统的设计
  • 装备UI的设计
  • 装备系统实现

先来看看装备的策划表,里面包含了很多信息,装备的id、数字、等级、装备名字、职业类型、以及一些与技能有关的信息(力量、智力、敏捷、HP等)、以及描述一类的,按照现有的这些信息,先将装备表格做出来,在做之前,要知道表格的大概划分,知道装备表的装备数据有哪些,需要知道那些是关键数据,那些是非关键数据,关键数据指的是那些影响逻辑实现,非关键数据仅存于UI上和逻辑无关的。就比如描述、来源和装备名是非关键数据;ID、职业种类(要真是使用的话肯定不是用战士这样的字段,而是会用枚举值)、以及后面的各种属性(力量、智力、敏捷等),但是因为还没有战斗系统,所以暂时不需要关系具体的数值,只需要将字段提供过来就好。

image-20250809150936024

但有这个表就有这些装备吗?并不是,而是要通过特定来源来获得之后才能穿,也就是说这些装备并不是直接能够获取到的,而要通过购买、兑换或者从怪物身上掉落来进行获取,接下来看看装备系统的策划案。

如下图,左侧是列出的所有可装备的道具列表,中间有叉叉的方块是穿着的装备,右边是当前角色的一些基础属性,(当穿上装备的时候一些属性会进行变化),但这里先将属性忽略掉,因为角色属性还没有做。那么现在要做的就是装备系统的核心功能即能将装备穿在身上、脱下来,而且是能够从商店购买到装备,再穿到身上,并且在下线回来之后,装备还在身上,这就表明要维护角色的数据。

image-20250809170823637

接下来看看效果图,大概就是这么个样子,但是这里也有问题,左下角的装备按钮布置其意义,因为这里是可以通过双击左边的装备来做到装备的,所以这个按钮就失去了它的意义。

那么接下来思考一下这个效果图体现出来那几块内容,实际可以分为两大块内容。

一块是左边的装备列表,这个列表从哪里来,这个列表一定是已经拥有的装备,那已经拥有的装备在没显示出来的时候会在道具系统中,理论上装备也是道具,因为都是从商店里面买来的,如果是一个道具的话,但如果要重新写一个装备来管理,那就是说商店要卖装备,装备和道具是两种东西,那么背包也要在对接装备,那之前对道具所进行的一些列都要再来一遍,所以,在这里将它们划分到一类,装备是道具的一个分支,二者虽然有些东西不一样,但是很多东西都一样。那么就将装备当作道具来实现,这样可以简化逻辑,商店不需要改动,只要把装备配置到道具表里,再把道具配置到商店表里,就可以直接从商店买到装备了,然后背包里面就可以有装备了,

第二块是中间这一块,中间这一块是穿在身上的道具,那怎么能知道哪个部位穿那个呢?每一个地方所穿戴的装备并不相同,也就意味着需要有个地方来保存这些数据,那个装备装在哪里,那这些数据肯定是要存在服务端的,这样就可以保证今天穿上装备之后,明天装备还在,关闭客户端也可以存在,那服务端就是不二的选择,服务端就一定是存在数据库中,那么中间这块的所有布局信息就一定是存在数据库中的。

那么分析到这里,左边列表的数据来源都是之前做的系统,只要做UI就可以了,唯一的就是要将表格配上,左边的好解决,而中间就需要做数据的管理。

image-20250809171648567

装备系统:1. 装备管理,2. 装备穿戴(核心)。那么这里就涉及到了穿戴数据的保存,该用什么方式来进行保存最合适,当然可以建立一张表创建7个字段来进行保存,也可以像之前的背包系统一样也用字节进行保存,二的区别在于背包的格子多,而装备的格子少,并且装备的每一个格子只会有一个装备,不会出现叠加的情况。

来看看装备系统的数据结构,假如这就是一个数组(RNT),枚举值代表了一个编号,从0-6七个格子,第一个格子(WEAPON)中一定是填一个武器的ID,而ACCESSORY一定是填一个副手的ID,接下来分别是头盔、护手、衣服、裤子、靴子的ID,这样的话位置就固定下来了。

image-20250809173709285

装备系统会有一个UI(UICharEquip),里面会有小格子(UICharEquipItem)用来存放列表中的元素,装备也会有一个管理器(EquipManager),既然也将装备分为了道具,将装装备和拆装备也交给Service来管理。

image-20250809175039786

既然装备可以穿戴也可以拆卸,那么先要加一个与之相关的协议(ItemEquipResquest,ItemEquipResponse)为什么制作一对响应和请求?因为装备只有穿戴和拆卸的区别,加一个字段0,1分别用来表示拆卸和穿戴,然后加一个道具系统的配置表(EquipDefine)。客户端和服务端要各做一个Manager,UI。数据库(DB)并不需要做新表,只需要加一个字段就ok。

image-20250809192617971

6.6.1 配置表的制作

针对新的装备配置表,要先对道具表进行修改,新的道具表对比之前的道具表多了很多东西,先将装备填到道具表中,从10开始,后面全部都是装备的信息,而在这基础之上还增加了道具类型(Type)的枚举值,上节中并没有增加。

image-20250810205252719

因为枚举在结构要用的位置很多,不仅是逻辑中要用,还要再协议中进行使用,所以直接将类型的枚举值加入到协议当中,这样它自己就可以生成代码,现在道具中新增加装备数据(EQUIO),下面的EQUIO_SLOT是装备的槽位,在这里定义一组枚举值来代表它放在那个槽上。

image-20250811115112078

定义一个字节数组来存储当前装备。

image-20250811162313756

因为装备还需要穿在身上和脱下来,还需要再增加装备的协议,一个装备请求协议(ItemEquipRequest),一个装备响应协议(ItemEquipResponse)。

image-20250811162546461

记得将协议的内容写上,在穿装备的时候,需要知道那个槽位(slot),和那个id,其实某种程度来讲在穿戴装备的时候只需要知道ID就可以,因为表中会定义这个ID属于那个槽位,它只能装在一个位置,这里的槽位是为了在脱装备的时候,只需要知道槽位就好了,不需要知道具体装备是什么,只要将槽位清空就好。

image-20250811162921460

然后再回到配置表上,道具的配置表也进行了修改,新增了一个字段限制职业,以前的道具是不限制职业的,但是装备呢,战士的只能给战士,法师只能给法师,弓箭手只能给弓箭手,所以必须要指明这个道具属于那个职业,所以增加了这么一个字段,还新增了等级和新的类型装备(EQUIP),这样就可以很好地利用这个道具表。

image-20250811163412673

既然道具表里进行了修改,那么自然而然地商店道具表里也需要进行修改,首先将原有的所有道具都分到杂货商店,而装备店中放了一组装备,会像刚才道具表一样有一组ID(1001、1002、2001等),按照key2的顺序排下来,还有数量,一次只能买一个,价格。

image-20250811163722999

然后新建一个装备表,但这个表现在还用不到,只是先加出来而已,但是Slot这个字段是需要在逻辑中使用的,在这里就固定了那件装备只能装在哪里。后面的属性(生命、法力、力量等)则是在之后的环节中才会使用的。

image-20250811164659187

既然增加了新的表,当然也要给新的表增加装备的定义了,新建一个EquipDefine类(服务端),将字段一一对应加进来。

image-20250811171000201

然后在道具表中增加等级和职业限制。

image-20250811171143833

既然新增加了协议,就一定不要忘记去加上分发逻辑,不然又会没有任何变化了。

image-20250811171506662

不要忘记重新生成解决方案后将Common复制到客户端一份。

image-20250811171844643

然后处理一下上节课的尾巴,背包需要重开一下才能够刷新数据,以及将背包原来格子上的那些道具全部清理掉(Clear),然后重新初始化一下(OnReset)。

image-20250811183619139

然后对商店的分页也进行了小的调整,使得商店每10个道具为一页。

商店

然后是商店道具的修改,增加了职业限制的显示,而下面的两行代码则是为了让信息能够显示在UI上。

image-20250811184130847

image-20250811184314750

完成这些后回到服务端,为数据库增加新的字段-装备(Equips),并将其改为二进制类型(Binary)。而这次这个属性与以往不同的是需要一个固定长度,最大长度为28(4*7)字节,可以装7个道具。

image-20250811195239793

再修改完之后一定要记得生成数据库,不然90%都会报错,然后到数据库中去生成一下,没有报错就可以了。

image-20250811195617487

然后就可以关掉了,(这里没有截到弹出是否保存)记得一定要保存修改。

image-20250811195717004

6.6.1.1 装备UI制作

既然是装备,那就定然会在UI上显示出来,去做一个UI界面,直接将背包或者商店的复制一份过来进行修改,样式差不多就可以了。

image-20250811204235616

然后在经过一点细节加工后,就差不多可以了,大概就是这么一个感觉。

image-20250811204310638

然后是左边的装备和中间的装备UI,药水这个直接将之前的复制过来用了,将多余的屏蔽掉,而装备的UI只是简单的一张图就好。

image-20250812105736772

image-20250812105814098

因为增加了一个新的UI界面,记得要去UIManager加入新的UI逻辑,这样就可以用UIManager来进行调用了。

image-20250812110021655

6.6.2 客户端,服务端逻辑增加

在加入完新的UI逻辑后,还需要有调用,装备UI肯定是希望在主城进行调用的,然后用一个小图标来启用它。别忘了绑上对应的脚本命令,不然打开的可能不是装备界面。

image-20250812110236801

image-20250812110730276

**小Tips:**换装系统和换装备是两个概念,换装备主要是为了改变属性,为了让角色变得更强,而不是为了变得更好看;但换装是为了与别人与众不同。换装有些时候只是更换外表,只是时装和皮肤变了,但是属性并没有改变。如果想要更换外观怎么办?每件装备上面定义一个部件的ID,给武器定义名字,然后当检测到角色身上有武器的时候,就将该武器对应的模型加载进来设置到角色身上。

接下来就给客户端的DataManager增加读取项,先来个定义,然后是读取逻辑。

image-20250812102703629

image-20250812102726014

image-20250812102840154

然后先挑简单的来做,之前数据库中新增了字段,所以一定要记得去Character脚本中初始化,只要初始化,装备数据就会随着用户登录自然而然地下发到客户端。

image-20250812190914041

然后是DataManager的读取逻辑,先加定义后加读取。

image-20250812191720096

image-20250812191740320

完成这些前置数据准备后,就先从道具开始写,先找到道具服务,先注册请求(MessageDistributer),然后实现方法(OnItemEquip)

image-20250812210543938

这里可能会因为EquipManager还没写,所以会报错。

image-20250816182130136

再去加一个EquipManager类,然后思考一下穿装备要考虑什么,穿装备发过来的信息,有什么槽位slot,什么道具ID(itemId),装备是穿还是脱(isEquip)。而这也就是说服务端要做的就是收到客户端的请求之后,将装备数据设置到装备里面(UpdateEquip),然后Save下来就行了。

image-20250813163218417

下面是更新装备的方法,只要知道当前是那个格子,用当前的指针(pt),加上槽子的id,乘以每个槽子占的大小(sizeof),如果是穿装备,将装备的Id给它(if),如果是脱装备,就将slotid清零。

image-20250813165516727

**注:**指针和固定大小反冲区,只能在不安全代码中进行使用。

那怎么解决呢,在服务端只需要在这个项目中右击属性,然后再左边侧边栏中找到生成,然后勾选允许不安全代码即可。

image-20250813170200358

image-20250813170227504

字段要在创建角色的时候创建,新建一个用于装备。

image-20250813171159773

而在新建角色的时候一般是会有新手大礼包的,但是这里还没有做这个功能,所以先直接给背包中增加各20瓶的血瓶和蓝瓶。

image-20250813171515971

然后可以将下面的道具测试代码删除了,因为当时道具系统的代码还没有写,所以临时做的,这里可以不要了,注意不要多删了就好。

image-20250813171645969

服务端最重要的就是EquipManager,处理好协议的解析数据生成(UpdateEquip),将数组生成好,并更新到数据库里面。这里再将保存代码(DBService)进行一个扩展,可以传递是同步保存和异步保存。

image-20250813172357304

接下来就专注于客户端的逻辑,先来处理一下上节课的尾巴,当购买一个道具后,如果一个格子是空的,会将新的道具追加到里面,但是加完之后继续执行,直到将整个背包填满,在下面加一个break就好了。

image-20250813172658755

在之前的时候将装备归入了道具之中,共用一个道具信息,所以要在道具中新增一个装备的定义。

image-20250813172948215

当然在道具加载的时候会同时加载道具信息和装备信息,这样就可以通过item随时访问到道具信息和装备信息了。

image-20250813173203149

刚才服务端的协议处理已经做好了,那客户端只要在做一个发送(Subscribe)就好了,当然协议发送成功之后也要接受返回(Unsubcribe)。

image-20250813173437904

然后类比上面的购买发送写一个装备道具发送。这里写了两个成员变量,但是重要的是第一个pendingEquip,用来保存当前穿的是那件装备,将装备信息记录一下(pendingEquip= equip),当消息返回的时候(OnItemEquip),就知道当前穿的是那件装备了。

image-20250813174336432

image-20250813174230941

响应的时候加了一个判定,是为了告诉前端(manager),穿上了那个装备(if(this.isEquip),或者脱下了那个装备。

image-20250813174911773

然后去EquipManager补全剩下的逻辑,先开放两个方法,一个穿装备,一个脱装备。因为装备道具的时候要发送到服务端,所以调用item的Service来Send一个道具,要装什么装备道具(equip),发送到服务器。

image-20250813202005755

发送成功之后,接收到响应之后,就到了这里,客户端收到服务器保存成功后,因为之前记录了一个临时变量(pendingEquip),记录了当前穿的是什么装备,然后再调用EquipManager的OnEquipItem。

image-20250813202710793

在收到穿戴装备和脱装备的消息之后又回到(EquipManager)的OnEquipItem,先做个检查检查这个槽位是不是已经穿上了(if),然后将道具从道具内存中拿出来,放到格子上,

image-20250813203331354

服务端保存数据用的是字节,而客户端保存使用了一个定长数组Equips,数组长度是7(可以转到定义看一下,是协议中定义的)。这也是装备Manager唯一会用到的数据。并且用了一个Data来维护两者之间的转换,因为装备要从服务端发过来,发过来的时候与背包相同都是字节,需要做初始化(Init)。

image-20250813203537518

image-20250813203618649

image-20250813204058387

再拿到数据之后,对数据进行解析。

image-20250813204213493

同样的,再从本地将信息打包成服务端所需要的数据类型。

image-20250813204327977

这个方法暂时没什么用,是为了给其他系统开放一个接口用来方便查询当前是什么装备。

image-20250813204420932

同样的,有查询当前是什么装备,就一定会有有没有穿什么装备的接口。

image-20250816230645813

6.6.3 UI逻辑

先来看看UICharEquip,每次启动的时候先刷新一边UI,同时给装备这里注册了一个事件,只要穿装备或者脱装备了,就刷新一下。

image-20250814210202295

关闭的时候就清理掉。

image-20250813205637670

刷新UI做了几件事,第一先将左边的装备列表清空了,第二初始化一边,将右边已装备的列表清空,第四步重新初始化,然后将金钱刷新一下。

image-20250813213651933

先遍历左边所有的道具列表,将所有的装备显示出来(foreach),显示的时候判断一下,已经穿在身上的不显示。如果没有,将itemPrefab初始化一下,然后将索引,item,当前界面,非装备的列表(代表左边的列表还是右边的列表,这里用布尔值来进行区分)SetEquipItem过去。

image-20250814210031607

清除简单,找到左边的每一个子元素,然后Destroy掉就好了。

image-20250813213818022

同理,右边的是找到每一个槽下面的,所有的子节点删掉,因为布局不一样,所以清除的方法也有所不同。

image-20250813214048366

创建右边的也简单,先将所有格子检查一遍,看看格子上有没有装备,如果有装备了,生成一个(GameObject),然后将装备信息设置进去(UIEquipItem)。

image-20250813214208540

还有两个事件,一个穿装备,一个脱装备。这两个事件是给点击事件用的,所以接下来来写UIEquIpItem。

image-20250813214337876

先定义一些组件,之后去Unity绑定,写一个是否选中的方法。

image-20250813222226245

再来一个索引,一个owner,一个维护装备的item,还有一个代表当前是装备列表还是非装备列表的布尔值(isEquiped)

image-20250813222643046

通过多出的布尔值,将其他信息,初始化到对应UI上,比如说owner,item,index。

image-20250814173610078

而再一开始就用了一个新的方法,指针点击处理器,这个处理的是点击,不再是选择,只要鼠标按一下就会执行了。

image-20250813223041591

因为这里需要点两次,如果是已经装备的道具,就执行脱装备的逻辑,会弹出一个是否要脱装备的提示框(MessageBox),如果点确认(OnYes),就将装备脱下来

image-20250813223248773

image-20250813223601585

穿装备也很好理解,这是一件非装备列表(左边的装备列表),就要先判断有无选中(this.selected),如果不是选中的,就降Selected设置为选中,当第二次点击的时候就能够走到选中(if),选中之后就穿装备(DoEquip),然后降转中状态改为false,为了再次点击的时候能再次执行逻辑。

image-20250813224643227

而穿装备会比脱装备多,第一件事是先询问是否要装备(msg),如果点了yes,如果原来有武器了(if(oldEquip)),要替换原来的武器吗?newmsg,如果确认,进行更换,如果说oldEquip是空的,不需要确认,直接就穿好装备。

image-20250813224849622

在注册通知这个地方(RegisterStatusNotify)是有BUG的,这个方法以前是在ItemManager的时候进行调用的,在初始化的时候进行注册,但ItemManager是单例,它初始化的时候是进入游戏(OnGameEnter)的时候进行初始化,也就是说要是游戏不退出,选择一个角色进去,再退出来再进一次,再退出再进一次,会导致这个函数(RegisterStatusNotify)进入3次,那就是说同样一个单例在这里面会有三份事件(action),那么当服务器通知要加装备的时候,那么它就会告诉道具管理器三次加一下装备,总共加了三次装备,所以装备会出现多次。

如何杜绝这样的清空,加一个哈希表(HashSet),它的查询性能高,可以很快地检索(action)有没有在集合(handles)里面。为什么不用Dictionary,而用HashSet是因为Dictionary必须要一个key和一个值,而HashSet只需要一个key就够了,只需要判断是否重复。

image-20250813225555912

image-20250813225741130

image-20250813225849329

6.6.4 装配UI脚本

首先先给UI挂上它们对应的脚本,因为装备列表和非装备列表都属于道具,所以它们挂的脚本是相同的,只是相应的信息少而已。将图片给icon,将UIEquipIcon给背景(background)。

image-20250814174044986

同理装备列表也要给相应的信息挂上脚本,将相应的组件给到相应的位置。

image-20250816231713223

最后是角色装备的UI界面,注意这里多了一个非装备列表的根节点(Item List Root),还有事角色地装备位置要一一对应。

image-20250814174613984

6.6.5 测试

在写完这些个代码之后,就可以进行测试了,先进去买点装备,如图,杂货商店的已经有装备了,并且可以进行正常得购买,能够弹出购买成功得弹窗。

image-20250816231522542

image-20250816231106500

来到背包也可以看到新买的装备,购买到背包显示也没有问题。

image-20250816231135217

来到角色装备界面,装备提示也没有问题。

image-20250816231328642

然后卸下装备,没有问题,代码测试没有问题。

image-20250816233820086

image-20250816233826861

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值