Guides
Players and movement

Players and movement

In this section, we will accomplish the following:

  • Spawn in each unique wallet address as an entity with the Player, Movable, and Position components.
  • Operate on a player's Position component with a system to create movement.
  • Optimistically render player movement in the client.

Create the components as tables

To create tables in MUD we are going to edit the mud.config.ts file. You can define tables, their types, their schemas, and other types of information here. MUD then autogenerates all of the files needed to make sure your app knows these tables exist.

We're going to start by defining three new tables:

  • Player: 'bool' - determine which entities are players (e.g. distinct wallet addresses).
  • Movable: 'bool' - determine whether or not an entity can move.
  • Position: { valueSchema: { x: 'uint32', y: 'uint32' } } - determine which position an entity is located on a 2D grid.

The syntax is as follows:

packages/contract/mud.config.ts
import { mudConfig } from "@latticexyz/world/register";
 
export default mudConfig({
  enums: {
    // TODO
  },
  tables: {
    Movable: "bool",
    Player: "bool",
    Position: {
      dataStruct: false,
      valueSchema: {
        x: "uint32",
        y: "uint32",
      },
    },
  },
});
Explanation
    Movable: "bool",
    Player: "bool",

When we do not specify a key schema for a table, MUD uses the default, a bytes32 value. The key in these cases is the entity ID for the entities being described. As in most cases in the Ethereum ecosystem, there is no distinction between zero (or in the case of boolean values, false) and nothing. So by default entites are neither Movable nor a Player.

    Position: {
      dataStruct: false,
      valueSchema: {
        x: "uint32",
        y: "uint32",
      },

The Position of an entity includes both an x coordinate and a y coordinate. When a table has multiple fields, we use valueSchema inside a structure to describe them instead of just using a string with the Solidity field type.

If after modifying the file you get an error on the pnpm dev process, restart it.

Create the map system and its methods

In MUD, a system can have an arbitrary number of methods inside of it. Since we will be moving players around on a 2D map, we start the codebase off by creating a system that will encompass all of the methods related to the map: MapSystem.sol in packages/contracts/src/systems.

spawn method

Before we add in the functionality of users moving we need to make sure each user is being properly identified as a player with the position and movable table. The former gives us a means of operating on it to create movement, and the latter allows us to grant the entity permission to use the move system.

To solve for these problems we can add the spawn method, which will assign the Player, Position, and Movable tables we created earlier, inside of MapSystem.sol.

packages/contract/src/systems/MapSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { Movable, Player, Position } from "../codegen/index.sol";
import { addressToEntityKey } from "../addressToEntityKey.sol";
 
contract MapSystem is System {
  function spawn(uint32 x, uint32 y) public {
    bytes32 player = addressToEntityKey(address(_msgSender()));
    require(!Player.get(player), "already spawned");
 
    Player.set(player, true);
    Position.set(player, x, y);
    Movable.set(player, true);
  }
 
  function distance2(uint32 deltaX, uint32 deltaY) internal pure returns (uint32) {
    return deltaX * deltaX + deltaY * deltaY;
  }
}
Explanation
import { Movable, Player, Position } from "../codegen/index.sol";

Import the components we use from the automatically generated table list.

import { addressToEntityKey } from "../addressToEntityKey.sol";

This function (opens in a new tab) converts an address, use as the player's, to a bytes32 value that can identify an entity.

  function spawn(uint32 x, uint32 y) public {

The spawn function spawns a new player entity on the map.

    bytes32 player = addressToEntityKey(address(_msgSender()));

In certain cases systems are called by a MUD World using the call opcode. In that case, msg.sender() is the World that called the system, not the actual player. _msgSender() takes care of this and gives us the real user identity, the one who called the World.

    require(!Player.get(player), "already spawned");

To get a component value we use <Component name>.get(<entity>).

    Player.set(player, true);
    Position.set(player, x, y);
    Movable.set(player, true);
  }

To set a component value we use <Component name>.set(<entity>, <value>).

As you see, writing systems and their methods in MUD is similar to writing regular smart contracts. The key difference is that their state is defined and stored in tables rather than in the system contract itself.

moveBy method

Next we’ll add the moveBy method to MapSystem.sol. This will allow us to move users (e.g. the user's wallet address as their entityID) by updating their Position table.

packages/contract/src/systems/MapSystem.sol
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;
import { System } from "@latticexyz/world/src/System.sol";
import { Movable, Player, Position } from "../codegen/index.sol";
import { addressToEntityKey } from "../addressToEntityKey.sol";
 
contract MapSystem is System {
  function spawn(uint32 x, uint32 y) public {
    bytes32 player = addressToEntityKey(address(_msgSender()));
    require(!Player.get(player), "already spawned");
 
    Player.set(player, true);
    Position.set(player, x, y);
    Movable.set(player, true);
  }
 
  function moveBy(uint32 clientX, uint32 clientY, int32 deltaX, int32 deltaY) public {
    bytes32 player = addressToEntityKey(_msgSender());
    require(Movable.get(player), "cannot move");
 
    (uint32 fromX, uint32 fromY) = Position.get(player);
    require(distance2(deltaX, deltaY) == 1, "can only move to adjacent spaces");
    require(clientX == fromX && clientY == fromY, "client confused about location");
 
    uint32 x = uint32(int32(fromX) + deltaX);
    uint32 y = uint32(int32(fromY) + deltaY);
 
    Position.set(player, x, y);
  }
 
  function distance2(int32 deltaX, int32 deltaY) internal pure returns (uint32) {
    return uint32(deltaX * deltaX + deltaY * deltaY);
  }
}
Explanation
  function moveBy(uint32 clientX, uint32 clientY, int32 deltaX, int32 deltaY) public {

If the client and the onchain code ever get out of sync about a player's position, it's best to forbid user movement until they synchronize again. To be able to detect that situation, we have the client code call the system onchain with what it thinks is the player's position.

    (uint32 fromX, uint32 fromY) = Position.get(player);

This is how we get the player's real position (the onchain state is the authoritative version).

    require(distance2(deltaX, deltaY) == 1, "can only move to adjacent spaces");
    require(clientX == fromX && clientY == fromY, "client confused about location");

If the attempted move is more than one space, or if the client is out of sync about the player's location, do not move.

This method will allow users to interact with a smart contract, auto-generated by MUD, to update their position. However, we are not yet able to visualize this on the client, so let's add that to make it feel more real.

Modify the user interface to call the map system

We’ll fill in the moveBy and spawn methods in our client’s createSystemCalls.ts.

packages/client/src/mud/createSystemCalls.ts
import { Has, HasValue, getComponentValue, runQuery } from "@latticexyz/recs";
import { uuid } from "@latticexyz/utils";
import { ClientComponents } from "./createClientComponents";
import { SetupNetworkResult } from "./setupNetwork";
 
export type SystemCalls = ReturnType<typeof createSystemCalls>;
 
export function createSystemCalls(
  { playerEntity, worldContract, waitForTransaction }: SetupNetworkResult,
  { Player, Position }: ClientComponents
) {
  const moveBy = async (deltaX: number, deltaY: number) => {
    if (!playerEntity) {
      throw new Error("no player");
    }
 
    const playerPosition = getComponentValue(Position, playerEntity);
    if (!playerPosition) {
      console.warn("cannot moveBy without a player position, not yet spawned?");
      return;
    }
 
    const tx = await worldContract.write.moveBy([playerPosition.x, playerPosition.y, deltaX, deltaY]);
    await waitForTransaction(tx);
  };
 
  const spawn = async (x: number, y: number) => {
    if (!playerEntity) {
      throw new Error("no player");
    }
 
    const canSpawn = getComponentValue(Player, playerEntity)?.value !== true;
    if (!canSpawn) {
      throw new Error("already spawned");
    }
 
    const tx = await worldContract.write.spawn([x, y]);
    await waitForTransaction(tx);
  };
 
  return {
    moveBy,
    spawn,
  };
}
Explanation
export function createSystemCalls(
  { playerEntity, worldContract, waitForTransaction }: SetupNetworkResult,

From the network setup we get the user identity (playerEntity), the address of the World we use, and the function to wait for a transaction to be included.

  { Player, Position }: ClientComponents

The client code that uses the system calls needs the Player and Position components, as you'll see below.

) {
  const moveBy = async (deltaX: number, deltaY: number) => {

This function moves the player by a specific amount.

if (!playerEntity) {
  throw new Error("no player");
}
 
const playerPosition = getComponentValue(Position, playerEntity);
if (!playerPosition) {
  console.warn("cannot moveBy without a player position, not yet spawned?");
  return;
}

If there is no player, or no player position, we can't move. Currently there is no way to spawn a player without setting a position, so this check is redundant - but we may introduce a spawn method in the future that is positionless, in which case we'll need it.

    const tx = await worldContract.write.moveBy([playerPosition.x, playerPosition.y,
      deltaX, deltaY]);
    await waitForTransaction(tx);
  };

This (worldContract.write.<method>) is the way we call a method on a system at the root namespace.

  const spawn = async (x: number, y: number) => {
    ...
  }

Spawn a new player. It is very similar to moveBy above.

  return {
    moveBy,
    spawn,
  };
}

Return the functions in a structure so they can be called.

Now we can apply all of these backend changes to the client by updating GameBoard.tsx to spawn the player when a map tile is clicked, show the player on the map, and move the player with the keyboard.

packages/client/src/GameBoard.tsx
import { useComponentValue } from "@latticexyz/react";
import { GameMap } from "./GameMap";
import { useMUD } from "./MUDContext";
import { useKeyboardMovement } from "./useKeyboardMovement";
 
export const GameBoard = () => {
  useKeyboardMovement();
 
  const {
    components: { Player, Position },
    network: { playerEntity },
    systemCalls: { spawn },
  } = useMUD();
 
  const canSpawn = useComponentValue(Player, playerEntity)?.value !== true;
 
  const playerPosition = useComponentValue(Position, playerEntity);
  const player =
    playerEntity && playerPosition
      ? {
          x: playerPosition.x,
          y: playerPosition.y,
          emoji: "🤠",
          entity: playerEntity,
        }
      : null;
 
  return <GameMap width={20} height={20} onTileClick={canSpawn ? spawn : undefined} players={player ? [player] : []} />;
};

You can run this command to update all the files to this point in the game's development.

git reset --hard 025bae043c6b1f787bba13bec298dbda10d92c37

Now that we have players, movement, and a basic map, let's start making improvements to the map itself.