Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7509: 实体组件系统

表示包含数据组件的实体,以及对实体组件进行操作的系统。

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

摘要

本提案定义了一个最小的实体组件系统 (ECS)。实体是被分配给多个组件(数据)然后使用系统(逻辑)处理的唯一标识。 本提案标准化了在智能合约中使用 ECS 的接口规范,提供了一组基本函数,允许用户自由组合和管理多合约应用程序。

动机

ECS 是一种通过将数据与行为分离来提高代码可重用性的设计模式。它通常用于游戏开发。一个最小的 ECS 包含 Entity: 一个唯一的标识符。 Component: 一个附加到实体的可重用数据容器。 System: 用于操作实体组件的逻辑。 World: 实体组件系统的容器。 本提案使用智能合约来实现一个易于使用的最小 ECS,消除了不必要的复杂性,并进行了一些与合约交互行为一致的功能改进。您可以轻松自由地组合组件和系统。 作为一名智能合约开发者,采用 ECS 的好处包括:

  • 它采用了简单的解耦、封装和模块化设计,使得您的游戏或应用程序的架构设计更加容易。
  • 它具有灵活的组合能力,每个实体可以组合不同的组件。您还可以定义不同的系统来操作这些新实体的数据。
  • 它有利于扩展,两个游戏或应用程序可以通过定义新的组件和系统进行交互。
  • 它可以帮助您的应用程序添加新功能或升级,因为数据和行为是分离的,新功能不会影响您的旧数据。
  • 它易于管理。当您的应用程序由多个合约组成时,它将帮助您有效地管理每个合约的状态。
  • 它的组件是可重用的,您可以与社区分享您的组件,以帮助他人提高开发效率。

规范

本文档中的关键词“必须”、“不得”、“必需”、“应”、“不应”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

World 合约是实体、组件合约和系统合约的容器。其核心原则是建立实体和组件合约之间的关系,其中不同的实体将附加不同的组件,并使用系统合约来动态更改组件中实体的数据。

构建基于 ECS 的程序的常用工作流程:

  1. 实现 IWorld 接口以创建一个 world 合约。
  2. 调用 world 合约的 createEntity() 来创建一个实体。
  3. 实现 IComponent 接口以创建一个 Component 合约。
  4. 调用 world 合约的 registerComponent() 来注册组件合约。
  5. 调用 world 合约的 addComponent() 将组件附加到实体。
  6. 创建一个系统合约,这是一个没有接口限制的合约,您可以在系统合约中定义任何函数。
  7. 调用 world 合约的 registerSystem() 来注册系统合约。
  8. 运行系统。

接口

IWorld.sol

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

interface IWorld {
    /**
     * 创建一个新的实体。
     * @dev 实体必须被分配一个唯一的 Id。
     * 如果实体的状态为 true,则表示它可用,如果为 false,则表示它不可用。
     * 当实体的状态为 false 时,您无法为实体添加或删除组件。
     * @return 新的实体 id。
     */
    function createEntity() external returns (uint256);

    /**
     * 实体是否存在于 world 中。
     * @param _entityId 是实体的 Id。
     * @return true 存在,false 不存在。
     */
    function entityExists(uint256 _entityId) external view returns (bool);

    /**
     * 获取 world 中实体的总数。
     * @return 实体的总数。
     */
    function getEntityCount() external view returns (uint256);

    /**
     * 设置实体的状态。
     * @dev 实体必须存在。
     * @param _entityId 是实体的 Id。
     * @param _entityState 是实体的状态,true 表示可用,false 表示不可用。
     */
    function setEntityState(uint256 _entityId, bool _entityState) external;

    /**
     * 获取实体的状态。
     * @param _entityId 实体的 Id。
     * @return 实体的当前状态。
     */
    function getEntityState(uint256 _entityId) external view returns (bool);

    /**
     * 向 world 注册一个组件。
     * @dev 组件必须先在 world 中注册才能附加到实体。
     * 不得重复向 world 注册同一个组件。
     * 应该检查组件合约的 world() 返回的合约地址是否与当前的 world 合约相同。
     * 组件的状态为 true 表示它可用,false 表示它不可用。当组件状态设置为 false 时,它不能附加到实体。
     * @param _componentAddress 是组件的合约地址。
     */
    function registerComponent(address _componentAddress) external;

    /**
     * 组件是否存在于 world 中。
     * @param _componentAddress 是组件的合约地址。
     * @return true 存在,false 不存在。
     */
    function componentExists(address _componentAddress)
        external
        view
        returns (bool);

    /**
     * 获取在 world 中注册的所有组件的合约地址。
     * @return 合约地址数组。
     */
    function getComponents() external view returns (address[] memory);

    /**
     * 设置组件状态。
     * @dev 组件必须存在。
     * @param _componentAddress 是组件的合约地址。
     * @param _componentState 是组件的状态,true 表示可用,false 表示不可用。
     */
    function setComponentState(address _componentAddress, bool _componentState)
        external;

    /**
     * 获取组件的状态。
     * @param _componentAddress 是组件的合约地址。
     * @return true 表示可用,false 表示不可用。
     */
    function getComponentState(address _componentAddress)
        external
        view
        returns (bool);

    /**
     * 将组件附加到实体。
     * @dev 实体必须可用。组件必须可用。一个组件不能重复添加到实体。
     * @param _entityId 是实体的 Id。
     * @param _componentAddress 是要附加的组件的地址。
     */
    function addComponent(uint256 _entityId, address _componentAddress)
        external;

    /**
     * 实体是否附加了组件。
     * @dev 实体必须存在。组件必须已注册。
     * @param _entityId 是实体的 Id。
     * @param _componentAddress 是组件地址。
     * @return true 已附加,false 未附加
     */
    function hasComponent(uint256 _entityId, address _componentAddress)
        external
        view
        returns (bool);

    /**
     * 从实体中移除组件。
     * @dev 实体必须可用。该组件必须在此之前已添加到实体。
     * @param _entityId 是实体的 Id。
     * @param _componentAddress 是要移除的组件的地址。
     */
    function removeComponent(uint256 _entityId, address _componentAddress)
        external;

    /**
     * 获取附加到实体的所有组件的合约地址。
     * @dev 实体必须存在。
     * @param _entityId 是实体的 Id。
     * @return 此实体拥有的组件的合约地址数组。
     */
    function getEntityComponents(uint256 _entityId)
        external
        view
        returns (address[] memory);

    /**
     * 向 world 注册一个系统。
     * @dev 不得重复向 world 注册同一个系统。系统状态为 true 表示可用,false 表示不可用。
     * @param _systemAddress 是系统的合约地址。
     */
    function registerSystem(address _systemAddress) external;

    /**
     * 系统是否存在于 world 中。
     * @param _systemAddress 是系统的合约地址。
     * @return true 存在,false 不存在。
     */
    function systemExists(address _systemAddress) external view returns (bool);

    /**
     * 获取在 world 中注册的所有系统的合约地址。
     * @return 合约地址数组。
     */
    function getSystems() external view returns (address[] memory);

    /**
     * 设置系统的状态。
     * @dev 系统必须存在。
     * @param _systemAddress 是系统的合约地址。
     * @param _systemState 是系统的状态。
     */
    function setSystemState(address _systemAddress, bool _systemState) external;

    /**
     * 获取系统的状态。
     * @param _systemAddress 是系统的合约地址。
     * @return 系统的状态。
     */
    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 {
    /**
     * 组件注册的 world 合约地址。
     * @return world 合约地址。
     */
    function world() external view returns (address);

    /**
     * 获取组件的数据类型和 get() 参数类型
     * @dev 应该导入 Types 库,这是一个包含所有数据类型的枚举库。
     * 实体数据可以根据数据类型存储。
     * get() 参数数据类型可用于获取实体数据。
     * @return 实体的数据类型数组
     * @return get 参数数据类型数组
     */
    function types()
        external
        view
        returns (Types.Type[] memory, Types.Type[] memory);

    /**
     * 存储实体数据。
     * @dev 实体必须可用。操作它的系统必须可用。
     * 实体附加了组件。
     * @param _entityId 是实体的 Id。
     * @param _data 是要存储的数据。
     */
    function set(uint256 _entityId, bytes memory _data) external;

    /**
     * 根据实体 Id 获取实体的数据。
     * @param _entityId 是实体的 Id。
     * @return 实体数据。
     */
    function get(uint256 _entityId) external view returns (bytes memory);

    /** 根据实体 Id 和参数获取实体的数据。
     * @param _entityId 是实体的 Id。
     * @param _params 是一个额外的参数,它应该取决于您是否需要它。
     * @return 实体数据。
     */
    function get(uint256 _entityId, bytes memory _params)
        external
        view
        returns (bytes memory);
}

Types.sol 包含上述接口中使用的 Solidity 类型的枚举。

理由

为什么包含类型信息而不是简单的 byte 数组?

这是为了确保使用组件时类型的正确性,以避免潜在的错误和不一致。外部开发者可以根据类型清晰地进行设置和获取。

为什么要区分不存在的实体和状态为 false 的实体?

我们不能仅根据状态来判断实体是否真正存在。外部贡献者可以基于实体创建组件。如果他使用的实体不存在,那么他创建的组件可能没有意义。组件创建者应该首先检查实体是否存在,如果实体确实存在,即使实体的状态为 false 也是有意义的。因为他可以等待实体状态变为 true,然后再将组件附加到实体。

为什么 getEntityComponents 函数返回所有组件的地址,而不是所有组件的 id?

getEntityComponents 有两种设计。另一种设计是为组件 id 和组件地址的存储添加一个额外的映射。每次我们调用 addComponent 时,函数的参数都是实体 id 和组件 id。当用户调用 getEntityComponents 时,它将返回一个组件 id 数组,他们使用每个组件 id 查询组件地址,然后基于每个组件地址查询数据。因为一个实体可能包含许多组件 id,这将导致用户多次请求组件地址。最后,我们选择直接使用 getEntityComponents 获取实体拥有的所有地址。

registerComponentregisterSystem 是否可以提供外部权限?

这取决于您的应用程序或游戏的开放性。如果您鼓励开发者参与,他们提交注册的组件和系统的状态应该是 false,并且您需要在使用 setComponentStatesetSystemState 启用它们之前检查它们是否提交了恶意代码。

何时在组件中使用带有额外参数的 get

该组件提供了两个 get 函数。一个 get 函数只需要传入实体 id,另一个有更多的 _params 参数,这将用作获取数据的附加参数。例如,您定义一个组件,该组件存储与实体的等级对应的 HP。如果您想获得与其等级匹配的实体的 HP,则可以调用 get 函数,并将实体等级作为 _params 传入。

参考实现

参见 Ethereum ECS 示例

安全考虑

除非您想实现特殊功能,否则不要直接向普通用户提供以下方法,它们应该由合约所有者设置。 createEntity(), setEntityState(), addComponent(), removeComponent(), registerComponent(), setComponentState(), registerSystem(), setSystemState()

不要在组件合约中提供除 set() 之外的修改实体的函数。并在 set() 中添加一个检查,以检查实体是否可用以及操作系统是否可用。

系统在 world 中注册后,将能够操作 world 中所有实体的组件数据。在将其注册到 world 之前,有必要检查和审计所有系统合约的代码安全性。

如果新版本已弃用某些实体、组件合约和系统合约,则需要使用 setEntityState()setComponentState()setSystemState() 及时禁用它们。

版权

CC0 下放弃版权和相关权利。

Citation

Please cite this document as:

Rickey (@HelloRickey), "ERC-7509: 实体组件系统 [DRAFT]," Ethereum Improvement Proposals, no. 7509, September 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7509.