器到模塊開發(fā)實戰(zhàn))
寫模組到現(xiàn)在第三十九篇終于該碰真正的核心機制了。前面幾篇基本都在和Block、Item、模型這些靜態(tài)的東西打交道而這一篇要講的TileEntity本質(zhì)上就是讓方塊“活”過來的關(guān)鍵——它可以讓一個方塊擁有自己的數(shù)據(jù)、自己的狀態(tài)、自己的運行邏輯。我用“反應(yīng)器”這個名字來作為載體是因為這類“機器型方塊”幾乎包含了一個TileEntity能遇到的所有問題內(nèi)部要存物品輸入槽、輸出槽、燃料槽要有處理進度要和服務(wù)端邏輯配合還要把進度同步到客戶端讓玩家在界面上看到進度條在動。能把反應(yīng)器做出來就等于掌握了模組開發(fā)里最常用的一類方塊實體。這篇文章會從需求拆解開始把注冊、NBT存儲、服務(wù)端tick邏輯、數(shù)據(jù)同步、渲染方案全部過一遍最后一節(jié)是我實際踩過的一些坑照著做就能繞開。1. 先用需求倒推反應(yīng)器的核心難點其實只有三點1.1 為什么必須引入TileEntityBlockState搞不定的三件事先回答一個很多人剛接觸時都會問的問題做個反應(yīng)器直接用一個Block類不行嗎答案是不行至少做不出真正意義上的“反應(yīng)器”。原因有三條。第一是狀態(tài)存儲。BlockState在1.12.2里的本質(zhì)是方塊自身屬性的組合它存在世界里的時候只是一組Property和對應(yīng)的值沒法塞進去一個ItemStack數(shù)組。可反應(yīng)器內(nèi)部要存燃料、原料、產(chǎn)物這是典型的多檔位物品存儲只有TileEntity能在世界數(shù)據(jù)里單獨掛一塊NBT給它存這些內(nèi)容。第二是運行邏輯。BlockState沒有tick方法當(dāng)然1.12.2里可以給方塊設(shè)置setTickRandomly但那只能做非常稀疏的隨機刻用來跑“每200tick燒一次燃料”這種流程別說手感不對實際表現(xiàn)也會非常奇怪。真正穩(wěn)定的做法是讓TileEntity實現(xiàn)ITickable每個游戲刻都被服務(wù)端調(diào)用一次update。第三是數(shù)據(jù)同步。客戶端的模型要根據(jù)工作狀態(tài)變化GUI界面要顯示當(dāng)前進度這些都需要一個能被服務(wù)器主動推送數(shù)據(jù)、客戶端能讀取解析的實體BlockState只靠IBlockState里的Property做不了這么細致的數(shù)據(jù)流通。打個比方Block是“外表”TileEntity是“內(nèi)臟”。外表可以換顏色、換朝向但里面的血液、呼吸、心跳這些數(shù)據(jù)信息必須要內(nèi)臟來管理。1.2 反應(yīng)器的功能定義輸入槽、輸出槽、燃料與進度在動手寫代碼之前先把這臺“反應(yīng)器”到底做什么定義清楚。我把它設(shè)計成一個基礎(chǔ)冶煉機器有兩個輸入槽一個是燃料槽比如煤炭一個是原料槽比如鐵錠一個輸出槽。反應(yīng)器會消耗燃料將原料轉(zhuǎn)化為產(chǎn)物同時積累一個進度值??紤]到系列文章的內(nèi)容密度我決定這期不寫GUI只把TileEntity的核心邏輯、數(shù)據(jù)存儲、同步和模型狀態(tài)做完整。沒有GUI并不影響學(xué)習(xí)TileEntity因為TileEntity本身的數(shù)據(jù)鏈和交互鏈?zhǔn)仟毩⒂贕UI的。GUI在下一期再單獨展開那一篇會引入Container和Slot的完整體系。這里要用到的技術(shù)點包括Block的hasTileEntity與createNewTileEntity方法、TileEntity的readFromNBT與writeToNBT、ITickable接口、IInventory接口讓Reactable可以被管道、漏斗等外部系統(tǒng)理解、服務(wù)端到客戶端的同步通道。如果這些詞你現(xiàn)在看著還發(fā)懵問題不大接下來每一個我都會給出完整的代碼和解釋。2. 從Block到TileEntity的注冊鏈路少了哪一步都不行2.1 注冊TileEntityGameRegistry與createNewTileEntity的配合TileEntity不是一個獨立的注冊對象它必須依附在Block上。你在游戲里放置一個方塊游戲才會根據(jù)這個方塊對應(yīng)的TileEntity類在內(nèi)存中new一個實體出來再綁定到那個BlockPos坐標(biāo)上。在1.12.2 Forge里第一步是用GameRegistry.registerTileEntity注冊類與ID的映射關(guān)系。注意這里的ID是字符串要和TileEntity類綁在一起并且只在服務(wù)端注冊一次即可。通常會寫在Mod主類或者一個專門放注冊邏輯的類里我習(xí)慣放在一個RegistryHandler類中。public class RegistryHandler { public static void registerTileEntities() { GameRegistry.registerTileEntity(TileEntityReactor.class, reactor_te); } }一個很容易被忽略的細節(jié)注冊用的key不要用中文也不要太長。它會被寫進存檔如果你以后改了key舊存檔里的區(qū)塊加載時就會找不到對應(yīng)的TileEntity類直接崩檔或者丟數(shù)據(jù)。所以命名一定要穩(wěn)定類似reactor_te這種短字符串最穩(wěn)妥。第二步最關(guān)鍵在Block類里重寫兩個方法Override public boolean hasTileEntity(IBlockState state) { return true; } Override public TileEntity createNewTileEntity(World world, int metadata) { return new TileEntityReactor(); }這兩個方法缺一不可。hasTileEntity決定這個方塊在放置時要不要附帶創(chuàng)建實體createNewTileEntity就是創(chuàng)建的具體邏輯。很多新手只寫了hasTileEntity就迫不及待進游戲測試結(jié)果放置的方塊完全沒有數(shù)據(jù)存儲能力查了半天發(fā)現(xiàn)是createNewTileEntity沒實現(xiàn)。2.2 方塊注冊、模型綁定與掉落處理的連帶改動Block本身還是要正常注冊的和前面的基礎(chǔ)方塊一樣setRegistryName、setUnlocalizedName、加到創(chuàng)造模式物品欄、注冊ModelLoader資源。這些內(nèi)容在系列前面已經(jīng)寫過這里就不重復(fù)貼代碼了只強調(diào)幾個和TileEntity相關(guān)的連帶改動。第一如果你用了物品欄里的方塊物品要確保在客戶端注冊對應(yīng)的Item模型否則放置后模型可能變紫黑色。這屬于老生常談但在加了TileEntity之后容易忘。第二方塊被破壞時TileEntity里的物品不會自動掉出來。你必須在breakBlock方法里自己處理掉落邏輯否則玩家用鎬子挖掉反應(yīng)器里面的燃料和產(chǎn)物會直接蒸發(fā)。Override public void breakBlock(World world, BlockPos pos, IBlockState state) { TileEntity tileEntity world.getTileEntity(pos); if (tileEntity instanceof TileEntityReactor) { TileEntityReactor reactor (TileEntityReactor) tileEntity; for (int i 0; i reactor.getSizeInventory(); i) { ItemStack stack reactor.getStackInSlot(i); if (!stack.isEmpty()) { spawnAsEntity(world, pos, stack); } } } super.breakBlock(world, pos, state); }這里用的是Forge提供的spawnAsEntity方法它負(fù)責(zé)在指定位置生成一個物品實體。注意循環(huán)里要先判斷stack是否為空1.11以后ItemStack不允許出現(xiàn)Item為null的實例一律用ItemStack.EMPTY來表示空槽位判斷方法就是isEmpty()。還有一個偏門但很重要的方法叫eventReceived它和Block與TileEntity之間的客戶端事件通信有關(guān)。比如你想在客戶端播放音效、粒子效果時可以通過world.addBlockEvent觸發(fā)最終會調(diào)用到這個方法并轉(zhuǎn)發(fā)給TileEntity的receiveClientEvent。1.12.2里很多機器型方塊會重寫它來實現(xiàn)“敲一下發(fā)個信號”的交互反應(yīng)器這版先不涉及但你要知道有這個東西存在。3. 核心實現(xiàn)讓TileEntity自己動起來的完整代碼3.1 TileEntityReactor的字段設(shè)計與生命周期現(xiàn)在進入正題。TileEntityReactor需要繼承TileEntity然后實現(xiàn)ITickable和IInventory。ITickable提供update方法IInventory提供物品操作相關(guān)接口。字段設(shè)計上我習(xí)慣把所有需要持久化的數(shù)據(jù)放到一起方便寫NBTpublic class TileEntityReactor extends TileEntity implements ITickable, IInventory { private ItemStack[] inventory new ItemStack[2]; private int progress 0; private static final int MAX_PROGRESS 200; private ItemStack fuelStack; private ItemStack materialStack; public TileEntityReactor() { for (int i 0; i inventory.length; i) { inventory[i] ItemStack.EMPTY; } } }我這里故意用了一個獨立的ItemStack[]數(shù)組而不是直接用兩個單獨的字段。為什么因為IInventory接口要求按槽位索引訪問一個數(shù)組能讓你寫循環(huán)遍歷、判斷空槽位、實現(xiàn)自動注入邏輯時都方便很多后續(xù)做漏斗交互、管道連接時優(yōu)勢更明顯。數(shù)組長度為20號槽放燃料1號槽放原料。實際上你完全可以擴展成3個、4個甚至用List管理動態(tài)槽位。進度值progress是處理進度的核心數(shù)值每次update會增加達到MAX_PROGRESS時觸發(fā)一次產(chǎn)出。MAX_PROGRESS200意味著大概10秒完成一次處理1.12.2的tick是每秒20次。這是一個很“手感化”的參數(shù)需要根據(jù)你的機器定位調(diào)整。如果希望反應(yīng)器是一個前期工具200剛好如果希望它是中后期才能效率化使用的設(shè)備可以抬高到400甚至更高。TileEntity的生命周期里有三個時刻最值得注意放置時構(gòu)造、每個tick被update調(diào)用、被保存或加載時執(zhí)行readFromNBT/writeToNBT。構(gòu)造時把數(shù)組所有槽位初始化成ItemStack.EMPTY可以避免很多空指針問題這個習(xí)慣從第一天寫TileEntity就要養(yǎng)成。3.2 核心update與配方判斷服務(wù)端主循環(huán)的寫法update方法是整個TileEntity的心臟也是最容易寫出性能問題的地方。第一行永遠是判斷客戶端因為TileEntity的tick邏輯必須在服務(wù)端執(zhí)行客戶端只需要被動接收同步數(shù)據(jù)。如果客戶端也跑了這套邏輯就會出現(xiàn)兩邊數(shù)據(jù)不一致進度條抽風(fēng)甚至服務(wù)端把客戶端數(shù)據(jù)覆蓋掉的嚴(yán)重bug。Override public void update() { if (world.isRemote) { return; } boolean needSync false; if (canProcess()) { if (fuelStack.isEmpty() || fuelStack.getCount() 0) { // 沒燃料試著從燃料槽拿一組進來 if (!inventory[0].isEmpty()) { fuelStack inventory[0].splitStack(1); needSync true; } } if (!fuelStack.isEmpty() !materialStack.isEmpty()) { progress; if (progress MAX_PROGRESS) { processResult(); progress 0; needSync true; } } else { progress 0; } } else { progress 0; } if (needSync || progress 0) { markDirty(); } if (needSync) { world.notifyBlockUpdate(pos, world.getBlockState(pos), world.getBlockState(pos), 3); } }canProcess方法負(fù)責(zé)判斷當(dāng)前是否滿足處理條件private boolean canProcess() { return !inventory[0].isEmpty() !inventory[1].isEmpty(); }這里簡化了“什么能當(dāng)燃料、什么能當(dāng)原料”的判定。實際項目里我會建議單獨寫一個方法比如isFuel(ItemStack)來判斷物品是否屬于燃料列表而原料則對應(yīng)一套配方表。配方表可以用MapItemStack, ItemStack或者更工程化的Recipe對象來實現(xiàn)。本篇為了聚焦TileEntity本身先用“非空即可處理”的邏輯占位后續(xù)加配方系統(tǒng)時只需替換canProcess和processResult內(nèi)部實現(xiàn)其他部分完全不用動。processResult負(fù)責(zé)一個周期結(jié)束后真正“產(chǎn)出”什么東西private void processResult() { ItemStack result new ItemStack(Items.DIAMOND); // 示意實際應(yīng)該按配方產(chǎn)出 inventory[0].shrink(1); inventory[1].shrink(1); inventory[2] result.copy(); fuelStack inventory[0]; materialStack inventory[1]; }這里有一個編程細節(jié)inventory[0].shrink(1)會就地減少堆疊數(shù)量但1.12.2里ItemStack是不可變對象shrink會返回一個新的棧所以更穩(wěn)妥的寫法是把shrink的結(jié)果重新賦值給原槽位。很多老教程里的stackSize--在新版MCP映射下已經(jīng)不可用了。我實際寫的時候更喜歡這樣inventory[0] inventory[0].splitStack(inventory[0].getCount() - 1); if (inventory[0].getCount() 0) { inventory[0] ItemStack.EMPTY; }看起來啰嗦但它能保證任何情況下槽位都不會出現(xiàn)“只剩一個空ItemStack對象但還沒變成EMPTY”的隱患。3.3 IInventory接口讓漏斗和其他Mod認(rèn)識你的機器為什么一定要實現(xiàn)IInventory因為這等于給你的TileEntity定了一套標(biāo)準(zhǔn)通信協(xié)議。原版的漏斗、投擲器、其他模組的自動化管道都通過IInventory來讀取和寫入物品。你只要實現(xiàn)這個接口反應(yīng)器天然就能被原版漏斗自動供料這個效果會讓人非常有成就感。IInventory在1.12.2里有一堆方法除了必須返回數(shù)組長度、判斷空、獲取槽位物品之外還有decrStackSize、setInventorySlotContents、markDirty、isUsableByPlayer等。寫的時候我沒必要全貼但有兩個方法值得單獨說。第一個是isItemValidForSlot(int index, ItemStack stack)它決定哪些槽位可以接收哪些物品。我建議在0號槽只接收燃料1號槽接收原料。這樣漏斗嘗試自動輸入時不會把鐵錠塞到燃料管里。Override public boolean isItemValidForSlot(int index, ItemStack stack) { if (index 0) { return isFuel(stack); } else if (index 1) { return isMaterial(stack); } return false; }第二個是markDirty()。每次物品數(shù)量變化、進度變化、狀態(tài)變化時都要調(diào)用它作用是把TileEntity標(biāo)記為“臟”提示存檔系統(tǒng)在合適的時間保存數(shù)據(jù)。漏寫markDirty的典型癥狀是服務(wù)端邏輯正常但區(qū)塊保存后重啟游戲物品或進度回到舊狀態(tài)。IInventory接口里還有setField和getField這兩個在配合GUI的Container做進度條同步時特別有用。1.12.2的Container里IPC通過getField讀取數(shù)值、setField寫入數(shù)值從而實現(xiàn)客戶端和服務(wù)端的整數(shù)同步。我這一期的代碼里沒寫GUI但這兩個方法還是要實現(xiàn)留好擴展位下一期做GUI時直接就能用。4. 數(shù)據(jù)同步原理解決進度條不動、界面上數(shù)值不對4.1 三條同步通道的職責(zé)劃分加載、更新、渲染TileEntity的數(shù)據(jù)同步是新手最容易翻車的地方因為這里涉及三條完全不同的通道每一條都對應(yīng)不同的游戲階段。第一條是區(qū)塊加載同步。當(dāng)玩家走進一個已加載的區(qū)塊時客戶端需要知道這個區(qū)塊里所有TileEntity的完整狀態(tài)。每條通道都由一個核心方法負(fù)責(zé)getUpdateTag讀寫出用于同步的完整的NBT數(shù)據(jù)然后通過SPacketUpdateTileEntity發(fā)送。這條通道會調(diào)用getUpdatePacket和onDataPacket方法。第三條是區(qū)塊渲染同步。當(dāng)客戶端開始渲染一個區(qū)塊時它不會直接拉取服務(wù)端的所有數(shù)據(jù)而是依賴服務(wù)端在方塊狀態(tài)變化、TileEntity狀態(tài)變化時主動推送。實際開發(fā)中最常用到的就是第二條方塊更新同步。在服務(wù)端改變進度、改變工作狀態(tài)后調(diào)用world.notifyBlockUpdate就可以觸發(fā)一次客戶端更新。這個方法的第三個參數(shù)是flags推薦用3表示同時通知鄰居更新和自身更新。4.2 讓進度條動起來的標(biāo)準(zhǔn)做法三次強調(diào)nottifyBlockUpdate進度條動不起來或者整個工作方的模型不隨狀態(tài)切換改變通常都是同一個原因服務(wù)端改了數(shù)據(jù)但沒通知客戶端。我處理這類問題有個標(biāo)準(zhǔn)三步走。第一步服務(wù)端邏輯里修改關(guān)鍵字段后立即markDirty保證之后區(qū)塊保存不丟數(shù)據(jù)。第二步調(diào)用world.notifyBlockUpdate(pos, state, state, 3)主動通知客戶端這個坐標(biāo)的方塊狀態(tài)需要刷新。第三步在TileEntity里實現(xiàn)getUpdatePacket方法返回一個SPacketUpdateTileEntity同時在onDataPacket里把收到的NBT讀取回來。三件事配合才是一條完整的數(shù)據(jù)同步鏈路。Override public NBTTagCompound getUpdateTag() { return writeToNBT(new NBTTagCompound()); } Nullable Override public SPacketUpdateTileEntity getUpdatePacket() { return new SPacketUpdateTileEntity(pos, 1, getUpdateTag()); } Override public void onDataPacket(NetworkManager net, SPacketUpdateTileEntity pkt) { readFromNBT(pkt.getNbtCompound()); world.markBlockRangeForRenderUpdate(pos, pos); }很多新手只實現(xiàn)了getUpdateTag忘了getUpdatePacket。結(jié)果就是打開存檔時能讀到正確數(shù)據(jù)但游戲運行中客戶端拿不到變化進度條自然不動。記住getUpdateTag負(fù)責(zé)“首次加載時同步全量數(shù)據(jù)”getUpdatePacket負(fù)責(zé)“運行中主動推送更新”兩者都要有。onDataPacket里的最后一行也不可省略。readFromNBT只是把數(shù)據(jù)寫回TileEntity字段但客戶端渲染層并不知道數(shù)據(jù)變了。markBlockRangeForRenderUpdate會主動標(biāo)臟渲染區(qū)域讓客戶端重新構(gòu)建這一段的渲染狀態(tài)。沒有這行模型切換有時就會卡在舊狀態(tài)上玩家的體感就是“反應(yīng)器一直在工作但外觀一直顯示待機”。5. 渲染方案進度條與工作動畫怎么做才不卡5.1 方案一用getActualState做多態(tài)模型推薦1.12.2里做機器工作狀態(tài)的渲染最常用也最推薦的做法是讓TileEntity的狀態(tài)驅(qū)動IBlockState的Property。具體思路是在Block類里定義一個PropertyBool類型的working屬性重寫getActualState方法時查詢該坐標(biāo)的TileEntity運行狀態(tài)把working設(shè)置為true或false。public static final PropertyBool WORKING PropertyBool.create(working); Override public IBlockState getActualState(IBlockState state, IBlockAccess world, BlockPos pos) { TileEntity te world.getTileEntity(pos); boolean working false; if (te instanceof TileEntityReactor) { working ((TileEntityReactor) te).isWorking(); } return state.withProperty(WORKING, working); }isWorking方法可以從TileEntity內(nèi)部狀態(tài)來定義比如燃料槽和原料槽都不為空或者progress 0。我建議用progress 0這個判定因為它更真實地反映了“正在處理進度中”的語義。然后你需要給這個方塊準(zhǔn)備兩個JSON模型文件分別對應(yīng)workingtrue和workingfalse。通過blockstate JSON里的multipart規(guī)則把方塊在working不同取值下用不同模型渲染。這是很多“機器在運行”外觀效果的標(biāo)準(zhǔn)做法。這套方案的優(yōu)點是性能極好幾乎沒有任何運行時開銷客戶端拿到的只是兩個不同靜態(tài)模型。缺點是做不了連續(xù)動畫比如進度條長度平滑增長就無能為力。如果你的反應(yīng)器外觀想展示一個“進度條逐步填滿”的視覺效果那就得上方案二。5.2 方案二TileEntitySpecialRenderer做自定義渲染TESR的思路是完全繞過普通方塊模型由自己寫OpenGL調(diào)用邏輯來渲染TileEntity的形狀和動態(tài)效果。1.12.2里有TileEntitySpecialRenderer 這個抽象類你重寫render方法綁定自己加載的模型然后根據(jù)TileEntity的progress字段來計算旋轉(zhuǎn)角度、位移、亮度、粒子效果等。TESR能做真正的平滑動畫這是它比getActualState強的地方。但代價也很明顯每幀每個TileEntity都要調(diào)用你的render方法如果你不做優(yōu)化成千上萬個機器同時在工作會直接把幀率拉垮。如果你決定用TESR我的建議是優(yōu)先嘗試FastTESR。FastTESR會跳過一些常規(guī)的渲染狀態(tài)準(zhǔn)備性能更好代價是你不能使用大部分原版渲染輔助方法而且要自己管理OpenGL狀態(tài)。新手不推薦一上來就寫FastTESR先學(xué)會普通TESR理解了渲染流程后再優(yōu)化不遲。還有一個必坑點TESR必須獨立注冊不能依賴方塊注冊流程。1.12.2里用ClientRegistry.bindTileEntitySpecialRenderer(TileEntityReactor.class, new RenderReactor()); 放在客戶端初始化階段執(zhí)行即可。6. 排查記錄我在這套代碼上踩過的坑6.1 六個常見問題速查表這幾個問題基本上每個做TileEntity的新手都會遇上我把它們整理出來遇到相同現(xiàn)象直接對著查。問題現(xiàn)象常見原因解決方案放入燃料和原料后機器完全不動update方法里沒寫world.isRemote判斷在update第一行return掉客戶端分支重啟游戲后進度和物品丟失槽位數(shù)組沒寫入NBT或沒調(diào)用markDirty檢查readFromNBT/writeToNBT確保每個槽位都序列化客戶端進度條數(shù)值永遠是0沒實現(xiàn)getUpdatePacket補上getUpdatePacket和onDataPacket玩家挖掉方塊內(nèi)部物品消失沒重寫breakBlock處理掉落遍歷IInventory調(diào)用spawnAsEntity模型不切換工作態(tài)看不出來getActualState的working屬性沒監(jiān)聽TileEntity狀態(tài)在getActualState里查詢shape并確保update里調(diào)用了notifyBlockUpdate漏斗能輸出但不能輸入isItemValidForSlot判斷過嚴(yán)檢查槽位索引和物品判斷邏輯是否合理6.2 幾個讓代碼更穩(wěn)的長期習(xí)慣做TileEntity開發(fā)時間久了我總結(jié)出幾條保命經(jīng)驗。第一所有持久化字段集中管理不要散落在類的各個角落。我見過很多項目把進度值、燃料時間、配方ID都寫成獨立字段寫NBT時漏一個補一個最后維護成本高到爆炸。統(tǒng)一放到一個區(qū)域每次改字段都順手檢查NBT的讀寫是否同步更新。第二永遠不要在update里直接new ItemStack。游戲每秒鐘會調(diào)用20次update如果你的處理邏輯里每tick都構(gòu)造新對象GC壓力會很明顯。最好在配方判定階段緩存結(jié)果或者用同一組ItemStack復(fù)用。原版的熔爐實現(xiàn)里有很多細節(jié)可以學(xué)習(xí)它用一個私有數(shù)組緩存當(dāng)前燒煉結(jié)果這是很成熟的設(shè)計思路。第三明確區(qū)分“服務(wù)端數(shù)據(jù)”和“客戶端數(shù)據(jù)”。progress這個字段在客戶端的存在意義只是為了顯示和渲染不應(yīng)該參與真正的邏輯判斷。如果你在canProcess里用了客戶端同步過來的progress做判定那服務(wù)端也會跟著出問題。唯一的原則就是業(yè)務(wù)邏輯要模擬在服務(wù)端的常用經(jīng)驗就是給update方法加上一個“是否客戶端”的判斷以后所有的邏輯判斷都用服務(wù)端獨立計算的字段客戶端只讀現(xiàn)成結(jié)果。第四方塊更新通知不能濫用。如果你每個tick都調(diào)world.notifyBlockUpdate那任何在附近觀察的玩家都會收到大量網(wǎng)絡(luò)包人多的時候服務(wù)器會卡。我建議只在幾個關(guān)鍵節(jié)點觸發(fā)同步開始處理時、處理完成時、燃料或原料耗盡時。中途的progress增長不需要實時同步等到狀態(tài)變化時一次性同步即可。很多機器模組之所以卡就是因為它們把同步頻率設(shè)成了每個tick一次。7. 擴展方向從一臺反應(yīng)器到一個完整的機器體系當(dāng)你把反應(yīng)器的TileEntity邏輯跑通以后后續(xù)的擴展就只是量變到質(zhì)變的問題了。最自然的幾個方向我在文章里也提過第一加入GUI和Container讓玩家能手動放置和取出物品第二加入真正的配方系統(tǒng)用Map或者JSON配置文件來定義“什么燃料燒什么材料”再和合成表對應(yīng)起來第三加入紅石控制比如用IBlockState里的powered屬性讓反應(yīng)器在收到紅石信號時才工作第四加入多方塊結(jié)構(gòu)讓反應(yīng)器需要一個外殼或配套方塊才能運行這就接近一些大型模組的機器體系了。我個人的建議是先把無GUI版的完整同步鏈路徹底吃透再一步步加上這些殼子。TileEntity的核心難點永遠不在表面花活而在數(shù)據(jù)、邏輯、同步這三者的協(xié)調(diào)。反應(yīng)器這期把這三件事都覆蓋到了下一期加GUI時你會發(fā)現(xiàn)前面積累的這些經(jīng)驗全部用得上。