三年前開始開發全鏈遊戲時,開發體驗糟糕,因此創建了MUD引擎來解決問題。逐步改善開發流程,實現目前儲存架構。考慮到客戶端同步大量資料狀態,提出實現狀態變更自動同步、避免自訂getter、event和reducer的願望。透過實現泛型事件、自訂函式庫來解決這些問題,最終達到目標。透過壓縮儲存位元組、編碼方式實現狀態讀取和解碼。雖然Solidity不支援通用返回類型,但透過程式碼產生成功實現理想的開發體驗。 MUD引擎已通過審核,穩定版2.0.0即將發布。
三年前,當我們開始開發全鏈遊戲時,開發體驗還非常糟糕。因此,我們有了MUD引擎,以減少開發過程中遇到的各種問題。
接下來,我將詳細介紹我們是如何在開發過程中逐步完善,並最終實現了目前的儲存架構。
回看2021年(即MUD成立的前一年),我們按照當時的「最佳實踐」在自訂的結構體(custom struct)、映射(mapping)和備份(array)中保存鏈上狀態,透過自訂的檢視函數(視圖函數)來取得狀態,對每個狀態變更編寫自訂的事件(自訂事件),並在客戶端和索引器上實作自訂事件處理的reducer函數。
這種方法對於小規模的鏈上應用是可行的,但是全鏈遊戲需要客戶端同步大量的資料狀態。很快,我們發現自己大部分時間都在處理資料模型的變更,並透過他們自訂網路堆疊來實現這些變化,而不是一股能量中心化在遊戲機制的開發上。
當開發過程中遇到困難和挑戰變得難以忍受時,我們決定暫停體驗,應該重新思考理想中的開發所關注的。
我們唯一的願望是在智能合約上設定一個狀態變量,並且能夠自動與客戶端同步,不用自訂getter、event和reducer,只需簡單地讀取狀態。
解決方案的第一步是實作一個泛型事件(generic event),他能夠在每次狀態改變時被觸發,使得索引器或用戶端能夠自動同步鏈上的狀態。
但問題是,在Solidity中並沒有現成的「泛型事件」。不過,我們還是找到了替代方案。
本質上,類型(Type)不過是對位元組(Byte)的分層封裝。因此,我們透過使用原始位元組(raw byte)作為事件數據,實現了一種能夠覆蓋所有狀態變更的通用事件機制。
接下來,我們需要一個能夠在每次狀態變更時觸發事件的泛型函式庫(通用函式庫),從而避免自訂setter函數的需求。
Solidity並沒有提供這樣的泛型函式庫,但我們採用了類似的策略來實現這一目標。我們沒有使用泛型類型(generic type),而是雖然採用了所有類型(type)的共同基礎——字節,作為函數簽署的參數。
這帶來了一個新的挑戰:如何將各種類型的資料轉換成字節,再傳遞給這個函式庫呢?最直接的方法是使用Solidity內建的abi.encode函式。然而,因為到處都添加填充而不一致用於編碼後的值。
一個更好的選擇是使用abi.encodePacked 函數,它能夠緊湊地壓縮數據,避免了重建填充。不過這個方法不能填入陣列(array)類型。
為此,我們不得不在Solidity中自行實作陣列的緊湊編碼方法。這種方法類似提案中的abi.encodeTightlyPacked(https://github.com/ethereum/solidity/issues/8441…)。
更進一步,我們如何實現一個Solidity 函數,能夠接受任何類型的叢集並返回其緊湊備份的位元組形式?我們首先為所有基本類型群集的共同基礎—bytes32[] 實作資源邏輯。
然後,我們為Solidity支援的98種基本型別備份(如uint8)[]uint16[]…, 位元組32[])各增加了一個特定的處理邏輯。這樣,我們便擁有了一個能夠接受任意基本型別倉庫並傳回其壓縮儲存位元組的函數。
我們越來越接近目標了。
最後的挑戰是如何從中儲存讀取並解碼這些值。我們需要一個類似abi.decode 的函數,但是要適用於我們自訂能夠的壓縮編碼方式:一個根據給定的編碼位元組和一個「模式(schema)」傳回解碼值及其原始類型的函數。
由於Solidity不支援通用返回類型,我們也無法像以前一樣將其轉換為通用類型。因此,我們轉而採用了程式碼生成的方式。您只需要在一個設定檔中定義您的資料結構,MUD就可以為您產生具有類型資訊的讀寫庫。
至此,我們的目標就達成了
消耗任何自訂的getter、event和reducer。只需簡單地進行讀寫操作。每次狀態的寫入操作都會觸發一個事件,該事件用於自動將鏈上狀態同步到索引器和用戶端。
雖然Solidity本身不支援類型類型,但透過一些泛化的解決技巧,我們仍然能實現理想中的開發體驗。
我們投入大量時間開發MUD引擎,旨在提升建構可擴展的全鏈上應用的開發體驗。目前,MUD已通過OpenZeppelin的全面審核,穩定版2.0.0即將發布。
資訊來源:0x資訊編譯自網際網路。版權歸作者alvarius所有,未經許可,不得轉載