python-tcod-ecs icon indicating copy to clipboard operation
python-tcod-ecs copied to clipboard

Callbacks on state changes

Open HexDecimal opened this issue 2 years ago • 1 comments

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

HexDecimal avatar Jun 28 '23 18:06 HexDecimal

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.

HexDecimal avatar Oct 07 '23 09:10 HexDecimal