Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Ahmed Castro
Ahmed Castro

Posted on

     

Cómo crear un Juego en un Mundo Autónomo

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.

juego ejemplo mud

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
Enter fullscreen modeExit fullscreen mode

Una vez instaladas las dependencias puedes crear un proyecto nuevo de MUD.

pnpm create mud@latest tutorialcdtutorial
Enter fullscreen modeExit fullscreen mode

Durante este proceso seleccionamos phaser como plantilla.

slect phaser with mud

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"]}}});
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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);}}}
Enter fullscreen modeExit fullscreen mode

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 losSystems están diseñados para operar sin un estado específico. Es decir, un mismoSystem puede operar con múltiplesWorlds 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();}}
Enter fullscreen modeExit fullscreen mode

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);}})};
Enter fullscreen modeExit fullscreen mode

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.png3.png, etc..

En nuestro caso agregaremos 2 imágenes para nuestro personaje y una para las monedas.

packages/art/player/1.png
Image description

packages/art/player/2.png
Image description

packages/art/coin/1.png
Image description

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
Enter fullscreen modeExit fullscreen mode

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,};
Enter fullscreen modeExit fullscreen mode

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;
Enter fullscreen modeExit fullscreen mode

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};}
Enter fullscreen modeExit fullscreen mode

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);};
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Videos sobre programación en blockchain en español
  • Joined

More fromAhmed Castro

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp