本期教程我們將探討網絡通信。網絡用於服務器與客戶端之間的數據同步。有時,我們無法直接從遊戲中獲取數據,比如上次提到的覆蓋層(overlay)。覆蓋層的參數沒有直接獲取遊戲內相關信息的接口。這些數據與遊戲邏輯無關,因為覆蓋層類是遊戲客戶端的屬性,數據存儲在服務器端,因此無法直接獲取。
上次我們想到了一個解決辦法,即使用靜態變量保存數據,在數據發生更改時更新。雖然我沒有詳細描述,但提到了每次數據更改時都要更新。這樣,我們可以直接使用靜態變量,但會引發一個問題:如果連接的是服務器,數據仍存儲在服務器上,而客戶端不運行邏輯,因此數據始終為零。這導致覆蓋層在服務器環境下始終顯示為零。要解決這個問題,需要通過網絡將服務器端數據同步到客戶端。
在此之前,我們還注意到 Parchment MC 和 New Forge 需要更新。Parchment MC 的地圖版本是 8 月 13 日版,New Forge 的版本是 47.1.70。我們可以順便更新一下,操作起來不會太麻煩。將地圖版本改為 8.13,將 Forge 版本改為 47.1.70,然後加載 GRADLE 進行更改,出現“build successful”提示後,更新完成。
更新完成後,我們來實現網絡通信。首先創建一個軟件包,然後創建一個 Java 類,命名為 Channel。稍後我們會繼續編寫該類。接著創建另一個軟件包,命名為 packet.paci,其中創建一個 Java 類,命名為 FarmXP,用於同步服務端數據到客戶端。
首先,在packet包下創建一個新的軟件包,命名為"server to player"。然後將 FarmXPPacket.java 文件拖拽到該包中。
現在,我們需要自己實現一些方法來處理這個packet。首先,我們需要定義 packet 中要使用的參數。由於我們只需要一個參數,即農場經驗,因此我們可以定義一個私有的 final int 變量,命名為 farmXP。
接下來,我們創建構造函數。這裡我們需要兩個方法,首先是解碼器。我們可以直接編寫一個構造函數,命名為 public FarmXPPacket,參數為一個 friendly bad Buff。這個 Buff 是 Minecraft 提供的一種專門用於網絡傳輸的工具,裡面包含了許多現成可用的方法。我們可以直接使用這些方法,例如,我們可以寫 farmXP = Buff.re() 來讀取數據。除了我們目前需要的 read 方法外,如果我們想傳輸更復雜的數據,Buff 中還有其他有用的方法,比如 readBlogPost 和 readNBT 等。但在這裡,我們只需使用 read 方法即可。
接下來,我們需要一個編碼器。我們編寫一個名為 public void encode 的方法,參數同樣是一個 friendly bad Buff,但這次我們需要將數據寫入 Buff 中。我們使用 Buff.right 方法來寫入 int 類型的 farmXP。
這兩個方法編寫完成後,我們還需要一個處理數據的方法。我們編寫一個名為 public void handle 的方法,參數為一個 supplier,其泛型為 network event 的 context。在這個方法中,我們可以對數據包進行處理。首先,我們可以通過 context.getPlayer() 來獲取玩家對象。然後,context 中還包含了一些其他有用的方法,比如 getPlayer()。但由於我們是從服務端發送數據包,因此服務端只有一個,所以我們不需要使用 getPlayer() 方法。
另外,還有一點需要注意的是,這個網絡系統是在另一個線程上運行的。它和MINECRAFT客戶端或邏輯並不在同一個線程上。是的,它是多線程的。因此,我們不能直接與遊戲的某些信息進行交互。但是,許多人都有這樣的需求。因此,我們提供了一個名為"in cool work"的解決方案。通過傳入一個RUNNABLE,在下一個遊戲主線程的時刻執行。
比如,"Player fx p provider"執行"點XPCLINT等於放XP",然後調用"context點set pack handled",最後直接返回true。因為在這裡,我們沒有複雜的邏輯,所以不需要判斷操作是否成功。如果這種簡單邏輯失敗的可能性較小。當然,如果你的邏輯可能出錯,就需要使用try-catch進行包裹,或者定義一些判斷條件。最後,如果失敗,就返回false。handle部分就完成了。
現在,我們需要在channel這一部分進行操作。我們可以看到文檔裡提供了許多關於network的內容。我們直接複製這些內容,並導入。對於"resource location",我們直接使用我們自己的mod地。因此,我們將其命名為"FMXP"。接下來,我們使用public static void register方法。我們需要註冊我們的頻道,所以我們必須對頻道進行初始化,包括註冊我們的packet。我們使用instance來註冊消息。
這裡有一些參數,比如Message、Encoder、Decoder、Consumer和network direction。但是,這種寫法可讀性較差,有時候會忘記參數的順序。因此,我們可以使用另一種方法,即使用message builder。使用message builder可以增強可讀性。首先,第一個參數是我們的firm xp packet的class。然後,我們在這裡定義一個id,讓其初始值為零。每次使用時,id都會自增,這樣就不會重複。
第三個參數是網絡方向,lay to clint。然後是decoder,即我們之前寫的構造函數。我們將其實例化為cos函數。編碼器是我們之前編寫的。最後,點consumer,這裡使用了之前講過的"my sweet network sweet"。還有一些自動生成的方法可供使用,比如method network three和farm x p take it handle。最後,點擊ADD即可完成註冊。
下面是我們自定義的方法。在這裡,我們繼續編寫public static void sendToPlayer方法。關於參數,我們可以看到這個instance,在這個方法中我們可以發送任何內容。因此,我們可以直接仿照這個方法編寫。這個方法中的message是在這個位置定義的,所以我們也在這裡定義一個message參數。接著我們需要另一個參數,即要發送給誰。
因此我們需要添加一個player參數。在這裡,我們可以根據文檔上的packet Distribute,點選player,然後將message發送給對應的玩家。對於farm xp overlay,我們可以在這裡添加player、FarmXPProvider,然後點選XPCLIENT,將文字改為farm xp:。回到我們的farmXP類,我們需要在XP發生更改時更新這個數值。我們可以找一個合適的時機,在這個時機同步這個數據。如果找不到我們的這個event listener,我們可以在使用XP的時候進行同步。因為在使用之後就會發生更改。
在這裡,我們可以編寫channel.sendToPlayer(newBMXPitXP, player),然後從event中獲取玩家並轉換成服務器玩家。當然,在這裡可能會遇到一些潛在的問題。同時,player clone也需要同步。我們可以直接複製這個內容。同樣地,在玩家加入服務器時也需要同步這個數據。可以使用PlayerLoginEvent,我們之前應該已經寫過了。
完成了這些工作之後,我們需要將其註冊進去。在FMLCommonSetupEvent中,調用channel.register方法註冊。完成這一系列步驟後,我們進行測試。由於這次測試涉及到網絡,所以我們需要運行一個服務器。服務器啟動完成後,我們運行lint,然後在多人遊戲中連接到本地主機。現在我們的XP是零。點擊右鍵後,程序崩潰了。但不用擔心,這是意料之中的。
在這一段中,我們要探討一個重要的知識點,即如何根據錯誤報告來識別問題。當我們在處理代碼時,有時會遇到類似於 "net.minecraft"、"clean"、"player"、"local there" 這樣的提示信息。這些信息表明在客戶端和服務端都存在一些差異。在客戶端,"player" 實際上是指本地玩家,而在服務端,它指的是服務器上的玩家。因此,有些代碼只能在服務端執行,而在客戶端則不應該執行。如果錯誤的代碼在客戶端運行,程序就會崩潰。我們可以通過一些方法來確定當前是在哪個端,比如使用 "level.isClientSide" 或者 "Dist.isClient" 這樣的方法。
一般來說,我們優先使用前兩種方法,而最後一種方法只是在前兩種方法無法使用時才考慮。在代碼中,我們可以這樣寫:如果事件發生在客戶端,那麼就不執行特定的代碼,只有在服務端才執行。同樣的邏輯也適用於其他情況。通過這樣的方式,我們可以確保代碼在不同端的正確執行。在本段的最後,我們還介紹瞭如何在獲取經驗時同步數據,以及如何在客戶端和服務端之間發送數據的方法。那麼本期教程就到此結束。