跳转到主要内容

基于Python字典和集合的带类型提示的实体组件系统。

项目描述

关于

PyPI PyPI - License Documentation Status codecov CommitsSinceLastRelease

tcod-ecs 是使用Python的 dictset 类型实现的 稀疏集合 实体组件系统。有关更多信息,请参阅 ECS FAQ

此实现侧重于类型提示、组织,并设计为与Python很好地协同工作。目前实现以下功能

  • 实体可以存储任何Python对象的实例作为组件。通过类型查找组件。
  • 实体可以有一个类型的实例,或者使用可哈希的标记来区分它们,可以存储多个类型的实例。
  • 支持实体关系,可以是多对多或多对一关系。
  • 可以执行ECS查询以获取具有组件/标记/关系的组合或排除这些的实体。
  • 可以使用Python的pickle模块序列化ECS注册对象,以便轻松存储。

存在一个仅实现实体-组件框架的轻量级版本,称为 tcod-ectcod-ec 适用于动态类型字典风格的语法,并且缺少许多重要功能,如查询和命名组件。

安装

使用pip安装此库

pip install tcod-ecs

如果已安装 tcod 且版本低于 14.0.0,则 import tcod.ecs 将失败。删除或更新 tcod 以修复此问题。

示例

注册表

实体组件系统(ECS)注册表用于创建和存储实体及其组件。

>>> import tcod.ecs
>>> registry = tcod.ecs.Registry()  # New empty registry

实体

每个实体都通过其唯一的id(uid)来标识,这可以是任何可哈希的对象与所属的 registry 结合。可以通过 Registry.new_entity 创建新的唯一实体,它使用新的 object() 作为 uid,这保证了唯一性,但并不总是希望这样。实体始终了解其分配的注册表,可以通过任何实体实例的 Entity.registry 属性访问。注册表只在实体被分配名称、组件、标记或关系后才知道其实体。

>>> entity = registry.new_entity()  # Creates a unique entity using `object()` as the uid
>>> entity
<Entity(uid=object at ...)>
>>> entity.registry is registry  # Registry can always be accessed from their entity
True
>>> registry[entity.uid] is entity  # Entities with the same registry/uid are compared using `is`
True

# Reference an entity with the given uid, can be any hashable object:
>>> entity = registry["MyEntity"]
>>> entity
<Entity(uid='MyEntity')>
>>> registry["MyEntity"] is entity  # Matching entities ALWAYS share a single identity
True

使用 Registry.new_entity 创建唯一实体,并使用 Registry[x] 引用具有id的全局实体或关系。当您想在注册表中本身存储组件时,推荐使用 registry[None] 作为全局实体。

请勿将实体的 uid 保存用于以后使用 registry[uid],此过程比保留实体实例要慢。

序列化

注册表是正常的Python对象,只要所有存储的组件都是可序列化的,就可以进行序列化。

>>> import pickle
>>> pickled_data: bytes = pickle.dumps(registry)
>>> registry = pickle.loads(pickled_data)

稳定性是首要任务,但变更仍可能破坏旧版本的保存。向后兼容性不是首要任务,不应使用库的旧版本来反序列化序列化的注册表。此项目遵循 语义版本控制,主版本号的增加将破坏API、保存格式或两者,次要版本号的增加可能破坏向后兼容性。请查看 变更日志 以了解格式更改和中断。格式中断之前应始终有一个过渡期,因此保持最新版本是一个好主意。

组件

组件是任何Python类型的实例。可以通过类似于字典的 Entity.components 属性从实体中访问、分配或删除组件。类型用作访问组件的键。使用的类型可以是自定义类或标准Python类型。

>>> import attrs
>>> entity = registry.new_entity()
>>> entity.components[int] = 42
>>> entity.components[int]
42
>>> int in entity.components
True
>>> del entity.components[int]
>>> entity.components[int]  # Missing keys raise KeyError
Traceback (most recent call last):
  ...
KeyError: <class 'int'>
>>> entity.components.get(int, "default")  # Test keys with `.get()` like a dictionary
'default'
>>> @attrs.define
... class Vector2:
...     x: int = 0
...     y: int = 0
>>> entity.components[Vector2] = Vector2(1, 2)
>>> entity.components[Vector2]
Vector2(x=1, y=2)
>>> entity.components |= {int: 11, Vector2: Vector2(0, 0)}  # Multiple values can be assigned like a dict
>>> entity.components[int]
11
>>> entity.components[Vector2]
Vector2(x=0, y=0)

# Queries can be made on all entities of a registry with matching components
>>> for e in registry.Q.all_of(components=[Vector2]):
...     e.components[Vector2].x += 10
>>> entity.components[Vector2]
Vector2(x=10, y=0)

# You can match components and iterate over them at the same time.  This can be combined with the above
>>> for pos, i in registry.Q[Vector2, int]:
...     print((pos, i))
(Vector2(x=10, y=0), 11)

# You can include `Entity` to iterate over entities with their components
# This always iterates over the entity itself instead of an Entity component
>>> for e, pos, i in registry.Q[tcod.ecs.Entity, Vector2, int]:
...     print((e, pos, i))
(<Entity...>, Vector2(x=10, y=0), 11)

命名组件

如果没有为组件指定唯一的名称,则只能分配一个组件。可以在分配组件时使用键语法 (name, type) 为组件命名。名称不仅限于字符串,它们是标签等效物,可以是任何可哈希或冻结对象。在所有接受组件键的地方可以使用 [type][(name, type)] 语法互换。对组件的查询使用相同的语法访问命名组件,并且必须显式使用名称。

>>> entity = registry.new_entity()
>>> entity.components[Vector2] = Vector2(0, 0)
>>> entity.components[("velocity", Vector2)] = Vector2(1, 1)
>>> entity.components[("velocity", Vector2)]
Vector2(x=1, y=1)
>>> @attrs.define(frozen=True)
... class Slot:
...     index: int
>>> entity.components |= {  # Like a dict Entity.components can use |= to update items in-place
...     ("hp", int): 10,
...     ("max_hp", int): 12,
...     ("atk", int): 1,
...     str: "foo",
...     (Slot(1), str): "empty",
... }
>>> entity.components[("hp", int)]
10
>>> entity.components[str]
'foo'
>>> entity.components[(Slot(1), str)]
'empty'

# Queries can be made on all named components with the same syntax as normal ones
>>> for e in registry.Q.all_of(components=[("hp", int), ("max_hp", int)]):
...     e.components[("hp", int)] = e.components[("max_hp", int)]
>>> entity.components[("hp", int)]
12
>>> for e, pos, delta in registry.Q[tcod.ecs.Entity, Vector2, ("velocity", Vector2)]:
...     e.components[Vector2] = Vector2(pos.x + delta.x, pos.y + delta.y)
>>> entity.components[Vector2]
Vector2(x=1, y=1)

标记

标记是存储在类似于集合的 Entity.tags 中的可哈希对象。这些作为标志或用于将实体分组很有用。

>>> entity = registry.new_entity()
>>> entity.tags.add("player")  # Works well for groups
>>> "player" in entity.tags
True
>>> entity.tags.add(("eats", "fruit"))
>>> entity.tags.add(("eats", "meat"))
>>> set(registry.Q.all_of(tags=["player"])) == {entity}
True

关系

实体关系是从一个源实体到可能多个目标实体的单向关系。

  • 使用 origin.relation_tag[tag] = target 将源实体的标记唯一地关联到目标实体。这使用标准的赋值,对于没有多个目标的情况很有用。读取 origin.relation_tag[tag] 返回单个目标,同时强制只有一个目标的不变性质。
  • 使用 origin.relation_tags_many[tag].add(target) 来将一个标签与多个目标关联。这支持类似于 set 的语法,例如一次添加或删除多个目标。这允许多对多关系。
  • 使用 origin.relation_components[component_key][target] = component 来将一个目标实体与一个组件关联。这允许在关系中存储数据。这支持类似于 dict 的语法。可以像查询普通标签一样查询 component_key

关系查询

使用 registry.Q.all_of(relations=[...]) 来查询关系。这期望遵循以下规则的 2 项或 3 项元组

  • 使用 (tag, target) 来匹配具有关系 tag 的源实体与 target
  • 如果 tag 是组件键,则也会匹配组件关系。这意味着你应该小心看起来像组件键的标签。
  • target 可以是特定的实体。这意味着只有与该特定实体相关的实体会被匹配。
  • target 可以是查询本身。这意味着只有与子查询匹配的实体会被匹配。
  • target 可以是 ...,这意味着匹配与任何实体有关联的实体。
  • 要反转方向,请使用 3 项元组 (origin, tag, None)origin 可以是任何 target 可以是的内容。

使用子查询的关系可以链接在一起。参见 Sander Mertens - Why it is time to start thinking of games as databases 了解这一点的影响。

您可以使用以下表格来帮助构建关系查询

匹配 语法
具有关系 tagtarget_entity 的源实体 (tag, target_entity)
具有关系 tag 到任何目标实体的源实体 (tag, ...) (实际的省略号)
具有关系 tag 到匹配子查询的任何目标的源实体 (tag, registry.Q.all_of(...))
origin_entity 出发的关系 tag 的目标 (origin_entity, tag, None)
从任何源实体出发的关系 tag 的目标 (..., tag, None) (实际的省略号)
从匹配子查询的任何源实体出发的关系 tag 的目标 (registry.Q.all_of(...), tag, None)
>>> @attrs.define
... class OrbitOf:  # OrbitOf component
...     dist: int
>>> LandedOn = "LandedOn"  # LandedOn tag
>>> star = registry.new_entity()
>>> planet = registry.new_entity()
>>> moon = registry.new_entity()
>>> ship = registry.new_entity()
>>> player = registry.new_entity()
>>> moon_rock = registry.new_entity()
>>> planet.relation_components[OrbitOf][star] = OrbitOf(dist=1000)
>>> moon.relation_components[OrbitOf][planet] = OrbitOf(dist=10)
>>> ship.relation_tag[LandedOn] = moon
>>> moon_rock.relation_tag[LandedOn] = moon
>>> player.relation_tag[LandedOn] = moon_rock
>>> set(registry.Q.all_of(relations=[(OrbitOf, planet)])) == {moon}
True
>>> set(registry.Q.all_of(relations=[(OrbitOf, ...)])) == {planet, moon}  # Get objects in an orbit
True
>>> set(registry.Q.all_of(relations=[(..., OrbitOf, None)])) == {star, planet}  # Get objects being orbited
True
>>> set(registry.Q.all_of(relations=[(LandedOn, ...)])) == {ship, moon_rock, player}
True
>>> set(registry.Q.all_of(relations=[(LandedOn, ...)]).none_of(relations=[(LandedOn, moon)])) == {player}
True

项目详情


下载文件

下载您平台的文件。如果您不确定选择哪个,请了解有关 安装软件包 的更多信息。

源分布

tcod_ecs-5.2.3.tar.gz (27.2 kB 查看散列值)

上传时间

构建分布

tcod_ecs-5.2.3-py3-none-any.whl (25.9 kB 查看散列值)

上传于 Python 3

由以下组织支持