• Bukkit编程杂谈:雷点|模板|思路|设计

    by 南外丶仓鼠 字数:48000字

    Bukkit编程杂谈:雷点|模板|思路|设计一、雷点|常见问题处理方案|必须写的代码○ InventoryClickEvent的注意事项○ 监听事件与优先度的注意事项○ Bukkit.getPlayer(UUID uuid)优于Bukkit.getPlayer(String name)○ 载入时或重载时需要注意的事情○ 异步与Bukkit API○ 不可监听AbstractEvent○ Location操作记得clone,而不是直接修改○ 请勿将玩家和NPC混为一谈○ Map遍历时不要修改○ 避免药水效果赋予失败○ 玩家加入时的处理○ 空气物品没有ItemMeta○ 差之毫厘,谬以千里○ 传送时会打断骑乘效果○ setTarget()只适用于攻击性生物二、常用模板|建议|设计规则○ 方块数据的操作与存储:BlockState与BlockData○ InventoryHolder优于标题○ 组装事件○ PlayerInteractEvent的注意点○ 时间的计算○ @EventHandler 的 ignoreCancelled = true○ 机制类插件的人性化设置○ 利用随机数○ 拒绝重复○ 插件的Log与Debug信息○ 提示权限化○ 指令TabComplete○ 手动寻路算法○ 不同版本的Material切换○ 各种Inventory的SlotID大表(1.12)○ Inventory安全性○ ItemMeta的子类○ 真实伤害与免疫后伤害○ 手动判等ItemStack○ 光照机制计算○ 序列化模板○ 有好的效果样式○ 避免卡顿○ 实体的速度○ 正版登陆判定○ NBT标签操作○ 大型方块与组合碰撞箱○ 方块、物品集合模板三、功能|思路○ 自定义原版升级经验○ 思考题:远程“便携”容器四、支持作者● 回复评分帖子● 加入QQ交流群● 打赏来自群组:Server CT

    一、雷点|常见问题处理方案|必须写的代码


    ○ InventoryClickEvent的注意事项

    在使用InventoryClickEvent时,不免会用到getClickedInventory()和getInventory()函数

    这两个函数有很大差别,无论何时,使用getClickedInventory()总比getInventory()保险

    getInventory()表示玩家额外打开的Inventory,即位于玩家背包上方的Inventory,如果没有额外的Inventory,则这个Inventory就是玩家的背包

    getClickedInventory()表示玩家当前点击的格子所在的Inventory

    同时地,玩家不一定点击的是格子,有可能点击GUI之外的区域,导致getInventory()有可能为null。

    此时需要先判定getInventory()是否为null,以免后续调用的时候出现NPE错误

    不过有一些注意点,点击格子之外时:

    若点击的是Gui外的灰色区域:

    getInventory()不为null,这是显而易见的,它会返回玩家打开的Inventory,点击的slot为999

    getClickedInventory()为null,毕竟玩家点击了本没有Inventory的位置

    若点击的是Gui内非格子区域,即边框区域:

    不会触发InventoryClickEvent

     

    ○ 监听事件与优先度的注意事项

    Bukkit提供了优先度系统以让开发者们更好地实现上面的功能,在@EventHandler注解处增加(priority=EventPriority.优先度等级)可为监听级设置优先度,等级越低越先执行,即执行顺序为:

    LOWEST>LOW>NORMAL>HIGH>HIGHEST>MONITOR

    因此,别的插件和你的插件可能会监听相同的事件,如果你的监听器需要用到setCancelled(true),请降低你的监听器的优先度,让事件触发时被阻止得更快,以免其他插件出现错误。

    简单来说,我们一般认为在优先度为EventPriority.HIGH以上的监听器中setCancelled(true)的行为是自取灭亡,这会造成别的插件和玩家的困扰!建议设置这种监听器的优先度为EventPriority.LOW及以下。因为有很多开发者懒得标注priority,导致他们的监听器优先度为EventPriority.NORMAL,此时可能会发生已经取消的事件触发了本不应该触发的代码

    同样地,如果你的监听器中包含提供后续效果、增添机制的代码,请一定要判定isCancelled(),并把优先度设置到EventPriority.HIGH以上,毕竟有很多开发者懒得标注priority,他们有可能在优先度为EventPriority.NORMAL的监听器中setCancelled(true),而你是无法改变这点的。

    至于同优先度时,事件是以什么顺序触发的呢:

    同优先度的事件触发顺序为它们的注册顺序(from 果粒橙姐姐)

     

    ○ Bukkit.getPlayer(UUID uuid)优于Bukkit.getPlayer(String name)

    尽量通过玩家的UUID而不是名字获取服务器玩家,原因有二:

    1.getPlayer(String name)会造成服务器负担(虽然不大,但是仍存在)

    2.正版玩家可以修改名字

    不过有时候的确需要Bukkit.getPlayer(String name),例如指令参数为玩家名

    另外如果获取不到玩家,返回null,注意后续的特判防止NPE

    至于Bukkit.getOfflinePlayer(String name)则不是很被忌讳,毕竟盗版uuid可以直接算出来,在线uuid从Mojang API/Yggdrasill API/特殊插件设置获取(from 星空姐姐、MiaoLio)

    对于多有Yggdrasill的服务器则需要另作处理(from 果粒橙姐姐)

    Yggdrasill API相关资料https://printempw.github.io/minecraft-yggdrasil-api-third-party-implementation/

     

    ○ 载入时或重载时需要注意的事情

    很多时候,服主们都是把插件扔进plugins,在服务器开启的情况下使用PlugMan或Yum这样的管理型插件来启用你的插件,如果你的插件没有自带reload,他们一般也会使用管理插件来重载你的插件,而不是/reload或重启。

    这个过程跳过了/reload和/restart正常该走的流程,以至于你的插件载入或重载时面临了一些问题

    例如已在服务器内的玩家信息未载入,解决方案是在每次启用插件时载入目前在线玩家的数据:

    同时地,如果你要自己写reload指令,不要无脑来一遍onDisable()、onLoad()和onEnable(),注意线程、监听器注册等的问题

    另一个比较不受欢迎的处理方案是拒绝reload,例如MCMMO,非重启服务器不能加载:这很安全,不过会导致使用的不便,以至于MCMMO不能重载的尿性成为服主们的饭后谈资。

     

    ○ 异步与Bukkit API

    很多开发者在追求线程安全的情况下,会选择异步处理

    但是一般情况下异步处理时不能调用BukkitAPI不过有时候可以(https://bdn.tdiant.net/#/unit/3-5

    此时的解决方案是在你的异步中再套一个非异步的BukkitRunnable(虽然我经常对此感觉不安)

    如果你的控制台出现以下报错,则大概率是出现这个问题了

    java.lang.IllegalStateException: Asynchronous xxx update!

     

    ○ 不可监听AbstractEvent

    曾今有人提出了这个问题:禁止一个玩家在服务器的所有举动(例如未登录时),直接监听关于该玩家的PlayerEvent,如果是Cancellable的,直接setCancelled(true)不就行了?

    这样的想法是想peach,不仅PlayerEvent,BlockEvent、EntityEvent都不可以这样监听。

    简而言之,有abstract关键字修饰的事件都不能监听!Bukkit API只允许我们监听详细的、实在的事件

    同样的,你在为自己和别的开发者创建自定义事件时,注意abstract的使用,它直接影响的自定义事件的可监听性。

     

    ○ Location操作记得clone,而不是直接修改

    这个是Java常识!

    例如Location#add(x,y,z)返回的是修改过的自己

    因此尽量先clone在修改

     

    ○ 请勿将玩家和NPC混为一谈

    在NPC在正常使用时和玩家没有明显的区别,即Citizens、FakePlayers等插件创建出来的假玩家会被代码检测到,且会被当做正常玩家处理

    有时候对NPC的“数据”修改时会出现报错,因此每次加载、保存、遍历时特判NPC是必要的,不仅为了防止报错,还可以减少能耗嘛。

    以下是特判的代码:

     

    ○ Map遍历时不要修改

    又是一个Java常识,修改正在遍历Map会报出ConcurrentModificationException

    可以把要更改的对象先扔到另一个类似于中转站的Map里,遍历完一起修改

    实在不行就遍历clone的Map,中途有需要再修改原Map。

    永远记住你可能会犯任何错误,当你发现你的Map在遍历时,另一个远在天边的类中的操作让你的代码报废就已经为时已晚了!

     

    ○ 避免药水效果赋予失败

    赋予药水效果时检测原来有没有同类型的效果,如果有先去除原效果再赋予

    不过有趣的是,有一个看起来比较棒的函数:

    LivingEntity#addPotionEffect(@NotNull PotionEffect effect, boolean force)

    可以强制覆盖原有药水效果,但不知道为什么被标记为过时了。

     

    ○ 玩家加入时的处理

    玩家进入服务器时对其操作要谨慎,如果是插件自身数据的加载就罢了,若对玩家的坐标、背包等数据通过Bukkit API进行修改,则需要等一会

    为什么呢?因为PlayerJoinEvent在触发时玩家还没有彻底载入完毕

    我们一般使用BukkitRunnable延迟几刻再进行处理:

     

    ○ 空气物品没有ItemMeta

    空气物品始终没有ItemMeta,对其getItemMeta()始终返回null

    即使把由内容的ItemMeta赋予空气物品,也是无济于事

    而有些物品则具有更详细的ItemMeta,后面会详细介绍。

    例如SkullItem,此时将其ItemMeta向下转型可以得到其SkullMeta。

     

    ○ 差之毫厘,谬以千里

    Bukkit.getPluginManager().registerEvents(@NotNull Listener listener, @NotNull Plugin plugin)

    Bukkit.getPluginManager().registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin)

    当你写出Bukkit.getPluginManager().registerEvent(@NotNull Listener listener, @NotNull Plugin plugin),常常很难看出来错在哪了。

    源于某人的提问,当时截图没有截到idea的改动提示,于是我百思不得其解。

     

    ○ 传送时会打断骑乘效果

    在玩家骑乘时,无论是传送玩家,还是传送坐骑或载具,均会打断骑乘状态

    因此你想移动骑乘中的玩家时,不如试试看对载具setVelocity(Vector vector),实在不行传送后重新设置setPassenger(Entity entity)(造成游戏体验不良好)

     

    ○ setTarget()只适用于攻击性生物

    不要被Bukkit的JavaDoc骗了!

    说的好听“friendly creatures may follow their target”,但是使用时非攻击性生物根本无动于衷

    这个方法只适用于攻击性生物,如果你要让友好的动物朋友们自己寻路,使用Nevigation

    我自己在测试时,发现上面这个speed很魔性,它以指数级上升,类似于某个高中函数

    测试得1.75f约为小僵尸移速1.25f约为正常走路移速


    二、常用模板|建议|设计规则

    ○ 方块数据的操作与存储:BlockState与BlockData

    机制类插件一般都会涉及方块数据的操作。

    该如何处理呢?

    如果是原版就有的数据,例如箱子内的物品,只需要对这个箱子的BlockState转成org.bukkit.block.data.type.Chest,再对其操作:

    这里可以发现:原版数据标签系统的数据标签与BlockState有联系(当然啊喂!)

    至于保存我们的方块,此时使用BlockData#getAsString()就可以了,读取时使用Bukkit.createBlockData(String dataString),再使用Block#setBlockData(BlockData blockData)即可。(1.13+)

    低版本不存在BlockData这个美妙的轮子,此时需要存TileEntity,转成NBT保存(from 果粒姐姐)

    如果要高效的存BlockState得用注册表序列化成int(from 海螺螺)

    如果是插件自己创造、操控的数据,建议使用Map把序列化的Location和数据对应起来,存进文件中。

     

    ○ InventoryHolder优于标题

    在很久很久之前InventoryHolder就已经存在了,但是大家都喜欢用标题去判定一个Inventory。

    直到1.13之后完全去除了直接从Inventory得到title的可能性,InventoryHolder广而应用。

    如何使用InventoryHolder?

    1️⃣ 自己写一个实现InventoryHolder的类,例如:

    2️⃣ 在创建你自己的Inventory时,owner填写你的Holder

    3️⃣ 判定时,只需要这样写就行了:

    你可以创建很多个Holder,对应不同需求的Inventory。

     

    ○ 组装事件

    Bukkit并没有给我们游戏内所有可能的事件,所以我们需要学会"组装"监听器。思路是通过多个监听器接连触发进行判定,使用Map存储需要中转的值

    例如玩家吃东西的动作可以分为两步,从前往后发生:

    消耗食品*1(PlayerItemConsumeEvent事件)

    饱食度+N(FoodLevelChangeEvent事件)

    因此我们分别监听以上两个事件,其中做一些交接工作即可。这里在玩家吃东西时输出了食品名称

     

    ○ PlayerInteractEvent的注意点

    如果你经常使用PlayerInteractEvent,会发现玩家点击一次,实际上触发了两次事件

    这是为什么呢?因为Bukkit分别触发了左右手点击的事件,例如玩家左手持火把,右手持剑,右击泥土方块,Bukkit会分别触发“火把右击泥土“和“剑右击泥土”两个事件。

    值得一提的是,即使玩家由一只手是空着的,或者两只手是空着的,也会触发两次

    这的确让开发者们困扰且恼火。我们一般使用PlayerInteractEvent#getHand()进一步判定左右手:

    与其相似的PlayerInteractEntityEvent等也是如此。

     

    ○ 时间的计算

    1️⃣ 冷却时间的处理

    一般使用Map存储玩家上一次操作的时间戳,下一次使用时进行比对,检测是否超过冷却时间

    开一个Runnable甚至Thread去每秒计算秒数是绝对浪费能耗

    不过如果需要可以实时显示、刷新的时间,一般采用后者。

    另外,在比较高的版本,你可以使用HumanEntity#setCooldown(Material type,int ticks),这样可以让玩家在一定的时间内不能使用某个type的所有物品。(参照末影珍珠冷却的效果)

    2️⃣ 世界时间的处理

    MC的一天为23000刻,粗略来说:

    傍晚开始的时候为14000刻

    深夜开始的时候为16000刻

    清晨开始的时候为23000刻

    这里提供一个美妙的函数,将世界时间可理解化

     

    ○ @EventHandler 的 ignoreCancelled = true

    这个巧妙的变量——ignoreCancelled,可以让你的监听器在已经被优先度更低的监听器取消的事件触发时正常执行!

    如果ignoreCancelled为true,则监听器会无视取消,一丝不苟地触发。

    反之,监听器在事件已经被取消时,保持沉默

     

    ○ 机制类插件的人性化设置

    这是设计上的小tricks:

    1️⃣ 在配置中允许用户调试启用的世界和禁用的世界

    毕竟有些服主不太会开群组服,他们的所有“服务器”都是集成于一个端,此时强制全局使用的机制插件就变得很令人讨厌

    2️⃣ 能自定义的常量都允许自定义

    可以省去很多麻烦的事情——例如项目归档后仍有服主跑来找你改内部数据

    不过在编写模板配置时需要注意其有序性,太多的常量挤在一个文件中不是很好,例如可恶的Essentials的config.yml,长达千行。

    3️⃣ 跟随版本自动更新的配置文件

    插件更新时难免会伴随配置文件的修修补补乃至翻天覆地的修改,尝试写一个转换配置版本小函数用不了你多长时间

    不然服主们会为每次更新都需要删去文件重新配置而抓狂!

    4️⃣ 语言文件的最大化前缀的分离

    很多服主都想让他们的服务器信息统一,因此可修改的前缀是很受欢迎的。

    前缀和后续的语言文字尽量分离,你只需要多写一行

    但是这可以解决服主修改几百条信息的前缀的麻烦。

    5️⃣ 自带注释的配置

    没有人想一边翻看教程一边配置!

    ※ 同时实现自动更新的配置(3️⃣)和自带注解(5️⃣)

    Miao和Karlatemp各写了一篇教程,我忘了链接了......因此直接献上我自己的做法:

    https://untiltheend.coding.net/public/UntilTheEnd/UntilTheEnd/git/files/master/src/main/java/ute/Config.java

    其中autoUpdateConfigs(String name)即为所求。

     

    ○ 利用随机数

    随机数不仅仅提供了随机的数字,我们可以用它干很多事情。

    例如某些插件看似无序不定时的“温馨提示”,实际上就是Runnable配合随机数的效果。

     

    ○ 拒绝重复

    你曾经有没有被这样的代码所困扰:

    有些人会把这些set函数压在一行,让代码变得又长又臭,如同酒罐底部的渣滓。

    问题是我们需要设置物品的格子ID往往是无序的,不能被for循环一步解决

    此时引入一个数组,将会使得代码令人神清气爽:

    这其实是很普通的一个做法,只是给无序的slotID们分配有序的下标,类似于链表但是简单且易懂。

     

    ○ 插件的Log与Debug信息

    一定要把插件的每一个动作都存进单独的LOG里!!!!

    一定要把插件的每一个动作都存进单独的LOG里!!!!

    一定要把插件的每一个动作都存进单独的LOG里!!!!

    不要问,问就是一年前在网易开服的时候插件出BUG被玩家爆破,又找不到记录无法惩治,最后不得已删档。

    谁知道哪些玩家利用了Bug呢?到那时候没有插件会帮你,滚去看千行的服务器Log吧!

    另外,这里存进LOG的信息输出到控制台的信息又有额外的讲究,那些琐碎的、重复易刷屏的不建议呈现,或是放进DEBUG模式再呈现到控制台。

    如果你稍微探索下,会发现Logger信息分Info、Warn、Error严重性递增,你可以使用它们对信息分级,让信息的传达性更高

     

    ○ 提示权限化

    服主不希望玩家知道服务器管理层面的内容,你一定也不希望

    万一这些本该隐藏内容存在Bug,玩家得知后加以利用呢?

    因此,我们需要做到提示的权限化,以下是一些常用做法:

    1️⃣ Help指令 玩家只能看到有权限执行的指令的帮助

    2️⃣ 插件更新信息只对Op发送

    3️⃣ 根据打开者拥有的权限,部分隐藏Gui中的按钮或内容

     

    ○ 指令TabComplete

    美观的指令语法,再配上可爱的Tab功能,是世界上最受人欢迎的东西了。

    Bukkit提供了两个实现Tab功能的方式,一个是TabExecutor,一个是TabCompleteEvent

    我更推荐使用前者,毕竟不用注册监听器嘛,还可以把指令一起写了。

    只需要重写onComplete(...)函数就行了,不过要注意发送者在补全的时候可能已经拼写了一部分,因此需要根据字典序匹配去生成补全列表:

    这个函数和onCommand(...)没啥区别,正常去写就行了,最后返回可补全的列表即可

     

    ○ 手动寻路算法

    如果你看不上原版的寻路算法,可以手写一个。

    大概的思路是使用广度优先搜索,简称BFS,本篇教程不再赘述,自行上网搜索。

    使用双向BFS,可以使用双倍的能耗,换取双倍的寻路距离(若使用普通BFS,双倍路程的计算量将会指数级增长

    切记不能使用深度优先搜索DFS或者取直线,不然生物要么找不到路线(此时能耗非常大,详情见DFS的原理),要么撞墙或是掉下悬崖。

    另外,注意控制临界点,毕竟太远了生物追踪不到嘛。

    BFS不影响性能的范围约为10格。

    如果你的生物的"视野"很远,此时使用BFS势必带来漫长且冗余的搜索。我们需要动脑筋,先让生物靠近一点或者使用A*等更高端的带估值期望的算法

    BFS的另一个用处就是处理连锁挖矿的问题,若使用DFS,挖掘的轨迹将会是很长的一条线(BFS是令人愉快的正八面体),以下是一个最多100方块的连锁挖矿:

     

    ○ 不同版本的Material切换

    高版本和低版本的Material可以说是大相径庭,不过Bukkit给我们留了一条后路:LEGACY_前缀

    这样我们就可以轻易地转换高低版本的Material,已经写好的代码如下:

    你只需要使用ItemFactory.fromLegacy(String typeName)获取Material即可,这里的typeName可以是高版本也可以是低版本。

    需要注意的是,如果你在高版本中,对使用以上方法生成的旧的ItemStack进行getType()操作,得到的Material是高版本的,而此时两个ItemStack进行比对时就不会相等。建议自己动手写equals函数

    另外,上面的代码让你无需在兼容不同版本时切换库,或者疯狂使用ItemFactory.fromLegacy(String typeName),例如在1.12的构建环境下直接调用Material.WOOL,插件放到1.13+中不会报错,上面的代码会自动做后续处理。

     

    ○ 各种Inventory的SlotID大表(1.12)

    以下是我"打印"的SlotID大表图片,欢迎直接伸手:

    格子ID=白色玻璃的个数-1

    格子ID=灰色玻璃个数+63

    BEACON

    BREWING

    CHEST

    CREATIVE

    DISPENSER

    DROPPER

    ENCHANTING

    ENDERCHEST

    FURNACE

    HOPPER

    PLAYER

    SHULKERBOX

    WORKBENCH

     

    ○ Inventory安全性

    1️⃣ 给予物品

    测试可知在满背包时直接Inventory#addItem(ItemStack item)是无效的,它会返回一个Map,其中包含给予失败的物品,通过这个Map我们可以进行后续的处理。

    如果你懒的话,可以直接模拟“背包溢出”——在玩家位置处掉落给予失败的物品,然而这难免会产生玩家和玩家或玩家和服主之间的纠纷。

    万一他们没有得到该得到的,胡闹一通呢?

    万一他们假装没有得到,胡闹一通呢?

    这些问题往往困扰服主,此时就是开发者做善事的时候了:

    做一个类似于物品中转站的功能,给予失败的物品都放入此中转站中,如果过了一段时间玩家仍未领取,则自动清理

    2️⃣ 可能存在的卡Bug方式

    取消你的Gui的InventoryClickEvent是远远不够的,因为玩家很有可能点击自己的背包内的物品,拖拽(Drag)到你的Gui中

    如果没有上传物品等功能,仅仅需要点击的Gui,建议开一个Set存储打开特殊Gui的玩家们并禁止一切该Set中的玩家的Gui操作

    在玩家打开Gui时就将其记录到Set中,在玩家关闭Gui时就将其从Set中移去。

     

    ○ ItemMeta的子类

    ItemMeta的子类有很多,在实际使用时,将ItemMeta转换类型后操作,再set回ItemStack即可。以下是两个例子:

    1️⃣ 自定义头颅 SkullMeta

    获取某个玩家的头颅:

    如果你想使用自己的头颅贴图,可以去了解CSCoreLib

    2️⃣ 刷怪蛋 SpawnEggMeta

    获取某种生物对应的刷怪蛋:

     

    ○ 真实伤害与免疫后伤害

    当你使用Damagable#damage(double amount)时,会发现参数为真实伤害,绕过了原版盔甲、免疫等的计算。

    如果你要进行原版伤害计算系统复现,以下对你有所帮助:

    1️⃣ 盔甲及附魔

    ※ 盔甲值与韧性

    ArmorDamageFormula Simplified.svg

    Damage dealt v3 Simplified.png

    Damage reduction v3 Simplified.png

    ※ 盔甲的附魔:附魔计算后伤害=计算前伤害* ( 1 - EPF综合 / 25 )

    魔咒能够减少的伤害伤害修正权重EPF 等级IEPF 等级IIEPF 等级IIIEPF 等级IV
    保护全部11234
    火焰保护熔岩岩浆块烈焰人火球22468
    爆炸保护爆炸22468
    弹射物保护恶魂烈焰人火球22468
    摔落保护掉落伤害(包括末影珍珠336912

    2️⃣ 药水效果

    抗性提升效果:计算后伤害=计算前伤害(1-效果等级0.2)

    伤害吸收效果:不需要额外判断,经测试,damage(double)会优先扣除黄色心。

    3️⃣ 伤害的原因

    原版伤害:

    伤害种类原始伤害计算公式或影响因素免疫后伤害计算
    摔落伤害(H-3) ❤盔甲、摔落保护、保护、抗性提升
    格斗伤害工具、攻击者、药水效果盔甲、保护、弹射物保护等
    火焰伤害岩浆每秒6❤,灼烧每秒1❤盔甲、火焰保护、保护、抗性提升、抗火(完全免疫)
    魔法伤害药水种类、等级抗性提升
    爆炸伤害取决于爆炸距离、爆炸强度盔甲、爆炸保护、保护、抗性提升
    窒息伤害每秒1❤生存模式下无法被减免,可以通过水下呼吸增长水造成的窒息伤害计算周期
    虚空伤害每秒2❤任何模式下无法被减免
    饥饿伤害每秒0.2❤饱和

    至于你自己杜撰的伤害,无非分为两种:

    伤害种类免疫后伤害计算
    物理伤害玩家拿着剑(1.8-)或盾牌(1.9+)格挡
    魔法伤害忽略盔甲的阻挡效果、玩家拿着鳕鱼的情况(仅个人看法[doge])

    当然,你的插件创造的伤害可能还有别的分类,这取决于你

    综上,从原始伤害计算免疫后伤害代码如下(这里不展示原始伤害计算过程):

    //TODO

    另外,你的damage可能会导致玩家死亡,然而“某某某死了”是很难看的死亡信息,而且起不到公示的效果,因此你可以考虑监听死亡事件,为不同的死亡输出不同的信息

     

    ○ 手动判等ItemStack

    有一些物品看上去一样,但是使用equals判断时并不相等,这可能是因为:

    1️⃣ Name不一样(例如RPGItem会改掉物品名方便其判定,在其前面增加无用的稀奇古怪的颜色代码)。

    2️⃣ Material不一样(例如上文提到的1.13+中LEGACY_WOOL和WHITE_WOOL)。

    3️⃣ 其他,例如NBT和Lore等,不过这些一般不会出错。

    此时你需要手动判断,把有关字符串的处理掉颜色代码,把LEGACY_的Material变成其对应值再比对。

     

    ○ 光照机制计算

    原版的光照系统这里就不全面介绍了,这里只提几个注意点:

    1️⃣ 天空光照不随白天黑夜交替而改变,永远都是15

    2️⃣ 完全透明的方块完全透光,半透明的方块不透天空的光,不透明的方块不透光。

    3️⃣ 天气对天空光照的影响

    时间+天气实际天空光照
    中午,晴天时15
    中午,降雨降雪12
    中午,雷暴10
    午夜,晴天时4

    这里提一个很新奇的玩意——不存在的光源LightAPI——https://www.mcbbs.net/thread-1016938-1-1.html

     

    ○ 序列化模板

    1️⃣ 实体序列化(通过NBT): From 果粒姐姐

    实体转换成NBT:

    NBT转换成实体:

    用这个更好(nms.EntityType中)(替代上面代码10-12行):

    1.13-没有Function形参

    1.14+有,作用是在new完实体之后把实体传给你的Function做预处理

    function中方法参数是给你预处理的实体,返回值是你处理之后的实体

    2️⃣ 坐标/区块序列化模板(Loc3D.locToStr(Location loc)和Loc3D.strToLoc(String string))

     

    ○ 有好的效果样式

    1️⃣ 药水效果的粒子隐形

    new PotionEffect(PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles)

    new PotionEffect(PotionEffectType type, int duration, int amplifier, boolean ambient, boolean particles, boolean icon)

    其中particles参数表示有无粒子效果。

    其中icon参数表示有无图标。

    2️⃣ 光灵效果的颜色

    假设箭的颜色卸载Lore里

    1️⃣ 监听光灵箭射出时,该箭的Lore,获取其颜色。

    1️⃣ 监听光灵箭击中时,取消无色效果,配合原版Scoreboard,赋予上对应的颜色。

     

    ○ 避免卡顿

    1️⃣ 网络操作异步处理,例如查找更新、获取贴图等

    2️⃣ 加载数据库、较大的Yaml异步处理,例如玩家数据、Mysql连接等

    不要像脚本插件Skripts一样不按以上建议行事,它把开服速度延迟了5s以上!

     

    ○ 实体的速度

    0.2-玩家正常走路速度

    0.1-玩家正常飞行速度

    对于速度的理解因人而异,你可以自己试试看这一段代码,通过时间感受速度的变化:

    另外,玩家的速度在其退出时会保存,下一次上线时不会重置

     

    //以下内容等待更新!

    ○ 正版登陆判定

    //TODO

     

    ○ NBT标签操作

    attribute管理,最大血量,nbt标签

     

    ○ 大型方块与组合碰撞箱

    大型方块的方向、碰撞箱拆解

     

    ○ 方块、物品集合模板

    透明方块集合

     

    工具物品集合

     


    三、功能|思路

    ○ 自定义原版升级经验

    经常地服务器需要延长游戏周期,或增加难度,会选择增加升级所需经验。

    但是Bukkit并没有给我们自定义升级经验的API,市面上也没有现成的插件和库,不如自己手写一个。

    该怎么实现呢?这里就考验我们的思路和实现能力了:自定义升级经验,其实不需要复杂地修改底层代码。

    以下是思考历程,可供各位参考:

    1️⃣ 自定义升级所需经验,等同于自定义每级的经验上限

    2️⃣ 上限是不方便直接修改

    3️⃣ Player#getExp()是进度百分比而不是经验点数

    4️⃣ 那么何不在玩家经验被修改时,按比例折算,不就间接地魔改了“上限”吗?

    以下是实现历程,可供各位参考

    1️⃣ 服主自定义某一级需要的经验到配置里供插件读取

    2️⃣ 有一些没有被自定义的等级,使用原版经验计算

    3️⃣ 玩家获得经验时,按比例折算,同时Player#getExp()便于我们直接取用

    以下是代码:

    ○ 思考题:远程“便携”容器

    玩家常常会为不能及时回家取东西而感到麻烦和苦恼,何不弄一个远程操控容器的功能呢?

    远程使用熔炉、附魔台、漏斗、箱子、投掷器等等

    远程与家里的村民交易

    但是每个玩家的容器显然是很多的,于是我们需要使用Gui管理面板去帮他们整理,支持多页等。

    同时地,玩家经常错误地添加了一些容器,我们需要提供删除容器的功能

    你会怎么实现呢?

    我的代码(很乱且不规范,仅仅是实现了功能):

    核心功能:https://gitee.com/hamsteryds/FunctionalToolSet/tree/master/src/main/java/fts/gui/capablegui

    指令衔接:https://gitee.com/hamsteryds/FunctionalToolSet/blob/master/src/main/java/fts/FTSCommands.java 第133-168行

    使用到的Loc序列化API已经在本文介绍了。

    20210212202430

    20210212202436

    20210212202448


    四、支持作者

    ● 回复评分帖子

    评人气不会扣自己的哦~

    ● 加入QQ交流群

    UntilTheEnd|FunctionalToolSet|官方:1051331429

    ● 打赏

    爱发电:http://afdian.net/@HamsterYDS


    来自群组:Server CT