All posts
cover image
MUD: An engine for Autonomous Worlds
last year By ludens, alvarius

Building systems of rules and entities with logic and storage fully on-chain is attractive for various reasons, and many of us want to see autonomous virtual worlds flourish. The thoughtful work put into projects like Conquest, Dark Forest, Isaac, Curio, Mithraeum, and others demonstrate the increasing interest in our small but growing scene of Autonomous Worlds.

We are also past the stage where building “fully on-chain games” is technologically risky. They have been built — multiple times even! — in different languages (Solidity and Cairo mostly) and with different architectures. Teams working on those projects rarely think about whether it is possible to ship their project; and instead go straight to coding it up.

However, in the process of exploring new designs for on-chain games, we were hit multiple time by the shortcomings of the traditional Dapp architectures and the libraries used to build DeFi apps. Given Dark Forest and other early on-chain games followed architectures and libraries used to build DeFi apps, those architectures became enshrined as the default of building on-chain virtual worlds.

The pattern can be summarized as such:

  • Different contracts touching the same state, sometimes using the Diamond Pattern in order to have a single address with all mutations.
  • One data structure per type of entities in the world: soliders[], planets[], etc. Each of those having their own struct and type.
  • “Getters” — functions returning batches of elements for each data structure — to load the initial state of the world: getPlanets(), getSoldiers(), …
  • Events, one per data structure, the client listen to in order sync state-updates in each new block after loading the initial state.

Work was done in order to reduce some of the pain involved with adding new rules and types of game entities when using this pattern (eg: Dark Forest network packages, Conquest Cloudflare cache worker); but it remained clear to us that we are much closer to handcrafted optimized codebases with almost no content (eg: Dark Forest “content” is the configuration file, Curio is their map and their config), and quite far from general engines with flexible networking.

The power of Engines

In 2004 — years before the first Bitcoin block was mined — World of Warcraft was released. It became the most popular massively online game of all time; and one of the highest grossing video game of all time.

WoW was developed over 5 years, four of those spent on building a custom engine and server architecture. Most of the content of WoW was added to the game in the last year of development. After four years, testers were able to walk around one village, fight a couple monsters, and play one quest. In the last 12 months, designers and artists implemented the entire game they had been working — mostly on paper! — over the last 4 years.

Engines are powerful: they leverage creatives at implementing the rules and art they want, while freeing the engineers from the minutiae and giving space for focusing on the hard problems.

From the perspective of modern game development and our experience building a few on-chain game prototypes where we ended up spending more time fighting with the Ethereum Dapp stack than designing rules and playtesting, we decided to experiment with building the missing on-chain game engine.

You see, we were using game engines: PhaserJS, Three, Godot, etc; but those were client engines: their job is to take the state of the World (where are the planets? is your bishop on the board?) and turn it into something the player can understand (eg: a 3D bishop piece with fancy shadows and a user interface to move it on the board).

We needed an engine that enforces a way to structure data, both on the chain and on the client, and takes care of the interface between two very different computing environment: a smart contract blockchain and a client on a personal computer.

Before writing any code, we first outlined a few intentions for this missing game engine:

  • We do not want to think about the engineering impact of adding a new property to a game object; We only care about the game design impact.
  • We do not want to ever have to modify the way the client stays in sync with the state on-chain, regardless of how many types of game entities there are.
  • We do not want to deal with low-level on-chain state mutations in the client. Ideally the client only executes actions when requirements are met and predicts updates of an action without calling a remote Ethereum node first.
  • We do not want to commit to the “types” of game entities upfront in the development. Modeling the state should be fluid, and code should be re-usable across different types of entities (eg: buying a soldier from a barrack should use the same code path as buying a mercenary from a camp).
  • We do not want to write different network services for each game. In order to load the state of on-chain games quickly, the developers of Dark Forest, Conquest, and Isaac all used different domain-specific indexers: The Graph, Cloudflare workers, and Apibara respectively. Each of those indexers need to be changed when the data modeling of the game changes and when the state mutations are updated.

Additionally, we ran an extensive survey of the software architecture pattern used in modern game engines to maintain developer’s productivity as the complexity of the relationships between game object increases. The most promising approach we found was an architecture pattern called ECS (Entity/Component/System).

We went to work about 7 months ago and built software running in the EVM, on the game client, next to the nodes, and on the developer’s machine.

We named this missing engine “MUD”, in homage to the Multi-User-Dungeons of the early internet that inspired many great multiplayer experiences from Ultima Online to Club Penguin.

We used MUD extensively this summer and built multiple game demos (strategy games, simulation prototypes, 3D voxel games, and more!) in a very short amount time. We believe MUD meaningfully advances the plot of complex applications running on-chain. Today, MUD is open-sourced at github.com/latticexyz/mud and the documentation can be found at MUD.dev.

MUD is the first significant release of Lattice, and we hope we can collectively steward together the first on-chain game engine in order to enable the next wave of World and Games running on Ethereum.

MUD “starter packs” and code-bases will be released in phases over the next few months, so make sure you follow Lattice on Twitter to be notified.

So what’s MUD?

MUD allows developers to create on-chain ownerless data namespaces called Worlds. A World is a registry of all things in the namespace, called Entities. An entity is a numerical ID. As an example, in a simple World resembling an ERC-20 contract, entities would be addresses. In a different World filled with flowers and birds, each of those would have a unique entity ID.

In order to give substance to the entities, MUD uses contracts called Components. Anyone can register a Component contract on the World, as long as their ID is unique. Components are little data-packets with a type that can be attached to entities.

Examples of two entities with different components attached to them.

Defining Components on-chain with MUD is trivial if it fits one of the data type in the standard library:

// A Component with a single property 
// storing a String in MUD
uint256 constant ID = 
  uint256(keccak256("mud.component.name"));
contract NameComponent is StringComponent {
  constructor(address world) StringComponent(world, ID) {}
}

In case the data-type of the Component is more specific, the developer needs to specify a schema for the client and a way to serialize / deserialize the data from and to bytes.

// A Component storing a complex data type
uint256 constant ID = 
  uint256(keccak256("mud.component.gameConfig"));
struct GameConfig {
  bool creativeMode;
}
contract GameConfigComponent is Component {
  constructor(address world) Component(world, ID) {}

  function getSchema() public pure override 
    returns (
      string[] memory keys, 
      LibTypes.SchemaValue[] memory values
    ) 
    {
    keys = new string[](1);
    values = new LibTypes.SchemaValue[](1);

    keys[0] = "creativeMode";
    values[0] = LibTypes.SchemaValue.BOOL;
  }

  function set(
    uint256 entity, 
    GameConfig memory gameConfig
  ) 
    public 
  {
    set(entity, abi.encode(gameConfig));
  }

  function getValue(uint256 entity) public view 
    returns (GameConfig memory) 
  {
    GameConfig memory gameConfig = 
      abi.decode(getRawValue(entity), (GameConfig));
    return gameConfig;
  }

  function getEntitiesWithValue(GameConfig memory value) 
    public view returns (uint256[] memory) 
  {
    return getEntitiesWithValue(abi.encode(value));
  }
}

Components on their own have no logic — they do not specify what it “means” to have some data attached to an entity. Instead, Components can be added to entities by System contracts.

Those systems need to have been granted write access to the respective components by their owners. Systems are also registered on the World, and anyone can register new Systems — although they will need to request write access to some components in case they want to update them.

MUD doesn’t create a distinction between “first party” and “third party” developer. Every developer is treated the same from the perspective of the World: anyone can create Components and Systems, just like anyone can create new ERC-20 tokens and “attach” them to an address.

The state of the World must be stored in the form of Components attached to entities. That’s the only constraint when it comes to using MUD.

Example of two very different Worlds modelled with MUD.

MUD allows Systems to ask questions about entities in the World via Queries. Those Queries are evaluated left to right and can include fragments like “Give me all entities with the component CanFly” and “Then keep entities with the Position component with value {x: 1, y: 43}".

QueryFragment[] memory fragments = new QueryFragment[](2);
// The first fragment asks for all entities with 
// the "CanFly" component
fragments[0] = QueryFragment(
      QueryType.Has,
      PrototypeComponent(
        getAddressById(components, CanFlyComponentID)),
      new bytes(0)
);
// The second fragment asks for all entities with 
// a "Position" component with value {x: 1, y: 43}
fragments[1] = QueryFragment(
      QueryType.HasValue,
      ItemTypeComponent(
        getAddressById(components, PositionComponentID)),
      abi.encode(1, 43)
);
// Return the entities matching the query
uint256[] memory results = LibQuery.query(fragments);

Any System can run Queries on the World, but remember that Systems need to be permissioned by Components in order to write to their value.


When a World is represented on-chain via Components, and its rules are implemented in Systems, clients can recreate the state of the World and find all Components and Systems by replaying events emitted by the World.

Reconstructing this state can be done by calling an Ethereum full-node. In order to make this process faster and less intensive, we wrote native indexers for MUD in Go. Those indexers can be used as-is for each new projects using MUD. Developers do not need to write new indexers.

The MUD indexing service indexes the entire state of each World, and the MUD streaming service creates a highly bandwidth efficient delta-update stream anyone can subscribe to to learn about all updates to the World in each Ethereum block.

The combination of those two services lets any client speaking gRPC know about the complete state of the World without having to write custom The Graph indexers, listening to events, or calling getter view functions on Ethereum node.

Those services are optional, and the MUD client library can reconstruct the state of a World and listen to updates without them if an Ethereum RPC is provided, although it might be more resource intensive and slower.

On the client, developers get access to the World state (all entities and their components) that is kept up to date using an Ethereum node or a stream service. This state is periodically cached to be recovered when the client is started at a later time, with the minimal amount of network calls made in order to catch up to the current state.


In order to represent the World client side (as an example with React or Unreal Engine 5), the World is queried using recs; our functional reactive ECS library.

Developers register dynamic queries on the World that gets fired every time the state of interest changes.

// Here we define a client side query that reacts to each 
// update to the World. Those updates are transparently 
// synchronized from the chain using the MUD services or an 
// Ethereum JSON-RPC if they aren't available
defineSystem(world, [Has(Position), Has(Movable)], 
  (update) => {
    const entityId = update.entity;
    const newPosition = update.value;
    // ... do stuff, like rendering the entity on the 
    // screen, etc. This callback will be called every time 
    // the output of this query changes.
  }
);

Components and entities can also be added to the World entirely client side — the World on the client is thus a combination of entities with components living on Ethereum and in the client only.

To interact with the World state client side, MUD can be fed an Ethereum private key. This key is used to interact with the on-chain System. We are currently working on an ECS account abstraction design in order to link ownership and permission to a secure wallet, while allowing service keys to perform low stake updates with getting user approval.

MUD automatically keeps track of all systems; types them using Typescript; and allows the developer to call them using their human-readable ID.

// This is how you call a System in MUD
// MUD takes care of discovering systems via human-readable 
// name. It also manages transaction queues, typings, and 
// local optimistic execution using an EVM
systems["game.system.buildBlock"].executeTyped(
  { x: 1, y: 2 }, 
  BlockTypes.Wood
);

MUD creates an efficient transaction queue, as well as a local EVM (courtesy of EthereumJS) to simulate the TX execution before it gets mined on the chain. In order to do that, MUD downloads the bytecode of all systems, and mocks all state read/write with the local ECS state.

Using MUD, developers can create rich requirements on System calls in order to hold off sending a transaction on-chain before a certain ECS state is reached


You can think of MUD like a full-node for a namespace on Ethereum: instead of syncing the state of the entire chain, a MUD client can sync the state of a namespace — a World — and query that state locally, register reactive queries on it, and simulate the effect of transactions, all without making a single network call to a remote full-node. MUD strongly types the ECS state and all Systems, so you never have to second guess the shape of your date or the types arguments of state transitions.

MUD scales well: we have built prototypes with dozens of components, tens of systems, and millions (!!) of entities.

MUD doesn’t prescribe how to represent the ECS state: we have used React, PhaserJS, NOA, Babylon, and ThreeJS in order to link a reactive ECS state with a renderer using recs.

The initial release is missing some features (most notably the local EVM) that we aim to release as soon as we stabilize the API. If you are interested in using MUD, checkout the Github, the docs, the Discord server. You can stay up to date with releases by following Lattice on Twitter.

We are also looking for additional contributors on MUD and other projects. Please reach out to alvarius [at] lattice [dot] xyz if interested.