Skip to content

Callbacks on state changes #6

Open

Description

@HexDecimal

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.

  • On component changes
  • On tag added/removed
  • On relation added/removed
  • On relation component changes
  • World-specific callbacks
  • Entity added/removed to compiled query

Activity

  1. self-assigned this
    on Aug 2, 2023
  2. HexDecimal commented on Oct 7, 2023

    @HexDecimal
    OwnerAuthor

    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.
  3. removed their assignment
    on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions