Lattice (@latticexyz) MUD is the most longstanding and renowned full-chain game engine in the Web3 space. In its previous first-generation version, it clearly stated that MUDv1 is a framework based on ECS. I also wrote a related introductory article titled “In-depth Analysis of the Full-Chain Game Engine MUD.” A few weeks ago, Lattice announced the V2 version, which made significant changes to the overall architecture of MUD, weakening its connection to ECS and introducing a new on-chain database called “Store,” which is based on a “table” data structure. So how should we understand the relationship between “ECS” and “table”? How does the new component “Store” work? Is “ECS” suitable for creating full-chain games? We will explore these questions in this article.
Overview of MUDv2#
MUDv2 is a framework for building Ethereum applications. It compresses the complexity of building EVM applications through a tightly integrated software stack. MUDv2 includes: Store (an on-chain database), World (an entry point framework that provides standardized access control, upgrades, and modules), fast development tools based on Foundry, client data storage that reflects on-chain state, and MODE (a Postgres database that can use SQL queries and reflect your on-chain state).
MUDv2 is not a rollup or a chain; it is a set of libraries and tools that can work together to build on-chain applications. MUDv2 is not limited to the Ethereum mainnet; it can work on any EVM-compatible chain. MUDv2 is not only suitable for autonomous worlds and on-chain games, but it also does not impose a data model on developers. You can do anything with MUD that you can do with flat Solidity mappings and arrays. MUDv2's data availability scheme is similar to conventional applications deployed on the Ethereum mainnet, such as ENS and Uniswap.
The main idea of MUDv2 is that all on-chain state is stored in the Store (MUD's on-chain database). The way we currently handle smart contract state leads to some major issues, such as the coupling of state and logic, which makes upgrading logic very difficult. With MUD, you will never use data storage driven by the Solidity compiler. All state is saved and retrieved using the Store, which is an efficient on-chain database. The Store is like SQLite: it is an embedded database manually optimized in Yul. It has tables, columns, and rows.
MUDv2 logic is stateless and has custom permissions that can be called across contracts. MUD recommends using World: an entry point kernel responsible for mediating access to the Store from different contracts. World creates a Store upon deployment. The tables in each Store are registered under a namespace, represented by a name, just like a flat file system path.
MUDv2 does not require indexers or subgraphs; the frontend will sync itself: when using the Store (and the extended World), your on-chain data self-checks, and any changes are broadcast through standard events. These events and patterns are utilized by MODE: MODE transforms your on-chain state into an SQL database and maintains millisecond-level latency updates.
What is the Essence of ECS#
Through literature review, I personally believe (the author @hicaptainz is not a developer, so please correct any errors) that the ECS (Entity-Component-System) pattern is essentially a way of modeling data structures, with its core being how to store and organize data.
-
Entity: In the ECS pattern, an entity is an abstract concept that does not directly hold data but associates data through components. An entity can be seen as a container for one or more components, and its main role is to provide a unique identifier for the components.
-
Component: A component is a carrier of data. In the ECS pattern, all data is encapsulated in components. Each component represents a specific attribute or behavior, such as position, speed, color, etc. Components only contain data and do not include any logic or behavior.
-
System: A system is where data is processed. Systems decide how to process entities based on their components. Each system has one or more specific tasks, such as rendering, physics simulation, AI logic, etc.
From this perspective, the ECS pattern is indeed a way of modeling data structures. It separates data (components) from behavior (systems), making data storage and processing more flexible and efficient. The advantages of this pattern include:
- Composability: By combining different components, entities with various attributes and behaviors can be created without needing to create a large number of classes or structures.
- Data locality: Since components only contain data, related data can be stored closely together, improving cache utilization and thus enhancing performance.
- Reusability: Systems only care about data and not about which entity the data comes from, allowing the same system to be reused across multiple entities.
- Parallelism: The separation of data and behavior makes it easier to process data in parallel in a multithreaded environment.
Types of ECS Data Structure Modeling#
So what types of data structure implementations can achieve ECS? Generally, the two most popular types are Archetype and Sparse set, while others like Bitset and Reactive ECS are less common. Each has its advantages and applicable scenarios.
-
Archetype (also known as Table based ECS): Archetype is a storage method optimized for data locality, storing entities with the same component set in a table where components are columns and entities are rows. Unity's ECS system adopts this method. The advantage of Archetype is that since the same component set is contiguous in memory, it can greatly improve cache efficiency and data access speed. Additionally, since each Archetype knows the components it contains, entities can be created and destroyed dynamically at runtime without extensive memory operations.
-
Sparse Set (also known as Sparse ECS): Sparse Set is an efficient data storage and access method that combines the advantages of arrays and hash tables. In ECS based on Sparse Set, each component is stored in its own sparse set, with the set keyed by entity ID. The advantage of this method is that it can effectively utilize the cache while also saving space. However, the downside is that it is more complex to implement.
As for Bitset and Reactive ECS, they also have their unique advantages but may not be suitable for all application scenarios.
-
Bitset: Bitset is a simple and efficient method for checking whether an entity has a specific component. However, since Bitset itself cannot store component data, it usually needs to be combined with other storage methods.
-
Reactive ECS: Reactive ECS is a more advanced pattern that allows systems to respond to the addition, deletion, and modification of components. This pattern can make the code clearer and easier to understand but may increase implementation complexity.
If any readers have seen my article “How Curio Built ECS Game Engine into OPStack?”, they will immediately notice that Curio adopts the Sparse Set method to store and operate ECS data. So what about MUDv2? Clearly, it is “Archetype,” or “table based ECS.”
How the Store Component Works#
The data model provided by MUDv2's Store component can well support the ECS pattern. In the Store, you can create a table to represent entities, each with a unique key. You can add various components to each entity, which can be fields in the table. Then, you can write systems to process these components, which can be functions in smart contracts.
For example, you can create a "Player" table with "position" and "health" fields. Then, you can write a "move" function to change the player's position and a "damage" function to reduce the player's health. So, while the Store component of MUD does not directly implement the ECS pattern, its data model can effectively support the implementation of the ECS pattern.
Some may ask, does this table-based on-chain database also support OOP (Object-Oriented Programming) patterns?
The Store component provides a table-based data model, which is closer to a relational database or data-driven design rather than an object-oriented programming (OOP) model. In OOP, data and the methods that operate on that data are encapsulated in objects, while in the Store model, data is stored in tables, and the logic for operating on the data is handled by functions in smart contracts.
However, this does not mean you cannot use object-oriented design patterns in smart contracts. You can create a smart contract to represent an object, which can have state variables to represent the object's properties and functions to represent the object's methods. Then, you can use the Store to persist the state of these objects.
For example, you can create a "Player" smart contract with "position" and "health" state variables, as well as "move" and "damage" functions. Then, you can use the Store to store the "position" and "health" of each "Player" instance. So, while the data model of the Store does not directly support object-oriented programming, you can use object-oriented design patterns in smart contracts and use the Store to persist the state of the objects.
But as we have emphasized, for full-chain games, the ECS pattern is still more suitable because one significant advantage here is that new systems can be introduced at any stage of development and will automatically match any existing and new entities with the correct components. This promotes a design where systems are developed as single-responsibility, small functional units that can be easily deployed across different projects, thus achieving the “composability” of full-chain games.
Storage of On-Chain Data#
Since the Store component establishes an on-chain database, where exactly is this on-chain data stored? Let's recall the storage methods of the EVM.
In the Ethereum Virtual Machine (EVM), on-chain data is primarily stored in two data structures: storage and memory.
-
Storage: This is the persistent storage of each smart contract, and its data is persistent between transactions. Storage consists of key-value pairs, where both keys and values are 256 bits. In Solidity, the state variables of a contract are stored here. For example, if you define a
uint256 public counter;
in a contract, thiscounter
variable is stored in storage. Each time you call a function that changes thecounter
, it will update in storage. -
Memory: This is the temporary storage for each function call, and its data is cleared after the function call ends. Memory is linear and can be imagined as a byte array. In Solidity, local variables of functions are stored here.
Additionally, there is a data structure called the stack, which is used to store function call parameters and return values, as well as some temporary computation results. The stack size is limited, containing a maximum of 1024 elements.
In the EVM, the cost of reading and writing to storage is very high because it consumes gas. Therefore, smart contract developers usually try to optimize their code to reduce storage operations. In contrast, the cost of reading and writing to memory is much lower, but since the data in memory is cleared after the function call ends, it is only suitable for storing temporary data.
The Store component of MUDv2 stores data in the EVM's storage. Each smart contract has its own storage space in the EVM, which is persistent, meaning that even between transactions, the data in storage remains unchanged.
The Store component provides a higher-level abstraction that allows developers to more conveniently store and read data in smart contracts. Developers can define their own tables, each with a specific schema that defines the fields and their types in the table. Then, developers can use the API provided by the Store, such as set, get, etc., to operate on the data in these tables.
These operations will ultimately be converted into read and write operations on the EVM storage. For example, when you call the MyTable.set function to set a record, this function will encode the data into bytes and then write it to the EVM's storage. When you call the MyTable.get function to retrieve a record, this function will read data from the EVM's storage and then decode it into the type you defined.
The Overlooked State Machine Problem#
In our analysis above, we seem to see only the advantages of the ECS pattern. So are there really no drawbacks? Here we need to start with the EVM itself and state machines.
A state machine, also known as a state automaton, is a model that describes the changes in an object's state and how it responds to inputs (such as events or conditions). In a state machine, the system is in a certain state at any given time and will transition from one state to another under the influence of inputs. In traditional game engines, state machines are often used to manage the behavioral states of game objects (such as characters, enemies, AI, etc.). These states may include walking, running, jumping, attacking, defending, dying, etc. State machines can help us clearly define and control the transitions between different states of game objects.
For the EVM (Ethereum Virtual Machine), we can think of it as a globally distributed state machine. In the Ethereum network, each block contains a series of transactions, and these transactions are the inputs. The EVM executes these transactions and updates the global state of Ethereum based on the content of the transactions. The global state of Ethereum includes the balance information of all accounts, the code of smart contracts, and the data stored in smart contracts, etc. When a transaction is executed, it may transfer funds, call contracts, or change the storage state of contracts, all of which will cause changes in Ethereum's global state.
Therefore, for full-chain games, storing the state machine in the game engine is a necessary task. However, storing a state machine (especially a distributed state machine) in the ECS pattern poses a series of problems.
The problem of state machines in ECS: When developing games using ECS, there may be challenges regarding whether to store the state machine in ECS. This issue is much more complex than expected for various reasons. For example, changing the state of an entity, allowing an entity to participate in multiple state machines, retrieving the current state of an entity, changing the state list of the state machine, etc., all present challenges in ECS.
The problem of tagged states: An intuitive way to implement a state machine is to create a tag for each state. This method performs well when querying all entities in a given state, as ECS implementations typically excel at finding all entities with a given component/tag. However, other operations, such as changing states, require adding one tag and removing another, which may introduce complexity and performance issues.
Let’s analyze this specifically in the context of the Archetype data structure.
The implementation of Archetype is to gather all entities with the same component set into a single table. Avoiding specific details, this method facilitates efficient iteration caching of components since multiple components can be iterated continuously. However, this also comes at a cost.
To maintain the aggregation of entities with the same component set, every time a component is added or removed, entities need to be moved between different tables. This also applies to tags. Therefore, frequently adding or removing state-related tags may become very costly, as all components of the entity need to be copied each time the state changes (in fact, due to the storage method, each component needs to be copied twice).
To address the above issues, the authors of Bevy ECS proposed a new method for implementing storage variants: separating Archetype from tables. In this method, tables only store the (data) components of entities, while Archetype stores components + tags. Multiple Archetypes can point to the same table. With this, applications can add and remove tags (and states) in constant time, which is a significant improvement over having to copy all components.
Perhaps MUDv3 will improve in this direction; let’s see if it will add a new Archetype storage component outside of the table in the future.
Conclusion#
Now let's try to answer the initial question. The table is a modeling method for implementing “Archetype ECS.” MUDv2 has not abandoned ECS; it just happens that the table component can also implement OOP architecture. This table is generated through the Store component and stored in the EVM's Storage, so all read and write operations on the table will be converted into read and write operations on Storage. Although the ECS architecture faces many difficulties in storing the EVM state machine, there are still many ways to help solve these issues. If you are interested in these optimization methods, you can follow the author's Twitter (@hiCaptainZ) for the latest information. The author currently serves as Head of Research at Gametaverse (@GametaverseDAO), holds a Master's degree in Optical Engineering from the University of North Carolina and a Master's degree in Finance from the Chinese University of Hong Kong, and is currently researching Web3 Gaming, AI, and ZKP. The research topics expected in the coming weeks include:
- Comparative analysis of traditional game engines and Web3 game engines
- How to determine the playability of a game
- The rise of gambling games and GambleFi in Web3
- What are the composability aspects of full-chain games
- Economic system design for Web3 games
- How different game genres affect the design of Tokenomics
- Is the ECS architecture a paradigm (advantages and disadvantages)
- How to design an ECS from scratch
- How AI can assist in the production of chain games
- Overview of ZK zero-knowledge proofs across all tracks
- ZK technology and machine learning ZK-ML