Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Filosofía Código EN profile imageAhmed Castro
Ahmed Castro forFilosofía Código EN

Posted on • Edited on

How to create an Autonomous World Game

MUD started as a framework for creating 100% on-chain games, but now it is much more than that. In addition to creating interactive games with smart contract mechanics, MUD is a tool capable of creating autonomous worlds. What is an autonomous world? This goes beyond finance and games; in these worlds, you can create simulations where artificial intelligence is first-class citizens. If this sounds pretty crazy, it’s because it is.

To learn more about MUD, let’s start by creating a game where characters collect coins. In this guide, we will do everything from scratch. We’ll create the contracts, the project structure, and the animations.

MUD Example Game

Create a New MUD Project

We’ll be using Node v20 (>=18 should be ok), pnpm, and Foundry. If you don’t have them installed, here are the commands.

Dependency installation
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

Once the dependencies are installed, you can create a new MUD project.

pnpm create mud@latest tutorialcdtutorial
Enter fullscreen modeExit fullscreen mode

During the installation process, select a Phaser project.

slect phaser with mud

1. The State Schema

This is the most important file in MUD; it defines the data structure of the state of your contracts. In MUD, you don't declare state variables likemapping,uint,bool, etc., nor do you useenum. Instead, you define them in the file below.

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. Logic in Solidity

The logic for functions in MUD is the same as in a regular Solidity project. Declare yourview,payable,pure functions with modifiers and everything else just as you’re used to.

So, delete the default logic and create the game logic that validates when a player moves and collects coins.

Deletepackages/contracts/src/systems/IncrementSystem.sol that comes by default. You only need to do this if you’re using thevanilla,react-ecs, orphaser templates provided by 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;require(x>=-31&&x<=31&&y>=-31&&y<=31,"Invalid position");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

Actually, not all functions operate the same way as in a vanilla Solidity project. If you try, you’ll notice that the constructor doesn’t work as expected. This is becauseSystems are designed to operate without a specific state. This means that a singleSystem can operate with multipleWorlds, which are responsible for managing the state. This is why we put all the initialization logic in the post-deploy contract.

In our case, we place certain coins on the map.

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. Client Interaction

The client contains all the off-chain logic. This is where we define what is displayed on the screen and also what happens when the user clicks or presses a key.

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. Add Images and Animations

If you want to add an animated object, place it in its own folder underpackages/art/sprites/. Ensure that the animation frames are named sequentially, such as1.png,2.png,3.png, etc.

In our case, we will add 2 images for our character and one for the coins.

packages/art/player/1.png
Player 1 demo

packages/art/player/2.png
Player 2 demo

packages/art/coin/1.png
Coin demo

Next, run the following commands to automate the packaging of your files into sprite sheets.

cdpackages/artyarnyarn generate-multiatlas-sprites
Enter fullscreen modeExit fullscreen mode

In the Phaser file, you can configure the speed and name of the animations. Here, you can also define attributes that you can use in your system.

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. Bind everything together

Perhaps in the future, MUD will automate a couple of functions that you need to connect manually. This allows the client package to connect to the contracts.

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. Run the Game

MUD has a hot reload system. This means that when you make a change in the game, it is detected, and the game reloads, applying the changes. This applies to both the client and the contracts.

pnpm dev
Enter fullscreen modeExit fullscreen mode

Move with WASD, touch the coins to collect them. The game is multiplayer online by default!

Thanks for reading this guide!

Follow Filosofía Código on dev.to and inYoutube for everything related to Blockchain development.

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

More fromFilosofía Código EN

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