MUD inició como un motor para crear juegos 100% on-chain pero ahora es mucho más que eso. Además de crear juegos interactivos con mecánicas de smart contracts, MUD es una herramienta capáz de crear mundos autónomos. ¿Qué es un mundo autónomo? Esto vá más allá de las finanzas y los juegos, en estos mundos puedes crear simulaciones donde la inteligencia artificial son ciudadanos de primera categoría. Si esto suena bastante loco, es porque lo es.
Para conocer más sobre MUD, comenzemos creando juego, donde los personajes collecionen monedas. En esta guía haremos todo desde cero. Crearemos los contratos, la estructura del proyecto y las animaciones.
Crea un nuevo proyecto de MUD
Estaremos usando Node version 20, pnpm y foundry. Si no los tienes instalados aquí te dejo los comandos.
Instalación de dependencias
curl-o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash nvminstall20curl-L https://foundry.paradigm.xyz | bashexportPATH=$PATH:~/.foundry/binsudonpminstall-g pnpm
Una vez instaladas las dependencias puedes crear un proyecto nuevo de MUD.
pnpm create mud@latest tutorialcdtutorial
Durante este proceso seleccionamos phaser como plantilla.
1. El Esquema del Estado
Este es el archivo más importante de MUD, es el que define la estructura de datos del estado de tus contratos. En MUD, no declaras variables de estado tipomapping
,uint
,bool
, etc.. ni tampoco losenum
. En vez las defines el archivo a continuación.
packages/contracts/mud.config.ts
import{defineWorld}from"@latticexyz/world";exportdefaultdefineWorld({namespace:"app",enums:{Direction:["Up","Down","Left","Right"]},tables:{PlayerPosition:{schema:{player:"address",x:"int32",y:"int32",},key:["player"]},CoinPosition:{schema:{x:"int32",y:"int32",exists:"bool",},key:["x","y"]},PlayerCoins:{schema:{player:"address",amount:"uint32",},key:["player"]}}});
2. La Lógica en Solidity
La lógica de las funciones en MUD es igual que en un proyecto normal. Declara tus funciones tipoview
,payable
,pure
con modifiers y todo lo demás tal y como estás acostumbrado.
Así que borra la lógica que viene por defecto y crea la lógica del juego que valida cuando un jugador se mueve y colecta monedas.
Borrapackages/contracts/src/systems/IncrementSystem.sol
que viene por default. Esto solo ocupas hacerlo si usas las templatesvanilla
,react-ecs
yphaser
que ofrece Mud.
rm packages/contracts/src/systems/IncrementSystem.sol packages/contracts/test/CounterTest.t.sol
packages/contracts/src/systems/MyGameSystem.sol
// SPDX-License-Identifier: MITpragmasolidity>=0.8.24;import{System}from"@latticexyz/world/src/System.sol";import{PlayerPosition,PlayerPositionData,CoinPosition,PlayerCoins}from"../codegen/index.sol";import{Direction}from"../codegen/common.sol";import{getKeysWithValue}from"@latticexyz/world-modules/src/modules/keyswithvalue/getKeysWithValue.sol";import{EncodedLengths,EncodedLengthsLib}from"@latticexyz/store/src/EncodedLengths.sol";contractMyGameSystemisSystem{functiongenerateCoins()public{CoinPosition.set(1,1,true);CoinPosition.set(2,2,true);CoinPosition.set(2,3,true);}functionspawn(int32x,int32y)public{addressplayer=_msgSender();PlayerPosition.set(player,x,y);}functionmove(Directiondirection)public{addressplayer=_msgSender();PlayerPositionDatamemoryplayerPosition=PlayerPosition.get(player);int32x=playerPosition.x;int32y=playerPosition.y;if(direction==Direction.Up)y-=1;if(direction==Direction.Down)y+=1;if(direction==Direction.Left)x-=1;if(direction==Direction.Right)x+=1;PlayerPosition.set(player,x,y);if(CoinPosition.getExists(x,y)){CoinPosition.set(x,y,false);PlayerCoins.set(player,PlayerCoins.getAmount(player)+1);}}}
En realidad no todas las funciones operan igual que en un proyecto de Solidity vainilla. Si lo intentas notarás que el constructor no funciona como esperado. Esto es porque losSystem
s están diseñados para operar sin un estado específico. Es decir, un mismoSystem
puede operar con múltiplesWorld
s que son quienes se encargan de manejar el estado. Es por esto que colocamos toda la lógica de inicalización en el contrato de post deploy.
En nuestro caso, colocamos ciertas monedas en el mapa.
packages/contracts/script/PostDeploy.s.sol
// SPDX-License-Identifier: MITpragmasolidity>=0.8.24;import{Script}from"forge-std/Script.sol";import{console}from"forge-std/console.sol";import{StoreSwitch}from"@latticexyz/store/src/StoreSwitch.sol";import{IWorld}from"../src/codegen/world/IWorld.sol";contractPostDeployisScript{functionrun(addressworldAddress)external{StoreSwitch.setStoreAddress(worldAddress);uint256deployerPrivateKey=vm.envUint("PRIVATE_KEY");vm.startBroadcast(deployerPrivateKey);IWorld(worldAddress).app__generateCoins();vm.stopBroadcast();}}
3. Interacción con el cliente
El cliente contiene toda lógica que no está on-chain. En este definimos lo que se muestra en la pantalla y tambén qué es lo que ocurre cuando el usuario hace click o presiona una tecla.
packages/client/src/layers/phaser/systems/myGameSystem.ts
import{Has,defineEnterSystem,defineSystem,defineExitSystem,getComponentValueStrict}from"@latticexyz/recs";import{PhaserLayer}from"../createPhaserLayer";import{pixelCoordToTileCoord,tileCoordToPixelCoord}from"@latticexyz/phaserx";import{TILE_WIDTH,TILE_HEIGHT,Animations,Directions}from"../constants";functiondecodeHexString(hexString:string):[number,number]{constcleanHex=hexString.slice(2);constfirstHalf=cleanHex.slice(0,cleanHex.length/2);constsecondHalf=cleanHex.slice(cleanHex.length/2);return[parseInt(firstHalf,16),parseInt(secondHalf,16)];}exportconstcreateMyGameSystem=(layer:PhaserLayer)=>{const{world,networkLayer:{components:{PlayerPosition,CoinPosition},systemCalls:{spawn,move,generateCoins}},scenes:{Main:{objectPool,input}}}=layer;input.pointerdown$.subscribe((event)=>{constx=event.pointer.worldX;consty=event.pointer.worldY;constplayerPosition=pixelCoordToTileCoord({x,y},TILE_WIDTH,TILE_HEIGHT);if(playerPosition.x==0&&playerPosition.y==0)return;spawn(playerPosition.x,playerPosition.y)});input.onKeyPress((keys)=>keys.has("W"),()=>{move(Directions.UP);});input.onKeyPress((keys)=>keys.has("S"),()=>{move(Directions.DOWN);});input.onKeyPress((keys)=>keys.has("A"),()=>{move(Directions.LEFT);});input.onKeyPress((keys)=>keys.has("D"),()=>{move(Directions.RIGHT);});input.onKeyPress((keys)=>keys.has("I"),()=>{generateCoins();});defineEnterSystem(world,[Has(PlayerPosition)],({entity})=>{constplayerObj=objectPool.get(entity,"Sprite");playerObj.setComponent({id:'animation',once:(sprite)=>{sprite.play(Animations.Player);}})});defineEnterSystem(world,[Has(CoinPosition)],({entity})=>{constcoinObj=objectPool.get(entity,"Sprite");coinObj.setComponent({id:'animation',once:(sprite)=>{sprite.play(Animations.Coin);}})});defineSystem(world,[Has(PlayerPosition)],({entity})=>{constplayerPosition=getComponentValueStrict(PlayerPosition,entity);constpixelPosition=tileCoordToPixelCoord(playerPosition,TILE_WIDTH,TILE_HEIGHT);constplayerObj=objectPool.get(entity,"Sprite");playerObj.setComponent({id:"position",once:(sprite)=>{sprite.setPosition(pixelPosition.x,pixelPosition.y);}})})defineSystem(world,[Has(CoinPosition)],({entity})=>{const[coinX,coinY]=decodeHexString(entity);constcoinExists=getComponentValueStrict(CoinPosition,entity).exists;constpixelPosition=tileCoordToPixelCoord({x:coinX,y:coinY},TILE_WIDTH,TILE_HEIGHT);constcoinObj=objectPool.get(entity,"Sprite");if(coinExists){coinObj.setComponent({id:"position",once:(sprite)=>{sprite.setPosition(pixelPosition.x,pixelPosition.y);}})}else{objectPool.remove(entity);}})};
4. Agrega imágenes y animaciones
Si deseas agregar un objeto animado, colócalo en su carpeta propia enpackages/art/sprites/
. Con un los archivos de cada cuadro de animación separado con nombre secuencial:1.png
,2.png
3.png
, etc..
En nuestro caso agregaremos 2 imágenes para nuestro personaje y una para las monedas.
Una vez hecho eso ejecuta los siguientes comandos que automatizan el el empaquetado de tus archivos en formato de spritesheets.
cdpackages/artyarnyarn generate-multiatlas-sprites
En el archivo de phaser podrás configurar la velocidad y el nombre de las animaciones. Aquí también puedes definir atributos que puedes usar en tu sistema.
packages/client/src/layers/phaser/configurePhaser.ts
importPhaserfrom"phaser";import{defineSceneConfig,AssetType,defineScaleConfig,defineMapConfig,defineCameraConfig,}from"@latticexyz/phaserx";importworldTilesetfrom"../../../public/assets/tilesets/world.png";import{TileAnimations,Tileset}from"../../artTypes/world";import{Assets,Maps,Scenes,TILE_HEIGHT,TILE_WIDTH,Animations}from"./constants";constANIMATION_INTERVAL=200;constmainMap=defineMapConfig({chunkSize:TILE_WIDTH*64,// tile size * tile amounttileWidth:TILE_WIDTH,tileHeight:TILE_HEIGHT,backgroundTile:[Tileset.Grass],animationInterval:ANIMATION_INTERVAL,tileAnimations:TileAnimations,layers:{layers:{Background:{tilesets:["Default"]},Foreground:{tilesets:["Default"]},},defaultLayer:"Background",},});exportconstphaserConfig={sceneConfig:{[Scenes.Main]:defineSceneConfig({assets:{[Assets.Tileset]:{type:AssetType.Image,key:Assets.Tileset,path:worldTileset,},[Assets.MainAtlas]:{type:AssetType.MultiAtlas,key:Assets.MainAtlas,// Add a timestamp to the end of the path to prevent cachingpath:`/assets/atlases/atlas.json?timestamp=${Date.now()}`,options:{imagePath:"/assets/atlases/",},},},maps:{[Maps.Main]:mainMap,},sprites:{},animations:[{key:Animations.Player,assetKey:Assets.MainAtlas,startFrame:1,endFrame:2,frameRate:3,repeat:-1,prefix:"sprites/player/",suffix:".png",},{key:Animations.Coin,assetKey:Assets.MainAtlas,startFrame:1,endFrame:1,frameRate:12,repeat:-1,prefix:"sprites/coin/",suffix:".png",},],tilesets:{Default:{assetKey:Assets.Tileset,tileWidth:TILE_WIDTH,tileHeight:TILE_HEIGHT,},},}),},scale:defineScaleConfig({parent:"phaser-game",zoom:1,mode:Phaser.Scale.NONE,}),cameraConfig:defineCameraConfig({pinchSpeed:1,wheelSpeed:1,maxZoom:3,minZoom:1,}),cullingChunkSize:TILE_HEIGHT*16,};
5. Finalmente, un poco de carpintería
Quizás en el futuro MUD automatice el un par de funciones que debes de debes de conectar de manera manual. Esto permite al paquete del cliente conectarse los contratos.
packages/client/src/layers/phaser/constants.ts
exportenumScenes{Main="Main",}exportenumMaps{Main="Main",}exportenumAnimations{Player="Player",Coin="Coin",}exportenumDirections{UP=0,DOWN=1,LEFT=2,RIGHT=3,}exportenumAssets{MainAtlas="MainAtlas",Tileset="Tileset",}exportconstTILE_HEIGHT=32;exportconstTILE_WIDTH=32;
packages/client/src/mud/createSystemCalls.ts
import{getComponentValue}from"@latticexyz/recs";import{ClientComponents}from"./createClientComponents";import{SetupNetworkResult}from"./setupNetwork";import{singletonEntity}from"@latticexyz/store-sync/recs";exporttypeSystemCalls=ReturnType<typeofcreateSystemCalls>;exportfunctioncreateSystemCalls({worldContract,waitForTransaction}:SetupNetworkResult,{PlayerPosition,CoinPosition}:ClientComponents,){constspawn=async(x:number,y:number)=>{consttx=awaitworldContract.write.app__spawn([x,y]);awaitwaitForTransaction(tx);returngetComponentValue(PlayerPosition,singletonEntity);};constmove=async(direction:number)=>{consttx=awaitworldContract.write.app__move([direction]);awaitwaitForTransaction(tx);returngetComponentValue(PlayerPosition,singletonEntity);}constgenerateCoins=async(direction:number)=>{consttx=awaitworldContract.write.app__generateCoins();awaitwaitForTransaction(tx);returngetComponentValue(PlayerPosition,singletonEntity);}return{spawn,move,generateCoins};}
packages/client/src/layers/phaser/systems/registerSystems.ts
import{PhaserLayer}from"../createPhaserLayer";import{createCamera}from"./createCamera";import{createMapSystem}from"./createMapSystem";import{createMyGameSystem}from"./myGameSystem";exportconstregisterSystems=(layer:PhaserLayer)=>{createCamera(layer);createMapSystem(layer);createMyGameSystem(layer);};
6. Corre el juego
MUD tiene un sistema de hot reload. Esto significa que cuando haces un cambio en el juego, este es detectado y el juego se recarga efectuando los cambios. Esto aplica ambos el cliente y los contratos.
pnpm dev
Muévete con WASD, toca las monedas para seleccionarlas. ¡El juego es multiplayer online por defecto!
¡Gracias por leer esta guía!
Sígueme en dev.to y enYoutube para todo lo relacionado al desarrollo en Blockchain en Español.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse