Mod里制作剑(三)
浅海:所以到现在我还是不知道怎么做一把剑。需要很多代码吗?
深湖:不需要。实际上就跟做盔甲差不多,理想情况下你做一个ItemSwordBase,起一个工具质地……也可以不起,然后在ModItems里列举就可以了,和我讲做盔甲的那篇文章差不多。
浅海:那我们已经进入第三篇了,还没有开始做剑,岂不是三纸无驴?
深湖:不。这个实际上是有必要的。我看到群里一些人的提问之后才决定说这些。另一个原因就是,在物品描述里,盔甲本身显示的就是加成,和代码里的是一致的;剑与工具显示的不一致,所以理解起来要绕一个弯子。还有就是,盔甲那课有几个坑我没有讲。那个看起来详细,但仍然不完备。
浅海:比如?
深湖:一会儿你会在剑的部分看到盔甲教程里藏着的漏洞。
浅海:好吧……不过确实,就像费曼所说,要想让人在不理解原理的情况下,遵守一大堆莫名其妙的规矩,根本是天方夜谭。
深湖:是的,不过那个是产生核爆炸,咱们的充其量是炸个mod或者玩家的客户端……嗯当然也不是什么好事。扯远了。我们继续说属性修饰符的事。
属性修饰符(Attribute Modifier,参阅https://minecraft.fandom.com/zh/wiki/%E5%B1%9E%E6%80%A7),以前我也经常说成“属性修改器”,但这并不是什么外挂之类的。
每一条属性,都有一个基础值,这个基础值正常来说,只会在实体初始化时设置一次,之后便不再更改。虽然从技术上来说,想修改它很容易,但你一般都不应这样做。
浅海:这是为什么呢?
深湖:设想你做了一个功能叫做在水下时加1点攻击;另一个人做了一件RPG风格的盔甲,效果是穿上之后+30%攻击;玩家此时手里拿着一把铁剑,那他最后应该多少攻击力?更重要的,假如你先因为在水里而+1攻击,玩家又穿上那件+30%的攻击的盔甲,你的攻击加成就变成+1.3的话,你怎么知道你离开水的时候要扣除的是1点还是1.3点攻击呢?
浅海:还真是够麻烦的。如果每个人都去试图修改基础值,就会乱套了。不过刚才说的百分比那事,我不能让我的+1攻击不受那30%影响吗?
深湖:很遗憾,受不受是由百分比那哥们决定的,不是由+1这头决定的,这个我们后面再谈。但总之,我们需要分别记录下每项修改,使得它们分别生效又撤掉后属性不会乱掉,而这是只靠一个单纯的float变量所无法解决的。“在雨天攻击+1”,“在草中攻击x2”,你总不能在玩家进草丛后下雨,然后雨停了又出草丛,结果攻击永久+1消不掉了吧。
浅海:那我们规定永远先乘除后加减……之类的呢?
深湖:如果只有一个变量来记录属性,这东西的顺序不是你说了算的。你不知道玩家时先拿起剑,还是先进草丛攻击x2。属性改变的事情随着游戏流程自然进行,顺序基本无法控制。所以,我们只得把对属性的每一条修改都用一个类存储起来。这个类就是我们的属性修改器。
属性修改器主要有三个要素:
UUID。正如前文所述,同一个UUID的修改只能存在一项。此外,特殊UUID在物品上的描述会发生变化。
具体值。是+3还是-2?
类别。是加,还是加百分比?这里有0、1、2三种模式。
0是增加固定值;
1是百分比,但是所有1类的百分比会加在一起,就是两个+50%的叠加结果是+100%;
2也是百分比,但是所有2类的百分比会乘在一起,就是两个+50%的结果叠加是150%x150%=225%。
还有两个不重要的,分别是注释栏和是否存盘。我想你有很多问题,我们一个个来。
浅海:不不不,修饰符所修改的具体属性呢?只有+3,那它到底是加攻击还是加血量呢?
深湖:哈哈,修饰符的对象里不存储它到底修饰什么属性。属性知道他下面有什么修饰符,但是反过来是不知道的。你甚至可以给一个修饰符对象同时挂在两个属性下面,但是很蠢,尽量不要这样做。
浅海:为何要UUID呢?比如说A剑伤害是+5,那我卸掉A剑时没有必要找UUID匹配,只要找个+5的卸掉不就得了?
深湖:乍一看确实如此,不过这种用值当索引的管理很混乱……更关键的,你假定一把剑的伤害值是不变的。比如有一把剑,他的攻击力加成等于玩家的等级数。玩家从5级升7级,此时你应该如何?
浅海:把+5的删了,换一个+7进去,不行吗?
深湖:那你就必须在处理+7的时候记住前一刻是+5的。而这一点,很多时候其实不容易办到。你知道现在是玩家时7级,但你未必知道变动之前是几级。他可能是6级升上来的,或者8级掉下去的,或者5级连升两级来的。
浅海:确实,这样管理起来就必须要经常缓存前一刻的状态了,很麻烦。等等,攻击力加成等于玩家等级的剑?属性修饰符的检查会在玩家升级时更新吗?

深湖:不会。你很聪明,发现了这个设计的漏洞。物品属性修饰符,只有在装备栏里的物品ItemStack变动——也就是数量、耐久、类别、或者nbt有一个不相等时检查。值得注意的是,这里玩家就缓存了前一刻的状态。每次我们更改玩家装备栏的物品的时候,会更新【装备栏】,而这实际上和玩家的【库存栏】(含盔甲、主副手、玩家背包)并不是一个东西。生物会在每个tick(EntityLivingBase::onUpdate)的时候检查,如果装备栏和库存栏对应格子的东西不匹配,就解除掉库存栏旧东西的所有属性修饰符,然后把新的修饰符挂上去,再把库存栏的内容更新到和装备栏一致。另外就是除了tick,它在序列化存档的时候也会做一遍这件事,不过这不重要。
玩家升级并不会导致物品的任何一项内容发生变化,所以这修改器不会实时刷新。更可怕的是,它在物品描述的时候是刷新了的,你一看,他的描述会在升级后变化,但是这只是用于显示的东西变化了,实际上影响数值的修改器没变。这就写出了一个很隐蔽的bug。
浅海:解决方式呢?
深湖:每升一级,就强制给手里的这种物品变一下nbt,哪怕只是个无意义的内容在“1”、“2”之间来回变(你必须做出不同,1变1是不会有用的),就是为了触发更新,这样就足够了。或者,干脆别做这种不依赖于耐久、nbt、数量、物品种类的设计。当然,这里有另一个问题,就是物品的nbt变了之后会导致手里的物品抖一下,有个类似换物品的刷新动画。你要是想屏蔽那个动画的话要覆写Item::shouldCauseReequipAnimation返回false。这玩意原版从来没动过,不过你动了也没什么问题,如果你的设计需要的话就应该动。
浅海:好险,差点就写了个bug出来。
深湖:所以我说,
大师们写出来的代码并不一定更加复杂,他们只是知道哪些写法与设计会出问题,而这从最终的成品里很难看出来。简单的代码,其实蕴含了大量的规避问题的思考。
浅海:我想我需要休息一下。
深湖:再见。《红烧天堂》,启动!