【真正的和平模式】二、任務(wù)系統(tǒng)的實現(xiàn)

我像迷途小鹿 得不到救贖
才會在此后的路 忽視了所有景物
我眼里的天空變得荒蕪
連詩里的飛鳥也迷了路
我像迷途小鹿 得不到救贖
才會將所有心跡 毫無保留呈現(xiàn)出
你縱身躍進(jìn)了滿天大霧
我找不到你也忘了歸途
——《迷途小鹿》(歌手:葛雨晴,作詞:巒無眠)
偶然間聽到的歌曲,主題稍微契合就寫進(jìn)來了(逃
本文首發(fā)于知乎,筆者是作者本人,第二轉(zhuǎn)載到bilibili平臺。原文鏈接:https://zhuanlan.zhihu.com/p/644603030

背景
正如前文所述,我們的模組要實現(xiàn)一個任務(wù)系統(tǒng),記錄著每個玩家完成任務(wù)的進(jìn)度,以及觸發(fā)任務(wù)條件等等。
為了方便數(shù)據(jù)包作者和其它模組作者的擴(kuò)展,我決定使用數(shù)據(jù)包形式,單向鏈表結(jié)構(gòu)來添加任務(wù)。第一步就是確定數(shù)據(jù)格式——在這里我選擇使用如下格式:
沒錯,任務(wù)強制只在開始和結(jié)束時才觸發(fā)對話,而對話的目標(biāo)最多只有一位NPC(當(dāng)然也可以是玩家獨白)。這是這個簡易系統(tǒng)唯一的局限性。
至于觸發(fā),后面再講。這里提前簡要說一下,對于任務(wù)開始和結(jié)束,原生模組提供了兩種任務(wù)觸發(fā)方式:其一是summon_block,數(shù)據(jù)包作者們可以直接使用;其二是api.MissionHelper#triggerMissionForPlayers和api.MissionHelper#triggerMissionForPlayer,只有衍生和聯(lián)動模組開發(fā)者們可以使用。
接下來就要想如何實現(xiàn)了。之前做獨立游戲的時候?qū)崿F(xiàn)過任務(wù)系統(tǒng),不過跟Minecraft的情況相去較遠(yuǎn),至少沒辦法直接搬。所以我直接重新造一個輪子。
不過做模組,很重要的一點就是,想想原版有什么類似的功能,那么只需要輕松照抄,稍加修改即可。
我最先想到的則是原版的進(jìn)度系統(tǒng)——數(shù)據(jù)包作者們可以在data/<modid>/advancements中自由添加進(jìn)度。那就容易很多了,說干就干!
ServerAdvancementManager詳解
打開net.minecraft.server.ServerAdvancementManager文件,我們可以看到原版進(jìn)度系統(tǒng)的實現(xiàn):
不太重要的部分已經(jīng)略去,對于這部分我們逐一解讀。
1. MissionManager的實現(xiàn)
首先它繼承了SimpleJsonResourceReloadListener類,這個類原版有兩種Manager繼承了它,其一是進(jìn)度系統(tǒng),其二是合成系統(tǒng);而Forge也定義了LootModifierManager,用以實現(xiàn)戰(zhàn)利品表的更改。這個父類的功能很簡單,可以實現(xiàn)json格式的數(shù)據(jù)讀取和自動加載,只需重寫apply函數(shù)即可。
也許有寫過低版本模組的同仁們就要問了,戰(zhàn)利品表系統(tǒng)不也繼承了它嗎?不錯,曾經(jīng)是,不過1.20這部分被大幅修改了,如今LootDataManager僅僅是實現(xiàn)了SimpleJsonResourceReloadListener的爺爺接口PreparableReloadListener。
回歸正題,apply函數(shù)傳了三個參數(shù),分別是所有JSON文件內(nèi)容(按id索引在map中)、Resource Manager和Profiler Filler。事實上我們實現(xiàn)自己的需求也無需后兩個參數(shù),只要寫好讀取json文件的處理邏輯即可。
其次,構(gòu)造函數(shù)傳遞了兩個參數(shù),一個是編碼JSON文件的方法,一個是掃描文件目錄。對于進(jìn)度系統(tǒng)則是"advancements",如果我希望任務(wù)系統(tǒng)的掃描目錄是data/<modid>/rpm/missions,則傳入"rpm/missions"即可。
于是我們便可以實現(xiàn)任務(wù)系統(tǒng):
然后,如何將這個監(jiān)聽器真正監(jiān)聽在資源加載階段呢?當(dāng)然你可以mixin,但Forge是有這個API的,所以我優(yōu)先去調(diào)用這個API:
并在主類中,通過Forge bus中注冊它。
接著,我們有了總領(lǐng)的任務(wù)系統(tǒng),進(jìn)一步的,如何去維護(hù)每個人的任務(wù)的進(jìn)度?于是我們發(fā)現(xiàn)了PlayerAdvancements類。
2. PlayerMissions的實現(xiàn)
首先,我們可以看到在PlayerList中有一個維護(hù)每個人進(jìn)度完成情況的成員:
而在ServerPlayer中也有自身獨立的PlayerAdvancements:
當(dāng)然,這個advancements只是PlayerList中對應(yīng)的那個PlayerAdvancements的一個影子。
參考這個類,我們可以實現(xiàn)自己的PlayerMissions。它需要包含玩家完成過的任務(wù)、玩家正在進(jìn)行的任務(wù)(其余都是還未接收的任務(wù)):
為了安全性,在這里做了檢查,允許玩家接收的任務(wù),玩家必須已經(jīng)完成過所有前置任務(wù)。
那么,如何將它加進(jìn)PlayerList里呢?其實未必要加進(jìn)PlayerList中,你也可以自己寫一個SavedData來實現(xiàn),不過這次的mixin沒有副作用,而且更符合直覺架構(gòu),因此我選擇了mixin:
這里抽象了一個IPlayerListWithMissions接口,作用是,由于Mixin類無法被實例化或強制轉(zhuǎn)化,所以要想調(diào)用getPlayerMissions函數(shù),必須通過一個接口來訪問。比如:
沒辦法,都mixin了,還在意啥代碼美觀。
那么如何將它進(jìn)行序列化呢?我們又要mixin進(jìn)ServerPlayer類,注入讀寫nbt和restoreFrom方法:
這里的IMonsterHero接口也是同樣,方便其它部分調(diào)用,判斷玩家是否已經(jīng)實現(xiàn)了某個怪物的全部委托。
一定不要忘記restoreFrom函數(shù)!否則玩家不論是從末地返回主世界,還是死亡后重生,這些信息都會消失!
那么任務(wù)系統(tǒng)算是成功實現(xiàn)了,不過還需要客戶端的UI,顯示任務(wù)對話,如題圖所示。該怎么實現(xiàn)呢?
Menu+Screen的兩層架構(gòu)
首先介紹一下Minecraft的UI架構(gòu)。一般的功能性UI都是兩層結(jié)構(gòu):第一層是Menu,位于服務(wù)端(客戶端會同步它),便于與世界交互,如玩家放入熔爐一根烈焰棒(真有人這么富嗎?);第二層是Screen,位于客戶端,執(zhí)行顯示界面,處理玩家請求的功能,如顯示熔爐UI,顯示燃料剩余量、燒煉的進(jìn)度等等。有些UI由于無需與服務(wù)端部分交互,便只有Screen沒有Menu,比如玩家進(jìn)度、統(tǒng)計界面等,只需一次發(fā)包后便可顯示。
而我們的需求是,首先,任務(wù)界面打開過程中,怪物不能攻擊玩家——這就限制了我們的實現(xiàn),Menu部分必須要存在;其次,玩家客戶端要顯示對話,這部分是由服務(wù)端的MissionManager.Mission發(fā)包過來的;最后,對話結(jié)束后要提示玩家接收到了新的任務(wù)或完成了任務(wù),這又是服務(wù)端向客戶端發(fā)送的。
那么我們可以做如下設(shè)計:
Menu部分維護(hù)了該任務(wù)的所有對話。
Screen顯示了對話內(nèi)容和講話的生物,玩家可以按下按鈕來向前/向后閱讀。
Menu的實現(xiàn)
于是我們可以實現(xiàn)如下Menu:
第一個構(gòu)造函數(shù)用于RPMMenuTypes中的注冊:
第二個構(gòu)造函數(shù)則是用于在接收/完成任務(wù)時玩家openMenu的傳參。
由于無需物品欄和槽位的操作,quickMoveStack可直接返回空;stillValid也可以隨便寫寫了,這里是根據(jù)與NPC的距離判斷的。
mission的getter和setter則是用于服務(wù)端向客戶端的發(fā)包:
這一步將服務(wù)端的任務(wù)內(nèi)容傳給了客戶端,并進(jìn)行了驗證。
注冊發(fā)包則是在主類中完成:
Screen的實現(xiàn)
既然Menu實現(xiàn)好了,那Screen不就簡單了嗎?
注意高版本的PoseStack被UI系統(tǒng)棄用了,改用GuiGraphics做渲染,個人感覺更加方便了。
renderBg函數(shù)實現(xiàn)了背景渲染,以及界面右下方對話實體的顯示。renderButtons顯示了向前向后兩個按鈕的渲染,而處理玩家請求則是在mouseClicked(按下按鈕,改變按鈕顏色)和mouseReleased(釋放按鈕,執(zhí)行按鈕功能)中實現(xiàn)。而對話文本的分行是在loadCachedText函數(shù)中實現(xiàn)。
具體blit的數(shù)值取決于GUI資源圖片的排版,由于我將按鈕元素排版在下方,所以便從下方截取圖像并貼在對應(yīng)位置(詳見倉庫中GUI資源文件)。
實現(xiàn)了核心功能,也許玩家會想查看自己的任務(wù)完成情況和任務(wù)描述。接下來介紹顯示客戶端玩家任務(wù)這部分的實現(xiàn)方法。
顯示玩家任務(wù)完成情況
首先我添加了綁定按鍵(默認(rèn)M鍵),按下按鍵后可顯示任務(wù)屏幕GUI。
客戶端也要監(jiān)聽玩家按鍵的事件,處理打開窗口的請求:
這里這里通過客戶端和服務(wù)端之間互相發(fā)包的方式,來獲取玩家任務(wù)列表(包括已完成和進(jìn)行中):
發(fā)包注冊方式和前文相似:
顯示任務(wù)列表的Screen無需與世界交互,所以只實現(xiàn)一個Screen即可,無需Menu:
這里UI右上方有一個按鈕,用來決定客戶端玩家是否查看自己已完成的任務(wù)。這是唯一需要額外接收請求的部分。對于已完成與否的任務(wù),要顯示不同的提示,由于比較簡單,不做額外講解了。
總結(jié)
這樣,我們成功地把整個任務(wù)與對話系統(tǒng)搬到了Minecraft中,而且支持?jǐn)?shù)據(jù)包作者和拓展模組開發(fā)者們添加任務(wù),可以說實現(xiàn)地非常完美了。下一部分打算講輕松點的,主要是模組世界生成中添加結(jié)構(gòu)的方法。