物品系统是 VV 引擎最庞大的模块,但它的分层设计值得细品
一、基础操作层:给、拿、用,三件事物品系统最底层的操作就三件事:给玩家物品、从玩家身上拿物品、让玩家使用物品。这三个操作是所有物品相关逻辑的基础。
给物品用 giveitem,传玩家、物品名、数量,还能带绑定规则和描述。这个接口返回最后一个物品对象,但文档特意提醒了 —— 不建议在叠加物品和一次性给多个物品的场景下依赖这个返回值,因为物品添加到背包后可能被回收。这个提醒很重要,说明给物品这个操作不是原子的,中间可能触发背包整理、物品叠加、甚至删除,你拿到的物品对象不一定还在。
如果给物品的时候就想直接装备上,有 giveonitem,多传一个装备位置参数,物品生成后直接穿到对应位置。做新手礼包、初始装备的时候用这个很方便,不用先给再穿两步操作。
拿物品有两个版本: takeitem 和 takeitemex。基础版按名称扣,拓展版多了一个绑定类型参数,可以指定只扣绑定的还是只扣非绑定的。两个接口的文档都用加粗字体强调了同一句话:务必判断返回值,引擎锁定的物品并不会被扣除。
这句话是整个物品系统最重要的提醒之一。玩家身上的物品不是你想扣就能扣的 —— 正在穿戴的装备可能被锁定、交易中的物品可能被锁定、某些系统保护的物品也可能被锁定。如果你不判断返回值就默认扣除成功,继续往下走逻辑,就会出现 "东西没扣但奖励发了" 的严重 bug。
所以正确的写法永远是:先扣,判断返回值,成功了才发奖励,失败了提示玩家。这个顺序不能反。
使用物品用 eatitem,吃药、用卷轴、开宝箱这些都走这个。参数很简单,玩家、物品名、数量。这个接口的应用场景主要是脚本里模拟玩家使用物品,比如 NPC 帮你吃药、活动自动使用道具。
除了按名称操作,还有一套按唯一 ID 操作的接口。 getitembymakeindex 拿物品对象, delitembymakeindex 按 ID 删物品。唯一 ID 是物品的身份证,同一个名称的物品可能有很多个,但每个物品的唯一 ID 是不同的。当你需要精确操作某一个特定物品的时候(比如删除玩家选中的那件装备),就必须用唯一 ID,不能用名称 —— 用名称会把同名的都扣掉。
二、信息读取层:三种信息,三个来源
要操作物品,先得知道物品的信息。VV 引擎的物品信息分三个层次,对应三个来源。
第一个层次是实例信息,就是这个具体物品的当前状态。用 getiteminfo 读,传物品对象和一个 ID,能拿到唯一 ID、物品 ID、剩余持久、最大持久、叠加数量、绑定状态、物品名称、改名后的名称。这些是每个物品实例独有的 —— 同样叫 "木剑",两把的持久度可能不同,唯一 ID 肯定不同。
第二个层次是基础信息,就是物品数据库里的配置。用 getstditeminfo 读,传物品 ID 或名称,能拿到 idx、名称、StdMode、Shape、重量、AniCount、最大持久、叠加数量、价格、使用条件、使用等级、自定义常量、颜色这些。这些是所有同名物品共享的,是物品的 "模板" 信息。
还有 getstditematt 读基础属性,传物品 ID 和属性 ID,拿防御、攻击这些基础数值。这个和基础信息是配套的,一个读通用字段,一个读属性字段。
第三个层次是数据库原始字段,用 getdbitemfieldvalue 读,直接传字段名拿值。这个接口最底层,能读到前两个接口没暴露的字段。文档里列了一大串字段名和对应的列号,还标注了哪些字段引擎不读取、哪些已经取消了。
三个层次的设计思路是:常用的信息有专门的接口,不常用的可以走底层字段接口。大部分场景下 getiteminfo 和 getstditeminfo 就够了,只有做特殊功能需要读冷门字段的时候,才需要用 getdbitemfieldvalue。
这里有个心得:能用上层接口就不要用底层接口。上层接口是引擎封装好的,稳定、语义清晰。底层字段接口直接读数据库列,如果以后引擎调整了字段顺序或者含义,你的代码可能就出问题了。上层接口会帮你屏蔽这些变化。
获取物品对象的方式也值得一说。穿戴中的装备用 linkbodyitem,传装备位置就能关联到那个物品对象。背包里的物品可以用 getiteminfobyindex 按索引遍历,也可以用 getbagitems 拿整个背包的物品列表,还能按名称和绑定状态筛选。仓库物品用 getstorageitems 按页拿。
不同位置的物品有不同的获取方式,这个设计是合理的 —— 穿戴装备、背包、仓库是三个独立的数据区域,操作方式自然不同。但写代码的时候要注意,你拿到的物品对象是哪个区域的,对它的操作是否适用于那个区域。比如 linkbodyitem 拿到的是穿戴装备,你对它调用删除接口,本质上就是脱下并销毁,和删背包物品的效果不一样。
三、属性体系层:六层属性,从基础到自定义
物品的属性体系是整个物品系统最复杂、也最能体现设计功力的部分。我把它分成六层来看。
第一层是基础属性。 防御、魔御、攻击、魔法、道术、生命、魔法这些传统属性,存在物品数据库里,所有同名物品共享。用 getstditematt 读取,这层属性是物品的 "天生属性",一般不会变。
第二层是指定属性。 对应 GetItemValue 和 SetItemValue 的 Type=1,位置 0 到 22,包括 AC、MAC、DC、MC、SC、幸运、准确、敏捷、攻击速度、魔法躲避、毒物躲避、体力恢复、魔法恢复、中毒恢复、沙巴克升级标记、强化星数、自定义名称标记、神圣、强度、诅咒、神秘装备穿戴标记、投保次数、限时激活方式。这层是每个物品实例独有的,可以动态修改。
第三层是极品属性。 Type=2,位置 0 到 13。极品属性是装备掉落时随机生成的额外属性,和基础属性分开存储。这个设计让 "天生属性" 和 "随机属性" 互不干扰,洗极品、重铸这些功能操作的就是这层数据。
第四层是元素属性。 Type=3,位置是元素属性 ID。暴击几率、攻击伤害增加、物理伤害减少、魔法伤害减少、忽视防御、伤害反弹、目标爆率、体力增加、魔力增加、暴击伤害增加…… 这些是后来扩展的百分比属性,和传统的点数属性区分开。元素属性有单独的 setnewitemvalue 和 getnewitemaddvalue 接口,也可以通过 GetItemValue 的 Type=3 访问。
第五层是其他属性。 Type=4,位置 0 到 3。包括剩余时间(秒,到期自动销毁)、物品绑定规则、物品名字颜色、装备升级次数或星星数量。这层是一些杂项但重要的状态数据,比如限时物品的倒计时就存在这里。
第六层是扩展属性和扩展极品属性。 Type=5 和 Type=6,位置是属性 ID。这两层是给未来扩展留的,属性 ID 从 1 到 141,覆盖了 HP 上限、MP 上限、各种攻防上限、速度、暴击相关、经验加成、金币加成等等。Type=7 更是直接用数组序号 0 到 19 存储扩展极品属性,每个位置存一个属性 ID 和属性值。
六层属性的设计,本质上是历史迭代的产物。基础属性是传奇最原始的设计,指定属性和极品属性是早期扩展,元素属性是百分比属性流行后加的,扩展属性是为了支持更多自定义词条预留的。每一层都有自己的存储位置和访问方式,不是统一的一套体系。
这个设计的好处是向后兼容 —— 老物品的数据结构不用变,新功能加新的属性层就行。坏处是学习成本高,同样是 "加攻击",你得想清楚是加基础攻击、极品攻击、元素攻击还是扩展攻击,它们走的接口和存储位置都不一样。
实际开发中的心得是:做新系统的属性,优先用扩展属性层(Type=5/6/7),不要去动基础属性和指定属性。扩展属性是引擎专门留出来给自定义用的,怎么折腾都不会影响老系统。基础属性和指定属性牵扯到引擎底层的计算逻辑,乱改可能出意想不到的问题。
四、自定义属性:引擎给你留的一张白纸
如果说六层属性还是在引擎的框架里玩,那自定义属性就是引擎直接给了你一张白纸,你想画什么画什么。
自定义属性的接口很多,但核心逻辑是:每个装备位置支持 40 个属性槽(propindex 0~39),每个槽有 7 个值位置(valueindex 0~6),还有一套元数据控制显示和绑定。
值的读写用 GetCustomItemValue 和 SetCustomItemValue,支持加减等于三种操作符。类型的读写用 GetCustomItemValueType 和 SetCustomItemValueType。元数据用 getcustomitemabil 和 setcustomitemabil,能控制颜色、绑定到哪个基础属性、显示位置、是否百分比、属于哪个模块。
还有自定义文字内容和颜色, getcustomitemtext / setcustomitemtext 和对应的颜色接口,可以在装备上写一段自定义描述文字。
这套系统的设计思路是:引擎不关心你存的是什么属性,它只提供存储和显示的框架。你可以把 40 个槽位的任意一个定义成任何属性,值的含义、显示方式、和什么属性绑定,全部由你自己决定。
最妙的是 getallcustomitemvalue 这个聚合接口。它能计算人物所有装备上某个绑定属性的总和,还分三个返回值:非百分比值之和、单件百分比值之和、全身百分比值之和。这意味着你做自定义属性的时候,不用自己遍历所有装备去累加,引擎帮你算好了。
还有 getallcustomitemvaluebytextline,按显示行来聚合,一行最多返回 7 个值。这个接口是为自定义属性面板设计的 —— 你把属性按行排列,引擎直接给你算好每行的合计,不用自己做分组。
自定义属性的设计让我印象深刻,因为它体现了一种 "授人以渔" 的思路。引擎不可能预见到所有版本需要的属性类型,与其不断加新的属性层,不如直接提供一套通用的自定义存储和计算框架,让开发者自己定义。
但自定义属性也有它的门槛。40 个槽位、7 个值位置、5 种元数据,这套体系本身就需要你先做规划 —— 哪个槽放什么属性、值位置怎么分配、显示怎么排。如果不规划好就乱用,到后期槽位冲突、显示混乱,维护起来很痛苦。
我的建议是:用自定义属性之前,先写一张对照表,把 40 个 propindex 和 7 个 valueindex 的用途全部定义清楚,哪些是攻击类、哪些是防御类、哪些是特殊效果,写清楚再动手。不然后面加新属性的时候发现槽位用完了或者和老的冲突了,改起来很麻烦。
五、状态管理层:绑定、持久、来源、限时
物品除了属性,还有一系列状态需要管理。这部分接口虽然散,但每个都解决一个实际问题。
绑定状态 用位掩码设计。禁止扔、禁止交易、禁止存、禁止修、禁止出售、禁止爆出、丢弃消失、死亡必爆、禁止拍卖、禁止挑战、爆出消失,十一种规则,每种对应一个二进制位。要同时设置多个规则,就把对应的值加起来 —— 比如禁止扔、禁止交易、禁止出售就是 1+2+16=19,传 19 进去就行。
位掩码是非常经典的设计,用一个整数存多个布尔状态,节省空间,操作高效。但对不熟悉的人来说不太直观,你看到一个数字 19,得算一下才知道是哪几个规则的组合。文档里特意给了例子说明怎么算,这个很贴心。
绑定状态的设置用 setitemstate,检测用 checkitemstate,还有 getiteminfo 的 ID=6 能拿到整体绑定状态。注意 setitemstate 是对物品对象操作的,不管物品在背包还是穿戴中都能改。
持久度 用 getdura 和 setdura,按唯一 ID 操作,支持加减等于。持久度是传奇的传统设定,武器衣服用久了会坏,需要修。 repairall 可以一键修所有装备,但文档说需要在 NPC 脚本里用,还要在文件头设置 itemstype 表指定修哪些位置。这个限制有点奇怪,可能是和修复 NPC 的传统逻辑挂钩了。
物品来源 是个很有意思的功能。 setthrowitemly 传一个 JSON,能记录物品是哪来的 ——GM 生成、NPC 给的、商城买的、怪物掉的、挖矿挖的、宝箱开的,还能记录具体的怪物名、玩家名、时间。 getthrowitemly 能把这些信息读出来。
这个功能的应用场景很明确:物品追踪。玩家举报某件装备是复制的或者 bug 刷的,你一查来源就知道它到底是哪来的。做经济系统的风控、追查异常物品、回档的时候定位哪些物品需要删除,都靠这个。
文档里说设置来源后,脚本中给与物品时会自动绑定。这意味着你可以在全局配置里设好默认来源,之后所有给物品的操作自动带上来源信息,不用每次手动设。这个设计很周到。
限时物品 的倒计时存在 GetItemValue 的 Type=4 位置 0 里,单位是秒,到期自动销毁。 addfunitemdura 可以增加限次使用物品的次数。这两个功能配合起来,可以做 "使用 N 次后消失" 或者 "N 小时后过期" 的道具,做活动奖励、体验卡、限时道具的时候用。
六、视觉表现层:名字、颜色、特效、内观、进度条
物品的视觉表现是玩家能直接感受到的部分,VV 引擎在这一层给的自由度很高。
改名 用 changeitemname,支持按装备位置改,也支持按物品对象改。改名后 getiteminfo 的 ID=7 还是原始名称,ID=8 才是改名后的名称。这个区分很重要 —— 你查物品原始名和当前名要走不同的 ID。改名功能在做自定义装备、冠名武器、特殊称号物品的时候很有用。
名字颜色 用 changeitemnamecolor 和 getitemnamecolor,颜色 0 到 255,传 0 恢复默认。颜色值和人物名字颜色、称号颜色走的是同一套体系。
物品特效 用 setitemeffect,可以分别设置背包特效和内观特效,还能控制层级(前面还是后面)。支持按装备位置设置,也支持按物品对象设置。做发光武器、带特效的时装、高级装备的视觉区分的时候用。
内观图片 用 setitemlooks 修改装备的内观 Looks 值。内观就是角色面板里显示的装备图片,改这个可以让同一件装备在面板里显示成不同的外观,做装备幻化、外观升级的时候用。
自定义进度条 是刀魂系统的核心。 setcustomitemprogressbar 给装备加一个进度条,支持三个进度条索引(0 到 2),每个进度条可以配置开关、显示方式(百分比 / 数字)、名称、颜色、当前值、最大值、等级。用 JSON 传参,设置完调用 refreshitem 刷新到前端。
这个功能的想象空间很大。经验值条、锻造进度、充能等级、宝石镶嵌进度…… 任何需要在装备上显示 "进度" 的功能都可以用这个做。三个进度条意味着一件装备最多同时显示三种进度,基本够用了。
视觉表现层的接口有个共同特点:改完之后大多需要手动刷新。 refreshitem 刷新单个物品, UpdateItem 也是刷新前端物品属性, recalcabilitys 刷新人物整体属性。改了物品的名字、颜色、特效、属性,如果不刷新,客户端看到的还是旧的。
这个 "改完要刷新" 的机制,本质上是服务端和客户端的数据同步问题。服务端改了数据,但客户端不知道,必须主动推一条更新消息。引擎没有做成自动刷新,可能是考虑到性能 —— 如果你连续改一个物品的十个属性,自动刷新就会推十次,手动刷新可以只在最后推一次。但代价是开发者容易忘,改完发现没效果,查半天才想起没刷新。
我的习惯是:物品属性改完之后,紧跟着写 refreshitem,然后如果影响人物属性就再写 recalcabilitys。把这两步当成修改物品属性的固定收尾动作,就不会漏。
七、批量操作层:一次处理多件物品
单个物品的操作学会之后,批量操作就是效率问题了。VV 引擎给了一套批量接口,专门处理 "一次给一堆"" 一次扣一堆 ""一次查一堆" 的场景。
批量给 用 gives,传一个字符串,格式是 "物品名 #数量# 绑定状态 & 物品名 #数量# 绑定状态",用 & 分隔不同物品,用 #分隔同一物品的参数。一次调用就能给玩家十几种物品,不用写十几行 giveitem。
批量扣 用 takes,格式和 gives 类似,还多了几个参数:可以把扣除过程中是否遇到绑定物品的结果存到变量里,可以指定优先扣绑定还是非绑定。这个设计很实用 —— 比如做装备合成需要消耗材料,你想优先消耗绑定的材料(因为绑定的交易不了,留着也没用),就设成优先扣绑定。
批量检测 用 checkitems,格式一样,检测玩家背包里有没有足够的物品。通常和 takes 配合使用 —— 先 checkitems 确认有,再 takes 扣除。不过 takes 本身有返回值,其实可以直接 takes 然后判断返回值,不一定需要先检测。但如果要在扣除前给玩家显示 "你缺少以下物品" 的详细提示,就需要先检测。
批量检测佩戴 用 checkitemw,检测玩家身上有没有穿戴指定装备。做套装效果、装备门槛的时候用。
扣除穿戴装备 用 takew,直接从玩家身上脱下并扣除装备。文档里特意提醒了:传入数量大于 1 时返回值不准确,使用前要先判断是否满足条件。这个坑要注意,扣多件装备的时候不要依赖返回值,先自己检测数量够不够。
批量操作的字符串格式设计是个双刃剑。好处是简洁,一行字符串就能描述复杂的物品列表。坏处是不直观,拼字符串的时候容易写错分隔符或者顺序,而且如果物品名里包含 #或 & 就会出问题(虽然正常物品名不会有这些字符)。
我的心得是:批量操作的字符串,最好用变量拼接或者 table.concat 来生成,不要手写。手写长字符串容易漏参数、写错分隔符。用 table 把每个物品的参数存好,最后用 concat 拼起来,清晰又不容易错。
八、几个容易踩的坑
把物品系统过完,结合实际可能的使用场景,总结几个最容易踩的坑。
第一,扣除物品一定要判断返回值。 这个前面说了,但值得再强调一次。锁定的物品扣不掉、背包里没有那么多扣不掉、绑定状态不匹配扣不掉。任何扣除操作之后都要判断返回值,成功了才继续,失败了就提示玩家。这是物品相关逻辑的铁律。
第二,改完物品要刷新。 名字、颜色、属性、特效、进度条,改完不刷新客户端看不到。 refreshitem 刷物品, recalcabilitys 刷人物属性,养成改完就刷的习惯。
第三,物品对象可能失效。 你拿到一个物品对象,过了几行代码再用,中间如果发生了背包整理、物品叠加、删除等操作,这个对象可能已经失效了。尤其是 giveitem 的返回值,文档明确说了可能被回收。所以拿到物品对象后尽快操作,不要存着过很久再用。
第四,按名称操作会影响所有同名物品。 takeitem 按名称扣,会把背包里所有同名的都算进去。如果你只想扣某一件特定的,必须用唯一 ID。这个在做装备升级、物品合成的时候尤其重要 —— 玩家背包里可能有好几把同名的武器,你按名称扣就全扣了。
第五,属性分层不要搞混。 同样是 "加攻击",基础属性、极品属性、元素属性、扩展属性是四个不同的东西,计算方式、显示方式、存储位置都不一样。做功能之前先想清楚你的属性属于哪一层,用对应的接口。不要图省事随便选一层,到时候和其他系统冲突了再改就麻烦了。
第六,自定义属性要提前规划。 40 个槽位看起来多,但如果版本内容多,不知不觉就用完了。开做之前先把槽位分配表写好,哪些是攻击类、哪些是防御类、哪些是特殊效果、哪些预留,规划清楚再动手。
第七,批量操作的字符串注意格式。 #分隔参数,& 分隔物品,顺序不要错,数量和绑定状态不要搞反。建议用代码生成字符串,不要手写。
挠头的500贡献 挠头的500贡献 挠头的500贡献 #在这里快速回复# 挠头的500贡献
页:
[1]