Templates
TypeScript
Three.js

Three.js

This template is for developers who use Three.js (opens in a new tab).

You can start a new project using this template with:

pnpm create mud tutorial --template threejs

For more information, see the quickstart guide.

Onchain code

mud.config.ts

This file (opens in a new tab) contains the table definition.

import { mudConfig } from "@latticexyz/world/register";
 
export default mudConfig({
  tables: {
    Position: {
      valueSchema: {
        x: "int32",
        y: "int32",
        z: "int32",
      },
    },
  },
});

The table schema has one table, Position. This table has no specified key schema, so MUD uses the default, a single bytes32 field. This allows us to tie table records to entities, which in MUD have a bytes32 identifier.

MoveSystem.sol

This file (opens in a new tab) is the system that is used to modify the data in MUD, which in this case is only Position.

pragma solidity >=0.8.21;
import { System } from "@latticexyz/world/src/System.sol";

The basic definition of what a System is.

import { Position, PositionData } from "../codegen/index.sol";

Get the definitions related to the Position table, which are automatically generated from mud.config.ts.

function distance(PositionData memory a, PositionData memory b) pure returns (int32) {
  int32 deltaX = a.x > b.x ? a.x - b.x : b.x - a.x;
  int32 deltaY = a.y > b.y ? a.y - b.y : b.y - a.y;
  int32 deltaZ = a.z > b.z ? a.z - b.z : b.z - a.z;
 
  return deltaX + deltaY + deltaZ;
}

A utility function to calculate Manhattan Distance (opens in a new tab). This is used to ensure that a player does not move more than one unit at a time (in any direction).

contract MoveSystem is System {
  function move(int32 x, int32 y, int32 z) public {

This is the only function that MoveSystem exposes. It lets a user move in the game universe.

    bytes32 entityId = bytes32(uint256(uint160((_msgSender()))));

_msgSender() lets us know who called the World which received the call and then relayed it to the system. The caller is an ethereum address, which is 20 bytes long. To turn it into a bytes32 value, which is the key we use in the Position table, we need to cast the address uint160 (20 bytes are 160 bits), then to uint256, and finally to bytes32.

    PositionData memory position = Position.get(entityId);
    PositionData memory newPosition = PositionData(x, y, z);
    require(distance(position, newPosition) == 1, "can only move to adjacent spaces");

Use the PositionData struct to calculate the distance between the old position and the new one. If this distance is more than one unit, fail.

    Position.set(entityId, newPosition);
  }
}

Otherwise, update the position for entityId, which is the address that called the World, to the new position.

Offchain code

The files you are likely to need to change in the offchain code are:

App.tsx

The relevant definitions are in the Scene React component (opens in a new tab):

const Scene = () => {
  const {
    components: { Position },
    network: { playerEntity },
  } = useMUD();

Get the MUD API information.

useKeyboardMovement();

Register the handler in useKeyboardMovement.ts.

const playerPosition = useComponentValue(Position, playerEntity);

This is the way you specify a listener for a MUD table in React.

const players = useEntityQuery([Has(Position)]).map((entity) => {
  const position = getComponentValueStrict(Position, entity);
  return {
    entity,
    position,
  };
});

Get the list of players (with the entityId and position). Both useComponentValue and useEntityQuery cause the React component in which they are registered (in this case, Scene) to be rendered again when the information changes.

.
.
.
  return (
    <group>
      <ambientLight />
      {/* eslint-disable-next-line react/no-unknown-property */}
      <pointLight position={[10, 10, 10]} />
      <Plane position={[0, -5, 0]} />
      {players.map((p, i) => (
        <Player
          key={i}
          color={Math.floor(parseInt(p.entity) * 123456) % 16777215}
          position={[p.position.x, p.position.y, p.position.z]}
        />
      ))}
    </group>
  );

Return the React component, including the players.

useKeyboardMovement.ts

This file (opens in a new tab) registers the event handler for keyboard actions.

import { useEffect } from "react";
import { useMUD } from "./MUDContext";
 
export const useKeyboardMovement = () => {
  const {
    systemCalls: { moveBy },
  } = useMUD();

useMUD() gives us the moveBy system call, which lets us move.

  useEffect(() => {
    const listener = (e: KeyboardEvent) => {
      if (e.key === "ArrowUp") {
        moveBy(1, 0, 0);
      }
      if (e.key === "ArrowDown") {
        moveBy(-1, 0, 0);
      }      .
      .
      .
      .

Call moveBy with different parameters depending on the key pressed.

    window.addEventListener("keydown", listener);
    return () => window.removeEventListener("keydown", listener);
  }, [moveBy]);
};

The second parameter of useEffect (opens in a new tab) is a list of dependencies. Here we only have one, moveBy.

createSystemCalls.ts

This file (opens in a new tab) is where you place the calls that go to the onchain system. Here are two, moveTo and moveBy.

  const moveTo = async (x: number, y: number, z: number) => {

You can use moveTo to move to specific coordinates.

    /*
     * Because MoveSystem is in the root namespace, .move can be called directly
     * on the World contract.
     */
    const tx = await worldContract.write.move([x, y, z]);
    await waitForTransaction(tx);
  };

Create a transaction and wait for it to be included.

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

moveBy is a wrapper around moveTo that lets us specify movements by relative, rather than absolute, cooridnates.

    console.log({ Position, playerEntity });
    const playerPosition = getComponentValue(Position, playerEntity);
 
    if (playerPosition) {
      await moveTo(playerPosition.x + deltaX, playerPosition.y + deltaY, playerPosition.z + deltaZ);
    } else {
      await moveTo(deltaX, deltaY, deltaZ);
    }
  };

If there is a position for the player, move from there. Otherwise, just move from (0,0,0).

return {
  moveTo,
  moveBy,
};

Return the functions that the rest of the application can use.