基于Python字典和集合的带类型提示的实体组件系统。
项目描述
关于
tcod-ecs
是使用Python的 dict
和 set
类型实现的 稀疏集合 实体组件系统。有关更多信息,请参阅 ECS FAQ。
此实现侧重于类型提示、组织,并设计为与Python很好地协同工作。目前实现以下功能
- 实体可以存储任何Python对象的实例作为组件。通过类型查找组件。
- 实体可以有一个类型的实例,或者使用可哈希的标记来区分它们,可以存储多个类型的实例。
- 支持实体关系,可以是多对多或多对一关系。
- 可以执行ECS查询以获取具有组件/标记/关系的组合或排除这些的实体。
- 可以使用Python的pickle模块序列化ECS注册对象,以便轻松存储。
存在一个仅实现实体-组件框架的轻量级版本,称为 tcod-ec。 tcod-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 了解这一点的影响。
您可以使用以下表格来帮助构建关系查询
匹配 | 语法 |
---|---|
具有关系 tag 到 target_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
项目详情
下载文件
下载您平台的文件。如果您不确定选择哪个,请了解有关 安装软件包 的更多信息。