Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7509: Entity Component System

Represent entities comprising components of data, with systems which operate on entities' components.

Authors Rickey (@HelloRickey)
Created 2023-09-05
Discussion Link https://ethereum-magicians.org/t/a-new-proposal-of-entity-component-system/15665

Abstract

This proposal defines a minimal Entity Component System (ECS). Entities are unique identities that are assigned to multiple components (data) and then processed using the system (logic). This proposal standardizes the interface specification for using ECS in smart contracts, providing a set of basic functions that allow users to freely combine and manage multi-contract applications.

Motivation

ECS is a design pattern that improves code reusability by separating data from behavior. It is often used in game development. A minimal ECS consists of
Entity: a unique identifier.
Component: a reusable data container attached to an entity.
System: the logic for operating entity components.
World: a container for an entity component system.
This proposal uses smart contracts to implement an easy-to-use minimal ECS, eliminates unnecessary complexity, and makes some functional improvements that are consistent with contract interaction behavior. You can combine components and systems easily and freely. As a smart contract developer, the benefits of adopting ECS include:

  • It adopts a simple design of decoupling, encapsulation, and modularization, which makes the architecture design of your game or application easier.
  • It has flexible composition ability, each entity can combine different components. You can also define different systems for manipulating the data of these new entities.
  • It is conducive to expansion, and two games or applications can interact by defining new components and systems.
  • It can help your application add new features or upgrades, because data and behavior are separated, new features will not affect your old data.
  • It is easy to manage. When your application consists of multiple contracts, it will help you effectively manage the status of each contract.
  • Its components are reusable, and you can share your components with the community to help others improve development efficiency.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

World contracts are containers for entities, component contracts, and system contracts. Its core principle is to establish the relationship between entities and component contracts, where different entities will attach different components, and use system contracts to dynamically change the data of the entity in the component.

Usual workflow when building ECS-based programs:

  1. Implement the IWorld interface to create a world contract.
  2. Call createEntity() of the world contract to create an entity.
  3. Implement the IComponent interface to create a Component contract.
  4. Call registerComponent() of the world contract to register the component contract.
  5. Call addComponent() of the world contract to attach the component to the entity.
  6. Create a system contract, which is a contract without interface restrictions, and you can define any function in the system contract.
  7. Call registerSystem() of the world contract to register the system contract.
  8. Run the system.

Interfaces

IWorld.sol

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0;

interface IWorld {
    /**
     * Create a new entity.
     * @dev The entity MUST be assigned a unique Id.
     * If the state of the entity is true, it means it is available, and if it is false, it means it is not available.
     * When the state of the entity is false, you cannot add or remove components for the entity.
     * @return New entity id.
     */
    function createEntity() external returns (uint256);

    /**
     * Does the entity exist in the world.
     * @param _entityId is the Id of the entity.
     * @return true exists, false does not exist.
     */
    function entityExists(uint256 _entityId) external view returns (bool);

    /**
     * Get the total number of entities in the world.
     * @return The total number of entities.
     */
    function getEntityCount() external view returns (uint256);

    /**
     * Set the state of an entity.
     * @dev Entity MUST exist.
     * @param _entityId is the Id of the entity.
     * @param _entityState is the state of the entity, true means available, false means unavailable.
     */
    function setEntityState(uint256 _entityId, bool _entityState) external;

    /**
     * Get the state of an entity.
     * @param _entityId Id of the entity.
     * @return The current state of the entity.
     */
    function getEntityState(uint256 _entityId) external view returns (bool);

    /**
     * Register a component to the world.
     * @dev A component MUST be registered with the world before it can be attached to an entity.
     * MUST NOT register the same component to the world repeatedly.
     * It SHOULD be checked that the contract address returned by world() of the component contract is the same as the current world contract.
     * The state of the component is true means it is available, and false means it is not available. When the component state is set to false, it cannot be attached to the entity.
     * @param _componentAddress is the contract address of the component.
     */
    function registerComponent(address _componentAddress) external;

    /**
     * Does the component exist in the world.
     * @param _componentAddress is the contract address of the component.
     * @return true exists, false does not exist.
     */
    function componentExists(address _componentAddress)
        external
        view
        returns (bool);

    /**
     * Get the contract addresses of all components registered in the world.
     * @return Array of contract addresses.
     */
    function getComponents() external view returns (address[] memory);

    /**
     * Set component state.
     * @dev Component MUST exist.
     * @param _componentAddress is the contract address of the component.
     * @param _componentState is the state of the component, true means available, false means unavailable.
     */
    function setComponentState(address _componentAddress, bool _componentState)
        external;

    /**
     * Get the state of a component.
     * @param _componentAddress is the contract address of the component.
     * @return true means available, false means unavailable.
     */
    function getComponentState(address _componentAddress)
        external
        view
        returns (bool);

    /**
     * Attach a component to the entity.
     * @dev Entity MUST be available.Component MUST be available.A component MUST NOT be added to an entity repeatedly.
     * @param _entityId is the Id of the entity.
     * @param _componentAddress is the address of the component to be attached.
     */
    function addComponent(uint256 _entityId, address _componentAddress)
        external;

    /**
     * Whether the entity has a component attached,
     * @dev Entity MUST exist.Component MUST be registered.
     * @param _entityId is the Id of the entity.
     * @param _componentAddress is the component address.
     * @return true is attached, false is not attached
     */
    function hasComponent(uint256 _entityId, address _componentAddress)
        external
        view
        returns (bool);

    /**
     * Remove a component from the entity.
     * @dev Entity MUST be available.The component MUST have been added to the entity before.
     * @param _entityId is the Id of the entity.
     * @param _componentAddress is the address of the component to be removed.
     */
    function removeComponent(uint256 _entityId, address _componentAddress)
        external;

    /**
     * Get the contract addresses of all components attached to the entity.
     * @dev Entity MUST exist.
     * @param _entityId is the Id of the entity.
     * @return An array of contract addresses of the components owned by this entity.
     */
    function getEntityComponents(uint256 _entityId)
        external
        view
        returns (address[] memory);

    /**
     * Register a system to the world.
     * @dev MUST NOT register the same system to the world repeatedly.The system state is true means available, false means unavailable.
     * @param _systemAddress is the contract address of the system.
     */
    function registerSystem(address _systemAddress) external;

    /**
     * Does the system exist in the world.
     * @param _systemAddress is the contract address of the system.
     * @return true exists, false does not exist.
     */
    function systemExists(address _systemAddress) external view returns (bool);

    /**
     * Get the contract addresses of all systems registered in the world.
     * @return Array of contract addresses.
     */
    function getSystems() external view returns (address[] memory);

    /**
     * Set the system State.
     * @dev System MUST exist.
     * @param _systemAddress is the contract address of the system.
     * @param _systemState is the state of the system.
     */
    function setSystemState(address _systemAddress, bool _systemState) external;

    /**
     * Get the state of a system.
     * @param _systemAddress is the contract address of the system.
     * @return The state of the system.
     */
    function getSystemState(address _systemAddress)
        external
        view
        returns (bool);
}

IComponent.sol

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0;
import "./Types.sol";

interface IComponent {
    /**
     * The world contract address registered by the component.
     * @return world contract address.
     */
    function world() external view returns (address);

    /**
     *Get the data type and get() parameter type of the component
     * @dev SHOULD Import Types Library, which is an enumeration Library containing all data types.
     * Entity data can be stored according to the data type.
     * The get() parameter data type can be used to get entity data.
     * @return the data type array of the entity
     * @return get parameter data type array
     */
    function types()
        external
        view
        returns (Types.Type[] memory, Types.Type[] memory);

    /**
     *Store entity data.
     * @dev entity MUST be available. The system that operates on it MUST be available.
     * The entity has the component attached.
     * @param _entityId is the Id of the entity.
     * @param _data is the data to be stored.
     */
    function set(uint256 _entityId, bytes memory _data) external;

    /**
     *Get the data of the entity according to the entity Id.
     * @param _entityId is the Id of the entity.
     * @return Entity data.
     */
    function get(uint256 _entityId) external view returns (bytes memory);

    /** Get the data of the entity according to the entity Id and parameters.
     * @param _entityId is the Id of the entity.
     * @param _params is an extra parameter, it SHOULD depend on whether you need it.
     * @return Entity data.
     */
    function get(uint256 _entityId, bytes memory _params)
        external
        view
        returns (bytes memory);
}

Library

The library Types.sol contains an enumeration of Solidity types used in the above interfaces.

Rationale

Why include type information instead of simple byte arrays?

This is to ensure the correctness of types when using components, in order to avoid potential errors and inconsistencies. External developers can clearly set and get based on the type.

Why differentiate between a non-existent entity and an entity with false state?

We cannot judge whether an entity actually exists based on its state alone. External contributors can create components based on entities. If the entities he uses don’t exist, the components he creates may not make sense. Component creators should first check if the entity exists, and if the entity does exist, it makes sense even if the entity’s state is false. Because he can wait for the entity state to be true before attaching the component to the entity.

Why getEntityComponents function returns all addresses of components instead of all component ids?

There are two designs for getEntityComponents. The other design is to add an additional mapping for the storage of component id and component address. Every time we call addComponent, the parameters of the function are the entity id and component id. When the user calls getEntityComponents, it will returning an array of component ids, they query the component address with each component id, and then query the data based on each component address. Because a entity may contain many component ids, this will cause the user to request the component address multiple times. In the end, we chose to use getEntityComponents directly for all addresses owned by the entity.

Can registerComponent and registerSystem provide external permissions?

It depends on the openness of your application or game. If you encourage developers to participate, the state of the component and system they submit for registration should be false, and you need to check whether they have submitted malicious code before using setComponentState and setSystemState to enable them .

When to use get with extra parameters in component?

The component provides two get functions. One get function only needs to pass in the entity id, and the other has more _params parameters, which will be used as additional parameters for obtaining data. For example, you define a component that stores the HP corresponding to the level of an entity. If you want to get the HP of an entity that matches its level, then you call the get function with the entity level as _params.

Reference Implementation

See Ethereum ECS Example

Security Considerations

Unless you want to implement special functions, do not provide the following methods directly to ordinary users, they should be set by the contract owner.
createEntity(), setEntityState(), addComponent(), removeComponent(), registerComponent(), setComponentState(), registerSystem(), setSystemState()

Do not provide functions that modify entities other than set() in the component contract. And add a check in set() to check whether the entity is available and whether the operating system is available.

After the system is registered in the world, it will be able to operate the component data of all entities in the world. It is necessary to check and audit the code security of all system contracts before registering it in the world.

If the new version has deprecated some entities, component contracts and system contracts. They need to be disabled in time using setEntityState(), setComponentState(), and setSystemState().

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Rickey (@HelloRickey), "ERC-7509: Entity Component System [DRAFT]," Ethereum Improvement Proposals, no. 7509, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7509.