科學的Minecraft:速度?重力加速度?自由下落極限速度?
在現(xiàn)實生活中,我們把物體在某段時間內(nèi)的位移與這段位移所用時間的比值叫做“速度”。那么,在一個虛擬的世界中,是否有速度呢?
警告:這篇文章會有大量的代碼以及原理講解,請慎重觀看!
這篇文章涉及到的表觀現(xiàn)象:各種有關(guān)于移動的掛,MC中速度的表示,MC中重力加速度的計算,自由下落極限速度
一.Minecraft的速度定義
首先,MC里面定義了兩種測量速度的字段:deltaMovement與speed。
deltaMovement是一個矢量,它決定了在這1tick內(nèi)實體運動方向和距離,用它除以1tick就是平均速度。
v平均 = deltaMovement / 1t = deltaMovement / 0.05 (TPS=20)
speed則是標量,相當于平均速率的量度,是一個計算平均速率的參數(shù)。

二.非玩家的LivingEntity移動
首先,我們要了解speed到底儲存的位置——也就是Attribute里面。Attribute里面儲存了一個MOVEMENT_SPEED,也就是移動速度。而我們得到的速度則是通過直接讀取speed字段,而這個字段正是由Attribute得出的。
由于代碼又看不到具體調(diào)用,只好又修改LivingEntity.setSpeed,看看它的堆棧。

運行/de:debuginsert net.minecraft.world.entity.LivingEntity 2036 false dump插入代碼,得到了以下堆棧:


通過Server的堆棧我們可以看出生物(Mob)的運動都是由MoveControl處理的,下面先講一下Mob和LivingEntity的移動方式。
move的計算就在它的tick方法里面。這個代碼比較長,所以只會講一部分。
//注:其實生物運動分為navigation->move(X,Z軸上的移動)->look->jump(Y軸上的移動),這里只找出了有關(guān)于speed的部分

由于代碼過長,我參考了1.15.2的MDK源碼作為參考,下面我只會講這一大部分里面的STRAFE(你沒聽錯,這個單詞意思是掃射)部分,這里聯(lián)系到了一會要講的move。
首先,獲取到speed(65行),同時計算得出實際的行進速度(66行)。之后獲得生物根據(jù)面朝方向的向前(67行)和左右方向(68行)上的移動距離,計算出實際上的位移(69-72行),獲得這1tick內(nèi)能前進的距離的參數(shù)(73行,這時候f5單位是s^-1)。接下來應該是獲取到將要走到的位置,檢測是否能到達(74-86行)。之后,設(shè)置speed(87行),傳遞X,Z方向上的位移(88-89行),設(shè)置狀態(tài)為WAITING(90行)。
這里我們看到了怎么設(shè)置speed,但是沒有看到move,不用著急,這只是我們了解它的第一步。

緊接著,我們發(fā)現(xiàn)Mob這里的邏輯完成之后又調(diào)用了上一級的LivingEntity.travel,這個就是我們下一步說的移動的關(guān)鍵之一。
還記得MoveControl里面的“傳遞X,Z方向上的位移”嗎,我們可以看到,這個方法的傳入值就有xxa和zza,那么我們在這里找到move的可能性就很大了(ArmorStand沒有繼承Mob而直接繼承了LivingEntity,它是一個例外)
由于這個方法有200行,所以我只能講解一部分,發(fā)出一部分代碼。

下面對這段代碼進行解釋:
獲取腳下方塊(1889行),之后得到行進阻力(1890-1891行),通過moveRelative方法進行阻力行進運算(1892行)。對移動加上攀爬時的Y軸方向速度(1893行),將這些值代入,move中進行實際的移動(1894行)。(這些為該刻處理運動的部分)
獲取現(xiàn)在的移動速度(1895-1899行),進行Y軸速度的判斷:
? ? 1.具有漂浮效果,將Y軸速度改為 漂浮等級+1-原Y軸速度
? ? 2.如果重力存在,將Y軸速度向下加0.08(沒有緩降效果)/0.01(具有緩降效果)
? ? 3.在客戶端運行且區(qū)塊未加載,將Y軸速度改為-0.1或0
進行此判斷后重新計算速度,現(xiàn)速度X,Z軸上速度為原先的速度乘以阻力比例,Y軸則是判定結(jié)果乘上0.98。(這里是下一次移動前的準備)

關(guān)于阻力比例,公式是這樣的:
當實體站于地面,v = speed * (0.21600002 / (blockFriction ^ 3)
當實體在天空中,v = flyingSpeed (0.2)
這個方塊阻力和動摩擦因數(shù)μ差不多,算得的速度應該是speed*(0.6/friction)^3,因為浮點數(shù)誤差,所以原先的0.216變成了0.21600002,不過這對我們得出結(jié)論沒有影響。
下面來看它調(diào)用的最終級方法:move
這個方法定義在Entity里面,也就是所有實體公用的,不像LivingEntity的travel一樣只適用于LivingEntity和它的子類Mob。
由于這里又是150行代碼,我們還是講精華部分。

下面解讀一下代碼,注意,這里所有的XXX > 1.0E-7D都是在判斷移動發(fā)生,由于浮點數(shù)誤差,計算后的數(shù)據(jù)即使你看的是0,其實還是有很微小的值,用XXX == 0是不能起效果的:
(491和505行:報告狀態(tài),對于移動判斷沒有用處,就是調(diào)試用的)
492-496行:如果阻礙運動參數(shù)大于0,則應用阻礙,并將下一刻的參數(shù)和位移置為0
498行:應用潛行(對于玩家的)
499行:計算碰撞,返回實際的位移
500-503行:如果位移不為0,啟用真正的移動
真正的移動分為了兩部分:第一步,setBoundingBox,設(shè)置包圍盒。此時,該實體的包圍盒已經(jīng)變化了,但是渲染位置不變;第二步,setLocationFromBoundingbox,從現(xiàn)在的包圍盒位置獲取到現(xiàn)在的實體位置,這時候,渲染位置才改變到正確位置。
好了,我們看到了LivingEntity移動的全過程,那么如果不是LivingEntity呢?
三.非LivingEntity的移動
這里我將講這兩個非LivingEntity實體:下落的方塊,點燃的TNT。
1.下落的方塊(FallingBlockEntity)
下落的方塊的移動是寫死到tick里面的,由于又是一大串代碼,還是取其精華。

這幾段代碼里面清楚地寫到了move和deltaMovement,可以判斷出:這就是它運動的方式??墒悄憧赡軙枺夯钊苿酉侣浞綁K也會造成位移,這里沒有體現(xiàn)啊?其實,move的第一個參數(shù),MoverType里面定義了不同的移動方式,這種運動方式不需要實體自己計算,活塞推動是MoverType.PISTON,而實體自己移動是SELF。
2.點燃的TNT(PrimedTNT)
點燃TNT的位移是在兩個地方計算:tick和構(gòu)造函數(shù)。

這個代碼有兩個部分,一部分是構(gòu)造函數(shù)的,另一個在tick里面。tick里面的代碼和FallingBlockEntity的代碼大致相同,不再解釋。構(gòu)造函數(shù)中的代碼是點燃TNT的時候,TNT會“蹦一下”,這個的計算就是這里體現(xiàn)的。具體來說,這一刻跳起的高度應該是0.2(浮點數(shù)誤差x3),而X、Z軸上則是通過隨機數(shù)產(chǎn)生一個方向位移0.02。
四.玩家的移動
還記得最上面的那張Client堆棧嗎,我們在那里發(fā)現(xiàn)了Player的蹤跡。我們通過這條線索向下查:

這里能夠看出,speed是由服務(wù)器端計算的,并將這個值存到Abilities里面發(fā)送回來。
但是,deltaMovement去哪了?

由于Player是LivingEntity的子類,翻找過后,發(fā)現(xiàn)在服務(wù)器端的ServerPlayer覆蓋了tick并且沒有進行super調(diào)用,而我們普通LivingEntity進行移動正是通過tick調(diào)用aiStep進而調(diào)用travel實現(xiàn)移動的!并且在這個新的tick里面,并沒有aiStep和travel的蹤跡......
那么ServerPlayer是怎么實現(xiàn)移動的??
那么在這個問題解答之前,大家可以想一想飛行掛、快速移動和載具加速這種東西是怎么產(chǎn)生的。并且,如果你用過tweakeroo的靈魂出竅(FreeCamera),在靈魂出竅的時候被推動,你有可能就懸空了......在這些現(xiàn)象的背后,是什么原理?
對,沒錯。因為服務(wù)器端只負責了“移動你”,但是不負責“計算使你運動”,而客戶機端才真正負責了玩家的自身(PLAYER)實體移動。關(guān)于這一點為何這么實現(xiàn),其實也很簡單的道理——因為你可以操控玩家,而其他的實體都靠著物理引擎和AI直接運算。
//注:服務(wù)器端不是什么玩家移動都靠客戶機端,因為活塞移動等計算是在服務(wù)器進行的,因為活塞移動等是服務(wù)器操作,活塞移動同時就進行了move方法調(diào)用。而客戶機端掌握的移動只有PLAYER的自身移動。
為了證明這一點,我們可以去翻找LocalPlayer的源碼。

在LocalPlayer里面,我們發(fā)現(xiàn)了sendPostion發(fā)送位置,這代表了客戶機計算位置的正確性。為了繼續(xù)證實,我們翻找一下ServerboundMovePlayerPacket的執(zhí)行者,也就是ServerGamePacketListenerImpl。

由于代碼長度太大,120行左右,就簡單說一下它干了什么:
檢查數(shù)據(jù)的正確性
如果玩家還在等待區(qū)塊加載傳輸,則“固定”玩家位置,防止因為客戶端區(qū)塊未加載導致的掉入虛空
如果玩家是乘客,那么進行加載區(qū)塊緩存
如果玩家睡眠時的位移超過1,將被強制傳送回床上
如果在1tick內(nèi)玩家發(fā)送了超過5個移動請求,那么警告“<player> ?is sending move packets too frequently (<packets> packets since last tick)”,并且強制合并為一個包(也就是為什么移動會彈回的原因之一)
如果玩家不是在進行維度變更,或者鞘翅飛行速度檢查啟用時進行鞘翅飛行,如果速度過快(公式就不給了),將警告“<player> moved too quickly! <x>,<y>,<z>”,并強行傳送回之前的位置(彈回原因二)
在這之后,進行實際移動,MoverType為PLAYER
判斷玩家移動的正確性,不正確警告“<player> moved wrongly!”
處理摔落傷害等數(shù)據(jù)更新
通過這些,我們就能看出Player到底是怎么移動的了。
五.MC的重力加速度到底是多少
通過前文,我們基本上了解了移動是怎么發(fā)生的了,下面就來看看重力加速度到底是多少吧。
首先是LivingEntity里面的分析,抄一下之前的話:
獲取現(xiàn)在的移動速度(1895-1899行),進行Y軸速度的判斷,
如果重力存在,將Y軸速度向下加0.08(沒有緩降效果),
判定結(jié)果乘上0.98。
這樣,我們得出了一個有關(guān)于速度的以tick為自變量的函數(shù),記為F(x)

由于浮點數(shù)的誤差,這個值很快就會固定到3.92m/tick(實測是3.9200038147008747,因為有浮點數(shù)誤差),也就是78.4m/s,這也就是生物自由下落極限速度。

這時候,如果我們認為下落開始一瞬間不計空氣阻力,那么重力加速度就是x=0時的導數(shù)值
F'(0)=0.07919
也就是約為31.7m/s^2
下面是客戶端的驗證,這里用到的是fabric端,用了miniHUD的功能。

我們看到,隨著速度越來越接近78.4m/s,速度增長越慢,在78.4m/s徹底停止,也就認證了我們的理論。
然而這就結(jié)束了嗎?并沒有。非生物實體也有類似的公式,像下面這樣:
其中friction代表阻力,accel代表“每刻的加速度”。
由上面的公式,可以得到速度公式:
終端速度
具體的阻力和“加速度”依照下表,來自Minecraft Wiki“實體”頁面:

六.有關(guān)于粘液塊的事情
粘液塊是一個能將detlaMovement進行修改的方塊,在你站立于上方時,你本身會有一個向下的“趨勢位移”(在上一篇專欄有講),而粘液塊能修改它,導致你站在粘液塊上時deltaMovement會很歡快地跳動。具體可以看看驗證視頻。

專欄中使用的Minecraft反混淆+動態(tài)修改程序:MCDynamicExchanger,地址https://github.com/Nickid2018/MCDynamicExchanger
數(shù)學公式繪制:MathType? 函數(shù)繪制:幾何畫板
Minecraft:反混淆1.14.4端+動態(tài)修改程序
fabric端:模組是maLiLib,miniHUD,(tweakeroo,litematica,itemscroller,worldedit)
啟動器:HMCL 3.3.172
分配內(nèi)存:512MB
系統(tǒng):Windows 7 32位 運行內(nèi)存2GB
驗證視頻:

如果有任何問題或者文章中有錯誤,可以在評論區(qū)告訴我,我會修改掉錯誤的。