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:
packages/client/src/App.tsx
(opens in a new tab), which controls the information displayed to the user. It is the place where you register handlers that are called when the value in a component (basically, a table) changes.packages/client/src/useKeyboardMovement.ts
(opens in a new tab), which receives keyboard events and dispatches them to calls to the onchain system.packages/client/src/mud/createSystemCalls.ts
(opens in a new tab), which is where you write code that performs calls to an onchain system.
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.