淺析mc中漏斗卡頓的部分來源
mc中漏斗的卡頓是比較大的,很多人談漏斗色變,當(dāng)然這是完全沒必要的,因為生物的AI更卡一些(笑)然而漏斗的卡頓確實很大,其中比較大的一個部分歸結(jié)為尋找上方的物品實體以求吸取這一過程,但是其實還有一部分的卡頓來源于屎山代碼,這一部分長時間不被記載,比較不為人知?,F(xiàn)在我們來看一下:
首先從一個方法講起:net.minecraft.world#World.updateComparators

這是一個毫無槽點的方法,用于更新一個方塊附近的比較器。這用于讓比較器能夠檢測容器。那么我們看看它在什么時候可以被調(diào)用。下面是net.minecraft.block.entity#BlockEntity中的兩段代碼:

我們看到,markDirty() 方法用于標(biāo)注某個位置方塊實體的信息已被更改,保存世界的時候需要注意。它還會調(diào)用 world.updateComparators() 以更新附近的比較器,畢竟方塊實體的一大類是容器,當(dāng)容器內(nèi)容被更改時,更新一下比較器是必要的,至此,一切正常。
然后很快你就會發(fā)現(xiàn)事情變得非常的不正常……
我們現(xiàn)在看一下漏斗邏輯的控制,這部分代碼全都在net.minecraft.block.entity#HopperBlockEntity里面。以下是漏斗執(zhí)行一次吸取操作的代碼:

我們看到漏斗會優(yōu)先檢測上方有沒有容器,有的話就對容器的每一格執(zhí)行一次遍歷,如果能夠吸取這一格就吸取掉,然后結(jié)束遍歷,沒有的話就試著吸取物品實體。
問題就出在從容器吸取這一過程,我們來看代碼。

從這里,我們就可以看到麻將的迷惑操作了。我們可以從中看到主要的邏輯(以下并非是嚴(yán)格的源代碼邏輯,是經(jīng)過編者簡化的等價的邏輯):
把要吸取的容器的那一格取出來,如果非空進(jìn)行接下來的操作
將容器的這一格內(nèi)容復(fù)制一份,然后在原來的容器中,將這一格的內(nèi)容物數(shù)量 -1
試著將減掉的 1 個內(nèi)容物放到漏斗里面
如果放進(jìn)去了,那么很好,進(jìn)程結(jié)束
如果沒放進(jìn)去,就把剛剛復(fù)制的內(nèi)容替代容器那一格的內(nèi)容,相當(dāng)于撤銷剛剛 -1 的操作
這個邏輯雖然很奇怪,但是好像也能實現(xiàn)目的,貌似沒問題,但是結(jié)合剛才說的比較器更新,問題就大了。
這個過程中,假設(shè)吸取失敗,那么會先減掉容器的 1 個內(nèi)容物,然后再復(fù)原,這兩個過程都會更改容器的內(nèi)容,理應(yīng)調(diào)用 markDirty() 方法,從而引發(fā)比較器更新。事實上它確實會調(diào)用,比如我們以箱子為例,它的物品欄在 net.minecraft.block.entity#LootableContainerBlockEntity 中實現(xiàn),我們看看漏斗吸取過程中調(diào)用的兩個方法,一個是 removeStack(),一個是 setStack(),它們確實都調(diào)用了 markDirty():

于是,漏斗每次對一格失敗的吸取,會產(chǎn)生 2 次比較器更新,這問題貌似也不太大,但是別忘了每次吸取,漏斗會對上面的容器的每一格進(jìn)行遍歷!
現(xiàn)在考慮這樣一個情形,上面有一個塞滿東西了的大箱子,下面接了一個漏斗,漏斗里面的物品類型和箱子的內(nèi)容不匹配,因此吸不進(jìn)任何東西,并且漏斗不是滿的(所以會嘗試吸東西)。那么漏斗每次嘗試吸取一格的操作,都會是失敗的。于是每一格都會被遍歷一遍,嘗試吸取一遍——
這個過程中,上面產(chǎn)生 2 次沒必要的比較器更新的 extract 方法,將被調(diào)用 54 遍。
這會產(chǎn)生 108 次比較器更新?。。。。。?/strong>
也無怪乎漏斗的卡頓較大了,這樣的卡頓當(dāng)然是顯著的,都能拿來做卡服機(jī)了(逃)
不愧是你啊麻將
啊↑啊↓我要把麻將撕成兩半!