Package: tcod-ecs
Abouttcod-ecsis aSparse-setEntity-component-systemimplemented using Python'sdictandsettypes.
See theECS FAQfor more info.This implementation focuses on type-hinting, organization, and is designed to work well with Python.
The following features are currently implemented:Entities can store components which are instances of any Python object. Components are looked up by their type.Entities can have one instance of a type, or multiple instances of a type using a hashable tag to differentiate them.Entity relationships are supported, either as many-to-many or many-to-one relationships.ECS Queries can be made to fetch entities having a combination of components/tags/relations or excluding such.The ECS Registry object can be serialized with Python's pickle module for easy storage.A lightweight version which implements only the entity-component framework exists calledtcod-ec.tcod-ecwas geared towards a dynamic-typed-dict style of syntax and is missing a lot of important features such as queries and named components.InstallationUse pip to install this library:pip install tcod-ecsIftcodis installed and the version is less than14.0.0thenimport tcod.ecswill fail.
Remove or updatetcodto fix this issue.ExamplesRegistryThe ECS Registry is used to create and store entities and their components.>>>importtcod.ecs>>>registry=tcod.ecs.Registry()# New empty registryEntityEach Entity is identified by its unique id (uid) which can be any hashable object combined with theregistryit belongs.
New unique entities can be created withRegistry.new_entitywhich uses a newobject()as theuid, this guarantees uniqueness which is not always desireable.
An entity always knows about its assigned registry, which can be accessed with theEntity.registryproperty from any Entity instance.
Registries only know about their entities once the entity is assigned a name, component, tag, or relation.>>>entity=registry.new_entity()# Creates a unique entity using `object()` as the uid>>>entity<Entity(uid=objectat...)>>>>entity.registryisregistry# Registry can always be accessed from their entityTrue>>>registry[entity.uid]isentity# 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"]isentity# Matching entities ALWAYS share a single identityTrueUseRegistry.new_entityto create unique entities and useRegistry[x]to reference a global entity or relation with an id.registry[None]is recommend for use as a global entity when you want to store components in the registry itself.Do not save theuid's of entities to be used later withregistry[uid], this process is slower than holding onto the Entity instance.SerializationRegistries are normal Python objects and can be pickled as long as all stored components are pickleable.>>>importpickle>>>pickled_data:bytes=pickle.dumps(registry)>>>registry=pickle.loads(pickled_data)Stability is a priority but changes may still break older saves.
Backwards compatibility is not a priority, pickled registries should not be unpickled with an older version of the library.
This project followsSemantic Versioning, major version increments will break the API, the save format or both, minor version increments may break backwards compatibility.
Check thechangelogto be aware of format changes and breaks.
There should always be a transition period before a format break, so keeping up with the latest version is a good idea.ComponentsComponents are instances of any Python type.
These can be accessed, assigned, or removed from entities via the dict-likeEntity.componentsattribute.
The type is used as the key to access the component.
The types used can be custom classes or standard Python types.>>>importattrs>>>entity=registry.new_entity()>>>entity.components[int]=42>>>entity.components[int]42>>>intinentity.componentsTrue>>>delentity.components[int]>>>entity.components[int]# Missing keys raise KeyErrorTraceback(mostrecentcalllast):...KeyError:<class'int'>>>>entity.components.get(int,"default")# Test keys with `.get()` like a dictionary'default'>>>@attrs.define...classVector2:...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>>>foreinregistry.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>>>forpos,iinregistry.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>>>fore,pos,iinregistry.Q[tcod.ecs.Entity,Vector2,int]:...print((e,pos,i))(<Entity...>,Vector2(x=10,y=0),11)Named ComponentsOnly one component can be assigned unless that component is given a unique name.
You can name components with the key syntax(name, type)when assigning components.
Names are not limited to strings, they are a tag equivalent and can be any hashable or frozen object.
The syntax[type]and[(name, type)]can be used interchangeably in all places accepting a component key.
Queries on components access named components with the same syntax and must use names explicitly.>>>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)...classSlot:...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>>>foreinregistry.Q.all_of(components=[("hp",int),("max_hp",int)]):...e.components[("hp",int)]=e.components[("max_hp",int)]>>>entity.components[("hp",int)]12>>>fore,pos,deltainregistry.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)TagsTags are hashable objects stored in the set-likeEntity.tags.
These are useful as flags or to group entities together.>>>entity=registry.new_entity()>>>entity.tags.add("player")# Works well for groups>>>"player"inentity.tagsTrue>>>entity.tags.add(("eats","fruit"))>>>entity.tags.add(("eats","meat"))>>>set(registry.Q.all_of(tags=["player"]))=={entity}TrueRelationsUseEntity.relation_components[component_key][target] = componentto associate a target entity with a component.
UseEntity.relation_tag[tag] = targetto associate a tag exclusively with a target entity.
UseEntity.relation_tags_many[tag].add(target)to associate a tag with multiple targets.Relation queries are a little more complex than other queries.
Relation tags and relation components share the same space then queried, so 'normal' tags should not be in the format of a component key.
Relations are unidirectional, but you can query either end of a relation.>>>@attrs.define...classOrbitOf:# 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 orbitTrue>>>set(registry.Q.all_of(relations=[(...,OrbitOf,None)]))=={star,planet}# Get objects being orbitedTrue>>>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}TrueRelation queriesYou can use the following table to help with constructing relation queries.tagis a component key if you are querying for a component relation.IncludesSyntaxEntities with a relation tag to the given target(tag, target_entity)Entities with a relation tag to any target(tag, ...)(Literal dot-dot-dot)Entities with a relation tag to the targets in the given query(tag, registry.Q.all_of(...))The target entities of a relation of a given entity(origin_entity, tag, None)The target entities of any entity with the given relation tag(..., tag, None)(Literal dot-dot-dot)The target entities of the queried entities with the given relation(tag, registry.Q.all_of(...))