Interacting with MUD from a client
MUD is client agnostic: any client -- a browser, a game engine, or an iOS app -- can implement the synchronization protocol and a client-side cache to replicate Store tables, along with the necessary infrastructure to send transactions to the World.
However, currently we only officially support the browser and Node. We provide tooling for representing the onchain state and replicate it from an RPC or MODE via a network stack.
There also exists tooling for Unity (opens in a new tab) from the folks at emergence.land (opens in a new tab).
Representing state in the browser
When using MUD in the browser, we provide two different ways to query the Store and react to changes:
recs
:recs
is a client-side store optimize for ECS modelling. If you model your MUD data using ECS, this is a great choice.store-cache
:store-cache
is reactive tuple-database. It has a more powerful query system thanrecs
, and support multiple keys. If don't use the ECS data model (eg: you have multi-key tables, like one-to-many relationship), we recommend usingstore-cache
.
Note: when using the default MUD templates (created with pnpm create mud@canary
), the Store data will be available in both client-side stores. You are free to query with the one that makes the most sense.
Over time, we will migrate to store-cache
as the default and surface an opinionated layer for ECS data on top.
recs
: the reactive ECS database
recs
has been designed to represent and query ECS data.
If your MUD tables are set up with a single bytes32
key (see the ECS modelling guide), and you plan to represent an entity and its associated components as the same bytes32
key on multiple tables, then recs
will let you query your data in a natural way.
To read data and react to changes with recs
, there exists two APIs:
- Reading component value directly
- Subscribing to a query that returns a set of matching entities
For the examples below, let's assume this MUD config. Notice that each of these tables have an (implicit) bytes32
key; which is recommended when modelling your MUD data using ECS.
const config = mudConfig({
tables: {
NameComponent: "string",
PlayerComponent: "bool",
PositionComponent: { schema: { x: "int32", y: "int32" } },
},
});
Reading component value directly
If you have a specific bytes32
key and a reference to a component, you can query its value directly, or use a React hook that will re-render when the corresponding component value updates.
Reading component value in vanilla javascript
import { getComponentValueStrict } from "@latticexyz/recs";
// note: NameComponent is an recs component; components could come from the `setup` function of MUD.
const { NameComponent } = components
// get a reference to the recs world; as an example from the `setup` function of MUD.
const world = [...]
// you need a reference to an `Entity` from recs. For the sake of the example, let's say we know that the bytes32 key is "0xDEAD"
// you wouldn't do normally: entities would be found via queries.
const entityID = "0xDEAD" as EntityID;
const entity = world.registerEntity({ id: "0xDEAD" as EntityI })
// now we can fetch the value of the NameComponent on our entity
const name = getComponentValueStrict(NameComponent, entity)
// -> "John Doe"
Reading component value in react with @latticexyz/react
import { useComponentValue } from "@latticexyz/react";
function ExampleComponent() {
const { components, world } = useMUD;
const { NameComponent } = components;
// you need a reference to an `Entity` from recs. For the sake of the example, let's say we know that the bytes32 key is "0xDEAD"
// you wouldn't do this normally: entities would be found via queries.
const entityID = "0xDEAD" as EntityID;
const entity = world.registerEntity({ id: "0xDEAD" });
// now we can fetch the value of the NameComponent on our entity
const name = useComponentValue(NameComponent, entity);
// -> "John Doe"
// this will re-render when the component value changes!
return <p>{name ?? "<no name>"}</p>;
}
Running queries
A common way of working with ECS data is by using queries. A query is a function that returns a list of entities that match a set of predicates called query fragments.
There are 4 types of query fragments:
Has
: matches entities that have a specific component.Not
: matches entities that do not have a specific component.HasValue
: matches entities that have a specific component with a set value.NotValue
: matches entities that do not have a component with a set value (will also match if it doesn't have the component at all)
Running query with vanilla javascript
import { runQuery, Has, HasValue, getComponentValueStrict } from "@latticexyz/recs";
const { PlayerComponent, PositionComponent, NameComponent } = components
// query for all named players at the center of the universe
const matchingEntities = runQuery([
Has(PlayerComponent),
Has(Name),
HasValue(PositionCompoennt, {x: 0, y: 0})
])
// now you can map these to their name as an example
const names = matchingEntities.map(
playerEntity => getComponentValueStrict(NameComponent, playerEntity
)
// -> ["Bob", "Alice", "Eve"]
Reacting to queries with vanilla javascript
import { defineSystem, Has, HasValue, getComponentValueStrict, UpdateType } from "@latticexyz/recs";
const { PlayerComponent, PositionComponent, NameComponent } = components
// get a reference to the recs world; as an example from the `setup` function of MUD.
const world = [...]
defineSystem(world, [Has(PlayerComponent), Has(Name), HasValue(PositionComponent, {x: 0, y: 0}], ({entity, component, value, type}) => {
// every time an entity enter, exit, or get updated within a query; this callback with fire
// the "type" will match these: UpdateType.Enter, UpdateType.Exit, UpdateType.Update
// as an example, if a new entity is named, has a player component, and is at the center of the universe; the callback fires
// if the name of a player at the center of the universe changes, the callback will also fire for that entity
// let's only log when a new entity matches this query
// every time a named player reaches {0, 0}; we want to log their name
if(type !== UpdateType.Enter) return
console.log(getComponentValueStrict(NameComponent, entity) + " reached the center!")
})
Reacting to queries with React and @latticexyz/react
import { useEntityQuery } from "@latticexyz/react";
import { Has, HasValue, getComponentValueStrict } from "@latticexyz/recs";
function ExampleComponent() {
const { components, world } = useMUD;
const { NameComponent, PlayerComponent, PositionCompoennt } = components
// get a list of all entities that are named, players, and at the center of the universe
// it is reactive and will trigger a re-render when the set of matching queries update
const entities = useEntityQuery([Has(PlayerComponent), Has(Name), HasValue(PositionComponent, {x: 0, y: 0}])
// [entity1, entity2, ...]
return(
<div>
<span>Players at the center:</span>
<ul>
{entities.map(entity => (
<li>{getComponentValueStrict(NameComponent, entity)}</li>
))}
</ul>
</div>
)
}
store-cache
: a general tuple database
store-cache
is a typed reactive database used to replicate the onchain Store of a MUD project into a Javascript client (Node or Browser based).
Unlike recs
, store-cache
supports multiple keys. It also comes with a more powerful query system. Currently, ECS-like queries where the same primary key is assumed to be same entity (like what recs
does) are quite verbose. We are working on making this better. Meanwhile, please make these queries with recs
.
store-cache
surfaces up three APIs:
- Fetching a specific record from a table when the keys are known
- Scanning a table with a query
- Subscribing to a query
The store-cache
package also exposes React hooks for each of these use cases.
Let's assume this MUD config for the examples below. Notice that some tables have multiple keys in their
keySchema
. We recommend using store-cache
when querying these tables.
const config = mudConfig({
tables: {
MultiKey: { keySchema: { first: "bytes32", second: "uint32" }, schema: { value: "uint256" } },
AddressCounter: { keySchema: { owner: "address", counterId: "uint256" }, schema: { value: "int256" } },
Position: { schema: { x: "int32", y: "int32" } },
},
});
Fetching a specific record
Fetching a record in vanilla javascript
// This is typed!
const position = client.tables.Position.get({ key: "0x00" });
// -> { x: 1, y: 2}
const multiKey = client.tables.MultiKey.get({ first: "0xDEAD", second: 42 });
// -> { value: 38843n }
Fetching a record with React and @latticexyz/react
import { useRow } from "@latticexyz/react";
function ExampleComponent() {
const { storeCache } = useMUD;
const position = useRow(client, { table: "Position", key: { key: "0x01" } });
// -> { namespace: "", table: "Position", key: { key: "0x01" }, value: { x: 1, y: 2 } }
const { value } = position;
return (
<div>
<span>Player position</span>
<span>
x: {value.x}, y: {value.y}
</span>
</div>
);
}
Scanning a table with a query
Scanning a table in vanilla javascript
const rows = client.scan(); // get all rows in the store
// -> all rows in the store
const positions = client.tables.Position.scan(); // get all rows in the position table
// -> [{key: "0x00", value: {x: 10, y: 32}, namespace: config["namespace"], table: Position}, ...]
const positionsFiltered = client.tables.Position.scan({
key: { gt: { key: "0x02" } },
}); // get all entries whose key is bigger than 0x02
// -> [{key: "0x03", value: {x: 69, y: 4}, namespace: config["namespace"], table: Position}, ...]
Scanning a table with React and @latticexyz/react
import { useRows } from "@latticexyz/react";
function ExampleComponent() {
const { storeCache } = useMUD;
const positions = useRows(client, { table: "Position" });
// -> [{key: "0x00", value: {x: 10, y: 32}, namespace: config["namespace"], table: Position}, ...]
const positionsFiltered = useRows(client, { table: "Position", key: { gt: { key: "0x02" } } });
//-> [{key: "0x03", value: {x: 69, y: 4}, namespace: config["namespace"], table: Position}, ...]
return (
<div>
<span>Positions:</span>
<ul>
{positions.map(({ key, value }) => (
<li>
{key}: {value.x}, {value.y}
</li>
))}
</ul>
<span>Filtered Positions:</span>
<ul>
{positionsFiltered.map(({ key, value }) => (
<li>
{key}: {value.x}, {value.y}
</li>
))}
</ul>
</div>
);
}
Subscribe to queries
Subscribing to queries in vanilla javascript
// subscribe to all changes of the Position table
client.tables.Position.subscribe(({ set, remove }) => {
for (const entrySet of set) {
// { key: "0x00" }, { x: 1, y: 2 }
console.log(entrySet.key, entrySet.value);
}
});
// subscribe to all changes of a single record with key 0x01
client.tables.Position.subscribe(
({ set, remove }) => {
for (const entrySet of set) {
// { key: "0x01" }, { x: 1, y: 2 }
console.log(entrySet.key, entrySet.value);
}
},
{ key: { eq: { key: "0x01" } } }
);