python-tcod-ecs
python-tcod-ecs copied to clipboard
Callbacks on state changes
There should be support for callbacks on specific state changes such as a component being added or removed:
@attrs.define(frozen=True) # Class is frozen to ensure that a changed value is announced
class Position:
x: int
y: int
@tcod.ecs.register_on_component_changed(Position)
def on_changed_position(entity: Entity, old_pos: Position | None, new_pos: Position | None) -> None:
"""Check for Position changes in all worlds."""
if old_pos is not None and new_pos is not None:
print(f"Changed from {old_pos} to {new_pos}")
elif old_pos is None and new_pos is not None:
print(f"{new_pos} added")
else:
assert old_pos is not None and new_pos is None
print(f"{old_pos} removed")
This is global, there should also be a per-world variant.
These types of callbacks can also be used to manage compiled queries in #3
Would also need to add callbacks for tags and relations.
These should probably not be called during unpickling.
- [x] On component changes
- [ ] On tag added/removed
- [ ] On relation added/removed
- [ ] On relation component changes
- [ ] World-specific callbacks
- [ ] Entity added/removed to compiled query
Note that tcod.ecs.callbacks.register_component_changed is implemented in the current releases. This can already be used for some tricks such as tracking the spatial position of entities dynamically, such as with the following example:
@attrs.define(frozen=True) # Class is frozen to ensure that a changed value is announced
class Position:
"""Tile position of an entity."""
x: int
y: int
@attrs.define(frozen=True)
class MapChunkPosition:
"""Map chunk index of an entity."""
x: int
y: int
CHUNK_SIZE = 32
@tcod.ecs.callbacks.register_component_changed(component=Position)
def on_position_changed(entity: Entity, old: Position | None, new: Position | None) -> None:
"""Track the Position component as a tag in entities. Update the map chunk position."""
if old == new:
return # Reduce cache invalidation by not adding and removing the same tag
if old is not None:
entity.tags.remove(old)
if new is not None:
entity.tags.add(new)
entity.components[MapChunkPosition] = MapChunkPosition(new.x // CHUNK_SIZE , new.y // CHUNK_SIZE)
else: # new is None
del entity.components[MapChunkPosition]
@tcod.ecs.callbacks.register_component_changed(component=MapChunkPosition)
def on_chunk_position_changed(entity: Entity, old: MapChunkPosition | None, new: MapChunkPosition | None) -> None:
"""Track the MapChunkPosition component as a tag in entities."""
if old == new:
return
if old is not None:
entity.tags.remove(old)
if new is not None:
entity.tags.add(new)
With this setup you can query entities by an exact or approximate position.
world.Q.all_of(tags=[Position(0, 0)]) # Query all entities at an exact position.
world.Q.all_of(tags=[MapChunkPosition(0, 0)]) # Query all entities in a more general area.