Realm of Pepe:  Entity-Component-System

Photo by fabio on Unsplash

Realm of Pepe: Entity-Component-System

Designing Web3 Games with Solidity and the ECS Architecture

·

5 min read

I'm going to use the Realm of Pepe to explore a tangent topic to this project that I think is important.

If you got the opportunity to check MUD documentation, you will see ECS as a base to model your app architecture.

In this article, we are going to dig a bit more about Entity-Component-System.

The Entity-Component-System (ECS) architecture is particularly relevant to game development, where performance, simplicity, and flexibility are important.

  • Entity: An entity is a general-purpose object. Typically, it only consists of a unique identifier. They "tag every object" in the game setting. A "spaceship," "sword," or "player" can all be entities.

  • Component: Components are raw data for one aspect of the object, and how it interacts with the world. It can represent position, velocity, rendering, health, etc. They're data containers with no logic.

  • System: Systems provide the logic that processes entities that have a specific set of components. They define how entities behave and interact. Systems are where the actual "work" gets done.

The idea behind this pattern is the separation of data and logic. The logic (systems) operates on the raw data (components).

Composition vs Inheritance:

In ECS, an entity's behaviour and properties are determined by the components it's composed of, while in OOP, an object's behaviour and properties are determined by the class it's an instance of, and by inheritance hierarchy. This leads to greater flexibility in ECS, where you can easily compose different behaviours by attaching or detaching components to entities.

The benefit of ECS is its flexibility. An entity's behaviour can be modified simply by adding, removing, or modifying components. It also tends to be very efficient because systems can process entities with the same components in a batch.

Concurrency and Parallelism: ECS can better leverage multi-threading, as different systems can operate on different sets of components concurrently, assuming there are no dependencies between them.

Dynamic Entity Modification: In ECS, entities are mutable at runtime. Components can be added, changed or removed from an entity dynamically during the game. This allows a complex and varied entity behaviour and state.

I feel that ECS is a product of empirical development experience, not formalized as other architectural patterns but being a result of adapting existing languages and frameworks capacities to something that can scale. You will find some flavours of this pattern with a different approach and a lot of opinions about frameworks that use this pattern.

Can this work with Solidity?

Let's try to break down a partial ERC20 implementation to organize some concepts.

contract MyToken is ERC20 {
    // Components
    mapping (address => uint256) private _balances;
    mapping (address => mapping (address => uint256)) private _allowances;
    address private _owner;

    // ...omitted constructor 

    // Systems
    function transfer(address recipient, uint256 amount) public override returns (bool) {
        _transfer(_msgSender(), recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public override returns (bool) {
        _approve(_msgSender(), spender, amount);
        return true;
    }

    function _transfer(address sender, address recipient, uint256 amount) internal override {
        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");
        require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance");

        // Entity and its components interaction
        _balances[sender] -= amount;
        _balances[recipient] += amount;

        emit Transfer(sender, recipient, amount);
    }

    // ...additional code for mint, burn, allowance, etc.
}
  • Entities are represented by addresses.

  • Components are the balances, allowances, and the owner.

  • Systems are the functions that describe how the balances and allowances can change.

Wait... So ERC20 is using the ECS pattern? No, ECS is more about data-oriented design, where data (components) is processed by systems. In this case, ERC20 follow a more classical object-oriented pattern.
The contract encapsulates data (_balances, _allowances, etc) and behaviours (transfer, approve, etc) in a single unit as a contract.

With this organization in mind let's build a more game-themed toy example.

Entity

contract Entity {
    struct Player {
        uint health;
        uint strength;
        string location;
    }

    mapping(address => Player) public players;
}

Components

contract HealthComponent {
    mapping(address => uint256) public health;
}

contract StrengthComponent {
    mapping(address => uint256) public strength;
}

contract LocationComponent {
    mapping(address => string) public location;
}

System

import "./HealthComponent.sol";
import "./StrengthComponent.sol";
import "./LocationComponent.sol";

contract System {
    HealthComponent healthComponent;
    StrengthComponent strengthComponent;
    LocationComponent locationComponent;

    constructor(address _healthAddress, address _strengthAddress, address _locationAddress) {
        healthComponent = HealthComponent(_healthAddress);
        strengthComponent = StrengthComponent(_strengthAddress);
        locationComponent = LocationComponent(_locationAddress);
    }

    function dealDamage(address player, uint256 damage) public {
        uint currentHealth = healthComponent.health(player);
        healthComponent.health[player] = currentHealth - damage;
    }

    function movePlayer(address player, string memory newLocation) public {
        locationComponent.location[player] = newLocation;
    }

    // ... more functions to interact with player components
}

It's important to remember that Solidity and the Ethereum Virtual Machine (EVM) have limitations that make it difficult to implement ECS in the most efficient way possible. Due to the constraints in reading and writing data on the EVM, working with entities and components as separate contracts may have a significant gas overhead.

I'm not going to rephrase MUD/ECS documentation, but this is important to start working with MUD:

To model your data in an ECS-native way, represent every component as a table with a bytes32 key.

If you check our definition above about components is apparent that components are the data part. MUD abstracts this even more by creating a global database for all components.

From the docs, we can see how to create a table in storage layer.

import { mudConfig } from "@latticexyz/world/register";

export default mudConfig({
  tables: {
    PlayerComponent: "bool",
    PositionComponent: {
      schema: { x: "int32", y: "int32" },
    },
    NameComponent: "string",
    DamageComponent: uint256,
    HealthComponent: uint256,
  },
  modules: [
    {
      name: "UniqueEntityModule",
      root: true,
      args: [],
    },
  ],
});

This file will generate the necessary tables to support our Components. A nice feature is that the solidity part code is auto-generated. You can now start working on the Systems part (functions) and MUD will glue everything together from the blockchain to the frontend.

If you want to know more:

Mud
Unity Docs
SanderMertens/ecs-faq