ERC-7509: 实体组件系统
表示包含数据组件的实体,以及对实体组件进行操作的系统。
Authors | Rickey (@HelloRickey) |
---|---|
Created | 2023-09-05 |
Discussion Link | https://ethereum-magicians.org/t/a-new-proposal-of-entity-component-system/15665 |
Table of Contents
摘要
本提案定义了一个最小的实体组件系统 (ECS)。实体是被分配给多个组件(数据)然后使用系统(逻辑)处理的唯一标识。 本提案标准化了在智能合约中使用 ECS 的接口规范,提供了一组基本函数,允许用户自由组合和管理多合约应用程序。
动机
ECS 是一种通过将数据与行为分离来提高代码可重用性的设计模式。它通常用于游戏开发。一个最小的 ECS 包含 Entity: 一个唯一的标识符。 Component: 一个附加到实体的可重用数据容器。 System: 用于操作实体组件的逻辑。 World: 实体组件系统的容器。 本提案使用智能合约来实现一个易于使用的最小 ECS,消除了不必要的复杂性,并进行了一些与合约交互行为一致的功能改进。您可以轻松自由地组合组件和系统。 作为一名智能合约开发者,采用 ECS 的好处包括:
- 它采用了简单的解耦、封装和模块化设计,使得您的游戏或应用程序的架构设计更加容易。
- 它具有灵活的组合能力,每个实体可以组合不同的组件。您还可以定义不同的系统来操作这些新实体的数据。
- 它有利于扩展,两个游戏或应用程序可以通过定义新的组件和系统进行交互。
- 它可以帮助您的应用程序添加新功能或升级,因为数据和行为是分离的,新功能不会影响您的旧数据。
- 它易于管理。当您的应用程序由多个合约组成时,它将帮助您有效地管理每个合约的状态。
- 它的组件是可重用的,您可以与社区分享您的组件,以帮助他人提高开发效率。
规范
本文档中的关键词“必须”、“不得”、“必需”、“应”、“不应”、“应该”、“不应该”、“推荐”、“不推荐”、“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。
World 合约是实体、组件合约和系统合约的容器。其核心原则是建立实体和组件合约之间的关系,其中不同的实体将附加不同的组件,并使用系统合约来动态更改组件中实体的数据。
构建基于 ECS 的程序的常用工作流程:
- 实现
IWorld
接口以创建一个 world 合约。 - 调用 world 合约的
createEntity()
来创建一个实体。 - 实现
IComponent
接口以创建一个 Component 合约。 - 调用 world 合约的
registerComponent()
来注册组件合约。 - 调用 world 合约的
addComponent()
将组件附加到实体。 - 创建一个系统合约,这是一个没有接口限制的合约,您可以在系统合约中定义任何函数。
- 调用 world 合约的
registerSystem()
来注册系统合约。 - 运行系统。
接口
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
获取实体拥有的所有地址。
registerComponent
和 registerSystem
是否可以提供外部权限?
这取决于您的应用程序或游戏的开放性。如果您鼓励开发者参与,他们提交注册的组件和系统的状态应该是 false
,并且您需要在使用 setComponentState
和 setSystemState
启用它们之前检查它们是否提交了恶意代码。
何时在组件中使用带有额外参数的 get
?
该组件提供了两个 get
函数。一个 get
函数只需要传入实体 id,另一个有更多的 _params
参数,这将用作获取数据的附加参数。例如,您定义一个组件,该组件存储与实体的等级对应的 HP。如果您想获得与其等级匹配的实体的 HP,则可以调用 get
函数,并将实体等级作为 _params
传入。
参考实现
安全考虑
除非您想实现特殊功能,否则不要直接向普通用户提供以下方法,它们应该由合约所有者设置。
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.