eboer's Boat

Neboer's Blog isn't Only About Technique
dummy

小麦机器人凯露
nodejs凯露minecraft 473

用mineflayer库和javascript语言来实现一个mc机器人来种小麦收小麦,似乎并不复杂,但是真的很有趣……

用javascript写的mc小麦收割机器人

想写mc机器人的想法由来已久,这次终于实现了。我是在github explore的每日推荐里看到mineflayer的。简单介绍一下这个项目:

mineflayer是一个专注于node-minecraft的npm项目,它让用nodejs制作mc客户端和服务器成为了可能。其中mineflayer这个仓库本身就是一个优秀的minecraft客户端,它实现了一个机器人,你可以调用它的函数接口方法来控制它完成玩家能够在mc中完成的一切任务,包括走路、放置方块、摧毁方块、吃东西、(用工作台等)合成物品、使用熔炉等等过程。mineflayer更准确的来讲是一个致力于minecraft javascript化的大型项目,这让它在众多mc相关的库里脱颖而出,成为了“写一个机器人”的最佳选择。

Watcher-On-The-Field项目地址:https://github.com/Neboer/Watcher-On-The-Field

一、早期的糟糕设计

这个机器人并不是一开始就叫做“麦田里的守望者”,而是我在开发中期才给它取的一个名字。起初的时候,机器人仅仅是一个测试的版本,只能完成基本的收割和种植的任务。后来还出了一些问题,导致后来机器人的设计完全基于async等的异步方式,这样也增加了代码的可读性,效果确实很好。后来机器人增加了“如果失败就反复执行直到成功”和“超时之后直接失败”的逻辑,虽然在开发的最后(也就是现在)确实觉得这些逻辑是没有意义的,其实这证实了mineflayer库还是比较稳定的,也说明了我之前的代码逻辑并不符合mineflayer的设计规律,但是直到现在,还有很多的代码依赖于hooks文件夹内的类似方法,并且机器人基于async/await的设计并没有改变。我认为基于async的异步方法更加符合“mc机器人”这一理念,同时也更加pythonic。之前用回调的设计会导致nodejs的堆栈裂开,我只能说js真是一门nb的语言(大雾)。

二、pathfinder从入门到放弃

在早期版本中,机器人依赖于mineflayer-pathfinder进行“走到某一点”的操作。这个库也是mineflayer众多库之一,但是它真的是 一个 库吗?

pathfinder这个库其实非常强大,它可以让机器人翻越高山、穿过河流、在岩浆池里寻找到一条最佳的路线抵达目的地——甚至可以在没有路的时候自己用身上已有的方块创造一条路——甚至可以在身上没有方块的时候动手挖下一些方块。这个库设计的目的就是,它可以让机器人“随叫随到”,不管中间有什么艰难险阻。

可能现在你已经能够意识到问题了——这个库的功能太复杂了,在平整的地面上工作的机器人没有必要拥有这么多的功能。最关键的一点就是pathfinder的代码写的并不够模块化,这也就意味着你永远没有办法“关闭”这个过程——机器人在有目标的时候,始终都在向着目标移动,没有目标的时候,也时刻在等待着目标的出现,这个过程在有的时候会严重的干扰到机器人正在准备做的事情,比如我不得不hook掉机器人的dig方法,让它在前进的时候不要把面前没有成熟的小麦当成“阻碍”打掉(its quite silly right?)。

三、困难重重的“抵达”判断

所以pathfinder不能用了,那么一个突出的问题就出来了——怎么让机器人抵达一个目标点(x, z)然后停下来。

这个是一个很标准的控制学问题,我先说一下我们有什么:on("move",func(){})。传入的函数func会在机器人移动的时候不断被调用,同时你可以通过bot的entity属性来获得机器人的位置,你还可以直接获得机器人的速度向量。

最容易想到的就是,沿着直线走路,机器人首先看向目标的方向,然后按下“w”,同时在onMove里不断的计算实体坐标和目标点的二维距离,如果小于一个定值就判定为“到达”,调用回调函数或者执行resolve()方法。

看起来很美好的设定,但是这个“判断到达”的阈值不能够设的太小,因为机器人可能会“错过”迫近目标点的过程,这也就导致了机器人不能精确的“走到”一个点,而这是不能够接受的,因为机器人“捡起”目标物品是需要精确的走到目的地的。

第二种想法就是,记录机器人一开始距离目标点的xz坐标差,然后时刻判断是否有坐标差的符号与最初的差值符号相反,如果是,这说明机器人已经走过了目标点,只需要停下来就可以了。

这个方法的问题比第一个还大——机器人不能够在离目标有一定距离的时候停下。而且当机器人与目标点距离过近的时候(甚至可能就在目标点),这个方法会让机器人无限向着一个方向走下去(所以不要停下来啊)(这nm也能迫害团长是我万万没有想到的)

其实解决这个问题的方法真的很简单,只需要跳出“距离”这个怪圈就行,只需要提前计算好机器人走到目标点所需要的时间,然后让他走动,到指定时间之后自动停止即可,这样就完美的解决了上述的所有问题。当然,地面上是不能有凸起或凹陷的方块的,因此机器人需要工作在平坦的地面上,堆肥桶、水坑等的表面必须都得被活板门或半砖所覆盖,这个限制一直沿用至今,并且极其稳定,没有问题。

四、机器人的键盘上没有shift

机器人当然没有键盘,它只是一个无情的发包稽器。看起来机器人工作要比我们玩家容易得多,但是mineflayer并没有那么省心。

机器人有一个很重要的功能叫做Crafter,作用就是把身上过量的小麦打包成干草块。实际执行的过程是把身上的九组小麦依次放进工作台的窗口中,然后按住shift对工作台右侧的格子进行左键单击。这看起来容易的不能再容易的操作竟然被mineflayer限制的困难重重——把九组小麦依次放进格子没有问题,只需要执行十八次点击左键的事件就可以了。但是在最后一步操作——按住shift点击最右侧的格子的时候,本来客户端应该发一个稍微和正常左键点击有点不同的包(mode=1,正常的左键单击是mode=0的点击),但是mineflayer的clickwindow里却不能够设置mode为1,会报“Assert Error”的错误,我的猜测是:由于在shift合成之后,服务器会返回大量的数据包,mineflayer不能够确认什么时候合成会结束,因此干脆禁止使用其他的mode对window进行点击,真的是不好办。

解决的方法?直接调用bot._client的writePacket方法即可。直接向服务器发送一个rawPacket(当然,还是需要进行编码和排队的),然后等待若干毫秒,免去所有的麻烦事,再也不用一个一个分析服务器返回的数据包了。

五、机器人是一个憨批

当乱七八糟的问题终于尘埃落定,机器人开始正常工作了。但是新的问题随之而来——这个机器人的工作效率太低了。

测试的时候,小麦田通常比较小,机器人走两步就到了,看不出来收小麦的效率有什么问题。但是服务器上开辟的小麦场是一个中等大小的小麦农场。一开始我通过调用bot.findBlock方法来寻找合适的小麦或者空的耕地,但是这种扫描非常低效率而且容易出错,比如机器人总是在忽略特定坐标上的方块,直到这个方块的状态改变才会意识到地图更新了——举个例子:有一块小麦在机器人的附近突然成熟了,但是机器人视而不见,继续在田野中间等(继 续 等),直到你亲手打掉这个小麦机器人才会反应过来,走过去在空的耕地上面种上种子——像个憨批。在看了findBlock函数的实现之后(其实就是不断地进行blockAt进行方块查询),我还是手动实现了一个在指定层(y坐标)和指定xz范围内的矩形农场搜索方法,一次遍历就可以直接找到最近的成熟小麦(可以打掉)、未成熟小麦(可以催熟)和空的耕地(可以种植)。

既然机器人已经这么强调“周围有什么”了,干脆也把周围的掉落物品一并找了吧!(赶紧走过去捡起来),干脆直接把周围的玩家实体也找到吧!(一旦收到“toss all items”信号,机器人就会向最近的玩家我“喷射”干草块)。那么为什么机器人不直接把自己的一些类似于饥饿度、小麦种子数量之类的关键状态一并传过去,以便于判断系统判断是否要吃东西/堆肥呢?说干就干,于是机器人的结构变成了收集游戏状态->做出决定->执行命令。这个工作流实现之后,虽然没有明显提高机器人的工作效率,但是提高了代码执行的效率,还是很合适的。

最后的优化非常简单,机器人只需打掉/种植距离自己最近的成熟小麦/空田就可以了,这样一来机器人的平均命令执行时间大大缩短,极大的增加了小麦的收割效率。

六、麦田里的守望者

于是,机器人便已经可以放到github上了。这个时候的机器人还没有很多的可配置性,于是我使用了config.js的形式做了简单的配置系统,让机器人可以任意被应用在各个服务端。然后给这个项目写了一个README,取了一个很熟悉的名字——麦田里的守望者,当然不是那本书,而是Watcher On The Field,想想看这个名字还真的挺形象的。其实这里也变相的点了《天气之子》里的帆高最喜欢的这本书,另外后者也是我最喜欢的电影之一,哈哈。

被迫害的黑猫凯露

经过我和我肝帝同学的不断努力,机器人终于工作在了宽敞明亮的大棚(划掉)农场里,工作就是不停的收小麦种小麦,周而复始,十分稳定,从未间断。当我上线进入农场之后,只需要对她说“把所有的物品给我(tossallitems)”,她就会乖乖照做,把身上所有的东西都扔给我,然后继续工作。

你可能很奇怪为什么这里我称呼机器人的时候用的是“她”,这个是因为这个机器人是……是凯露呀!我在在服务器上部署这个机器人的时候,给她起一个名字,正好这服务器挺二次元的,我也就把这个机器人起名凯露。但是取名字的时候忘记了凯露的罗马拼法,遂以其真名希留耶“kiruya"代替。万万没想到kiruya真的是一个mc账号的id,这个账号拥有一个很细腻的凯露的皮肤!

既然是猫猫,那么迫害起来应该会容易很多了(笑😀)。我首先

  • 用凯露不分昼夜肝出来的干草块做了一堵高墙,取名为“凯留壁”,凯留壁流弊流弊(??

    然后我

  • 把凯露吃面包的机制改成了“身上有面包再吃”而不是“饥饿的时候自己做”,而且每次只给她一点面包,可怜的凯露经常饿着肚子工作。

  • 就好比封面的大图上门口的牌子上的字样,可以打机器人,但别打死……可怜的猫猫。

  • 凯露不会睡觉,就算是累了也得忍着,在代码的驱动下完成一个又一个的任务……头顶上一直盘旋着幻翼,农场外虎视眈眈的僵尸和小白……

  • 有一次我长时间没有上线,上线之后凯露已经只剩最后一点血了,原来她因为太久没有吃东西,饥饿度已经掉到了0,哭了,要是困难难度的话她就被活活饿死了。

  • 末影人有时会在下雨的时候进入农场内部,此时的凯露不敢抬头看,也不可能打得过,只能默默的低头干活(其实机器人的代码里并没有特意注意到末影人,因此我也特别担心啊!担心末影人会因为凯露的指针突然扫过而攻击她,但是实际上并没有发生这种情况,因为凯露只会盯着脚底下的那层麦田看。)

可怜的凯露,但是迫害她真的好有意思啊!!!!!!!

充满欢乐的现世研车万mc联机服务器

现世研车万mc服务器➰

没错就是这么二次元。这个本来是动漫社的一个mc服,建在阿里云上的服务器,日常活跃的用户有四五个,总共大约十多个人吧。

服务器内部有大量的挂画,都是物品展示框+地图制作出来的,我就认得一个初音的画(

服务器定期举行一些活动,比如挖矿等等,活动结束之后会有很多的奖励,这个腐竹还是很会玩的。

你以为这样就完事了?不要小看这群人的搞事情的能力啊kora!

大家一起做接头霸王🍙

我把机器人部署到服务器之后,很多人过来参观,腐竹突发奇想,找到这个玩家的皮肤,用指令生成了一批凯露的头,取名“Kiruya的头”,然后率先换成了凯露头。

服务器里有各种各样的散落的猫猫头。

大家感觉好有趣,然后就一起换成了凯露头

大家进入田野和正在工作的凯露一起合影,每个人都看着头顶,每个人都是臭鼬头

大家回到了我家里门前的空地上,抓了一只僵尸,腐竹把可怜的僵尸也换成了凯露头

结果就是一只有着臭鼬头的僵尸伸着双手冲向玩家。

“新的身体在哪里……”

草,这群人真太会玩了。

我又想到了群里有人问,凯露死了之后会怎样,我回答说服务器会自动退出凯露的程序。有的人说凯露死了之后会变成只剩下猫猫头的幽灵,在小麦田上游荡……

结果当天服务器上就出现了一些只戴着凯露头,喷着隐身药水的玩家,在麦田上来回走……摄影机忠实的记录了这些珍贵的画面(

我们春黑必井

写在后面

mc玩家大多能肝且富有创造力,我的那个肝帝同学就是如此。在这个服务器里发生的各种事情都让我觉得很有趣,让人有一种归属感。同时这个小麦机器人真的是我一直以来希望做出来的东西,而且不得不承认mineflayer上手简单,但是确实存在很多问题,可以通过代码层面的优化来尽可能地规避。watcher on the field这个项目并不是一开始就这样的,起初也经历了很多次碰壁,最后这个代码设计也算是摸索+询问出来的。最让我印象深刻的是mineflayer的discord群组里常年活跃的大佬和一些乐于帮助别人的人,很多问题都秒懂,大家都很棒,尤其是聊到了discord本身的问题的时候,我说中国大陆正常来讲是不能够上discord的,他们的反应真的挺好玩(具体是啥就内緒なの~),开发者真的不分国界,社区里聊得开心就好。

我真的被mineflayer的规模惊讶到了,那么多的库,那么多的属性数值,那么多的contributors,属于让人看一眼就能够记住很久的那种大型的项目。没想到真的有人用nodejs来玩minecraft,找到这个库,完成了一个人生小目标!