]> Untitled Git - axy/ft/pacman.git/commitdiff
Hierarchical transform propagation
author= <=>
Tue, 15 Sep 2026 12:48:09 +0000 (14:48 +0200)
committer= <=>
Tue, 15 Sep 2026 12:48:09 +0000 (14:48 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/hierarchy.py
src/pacman/ecs/world.py

index ebe82f343de65b106906a2131cbc7d2cec329f2e..5c2b3b5624f5929166dd535978792d5c50c02da0 100644 (file)
@@ -6,7 +6,7 @@ from typing import Any, Literal, cast, get_args, overload
 import pygame.sprite
 from pygame import Vector2
 
-from pacman.ecs.hierarchy import hierachy_plugins
+from pacman.ecs.hierarchy import Children, Parent, hierachy_plugins
 from pacman.ecs.schedule import (
     MainSchedule,
     Schedule,
@@ -27,20 +27,73 @@ def run_minimal_subschedules(world: World) -> None:
     world.tick(UpdateSchedule)
 
 
+PreUpdate = SystemSet("preupdate")
+Update = SystemSet("update")
+PostUpdate = SystemSet("postupdate")
+
+
 def schedule_plugins(world: World) -> None:
-    world.with_systems(MainSchedule, run_minimal_subschedules)
+    world.with_systems(MainSchedule, run_minimal_subschedules).with_systems(
+        UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain()
+    )
 
 
-def minimal_plugins(world: World) -> None:
-    world.with_plugins(schedule_plugins, hierachy_plugins)
+class CoordAbs2D:
+    vec: Vector2
+    depth: float
+
+    def __init__(self) -> None:
+        self.vec = Vector2(0.0)
+        self.depth = 0.0
 
 
 @dataclass
-class ScreenCoord2D:
+class Coord2D:
     vec: Vector2
     depth: float
 
 
+def update_coord(world: World) -> None:
+    for abs_coord, coord in world.query(tuple[CoordAbs2D, Coord2D]):
+        abs_coord.vec = coord.vec.copy()
+        abs_coord.depth = coord.depth
+
+
+def update_coord_hierarchy(world: World) -> None:
+    work = []
+    for (children,) in world.query(tuple[Children], without=tuple[Parent]):
+        work.extend(children.children)
+    while work:
+        curr = work.pop()
+        child = world[curr]
+        parent = world[world[curr][Parent].parent]
+        if CoordAbs2D in parent and CoordAbs2D in child:
+            parent_coord = parent[CoordAbs2D]
+            child_coord = child[CoordAbs2D]
+            child_coord.vec += parent_coord.vec
+            child_coord.depth += parent_coord.depth
+        if Children in child:
+            work.extend(child[Children].children)
+
+
+PropagateTransform = SystemSet("propagate-transform")
+
+
+def coord_update_plugins(world: World) -> None:
+    world.with_systems(
+        UpdateSchedule,
+        Systems(update_coord, update_coord_hierarchy)
+        .chain()
+        .in_set(PostUpdate, PropagateTransform),
+    )
+
+
+def minimal_plugins(world: World) -> None:
+    world.with_plugins(
+        schedule_plugins, hierachy_plugins, coord_update_plugins
+    )
+
+
 class ShouldRender:
     pass
 
@@ -83,7 +136,7 @@ def close_on_close(world: World) -> None:
 def render_sprite2d(world: World) -> None:
     window = world.res_s(Window)
     sprites_raw = sorted(
-        world[tuple[ScreenCoord2D, Sprite2D, ShouldRender]],
+        world[tuple[CoordAbs2D, Sprite2D, ShouldRender]],
         key=lambda e: e[0].depth,
         reverse=True,
     )
@@ -97,30 +150,57 @@ def render_sprite2d(world: World) -> None:
 def graphics_plugins(world: World) -> None:
     world.with_systems(StartupSchedule, init_window).with_systems(
         UpdateSchedule,
-        Systems(poll_events, close_on_close, render_sprite2d).chain(),
+        Systems(poll_events, close_on_close, render_sprite2d)
+        .chain()
+        .after(PropagateTransform),
     )
 
 
+class WiggleSprite:
+    pass
+
+
 def move_sprites(world: World) -> None:
     import math
 
     time = pygame.time.get_ticks() / 100
-    for (coord,) in world[tuple[ScreenCoord2D]]:
+    for coord, _ in world[tuple[Coord2D, WiggleSprite]]:
         pos = max(time - coord.depth * 0.3, 0.0)
         coord.vec.y = 500.0 + math.sin(pos) * 30.0
 
 
+class WiggleRoot:
+    pass
+
+
+def move_root(world: World) -> None:
+    import math
+
+    time = pygame.time.get_ticks() / 100
+    for coord, _ in world[tuple[Coord2D, WiggleRoot]]:
+        coord.vec.x = 30.0 + math.cos(time) * 30.0
+
+
 if __name__ == "__main__":
     world = (
         World()
         .with_plugins(minimal_plugins, graphics_plugins)
-        .with_systems(UpdateSchedule, move_sprites)
+        .with_systems(UpdateSchedule, move_sprites, move_root)
     )
     assets = list(Path("./asset").rglob("*.png"))
+    root = Entity()
+    world[root] = (
+        Coord2D(Vector2(100.0, 0.0), 0.0),
+        CoordAbs2D(),
+        WiggleRoot(),
+    )
     for i, asset in enumerate(assets):
         world[Entity()] = (
-            ScreenCoord2D(Vector2(30.0 * i, 500.0), 1.0 * i),
+            Coord2D(Vector2(30.0 * i, 500.0), 1.0 * i),
+            CoordAbs2D(),
             Sprite2D(pygame.image.load(asset)),
             ShouldRender(),
+            WiggleSprite(),
+            Parent(root),
         )
     world.run_main()
index af8ca09d1f8733f1a8354a4f3bb812f7df149dc8..9106a35a8a3c482e9918fc4fbbf6c5cd15aa3ac8 100644 (file)
@@ -40,8 +40,6 @@ def children_insert_hook(
         child[Parent] = Parent(entity)
         __no_hook = False
 
-        pass
-
 
 def children_remove_hook(
     world: World, _entity: Entity, component: Children
@@ -71,7 +69,7 @@ def parent_insert_hook(
     if __no_hook:
         return
     parent = world[component.parent]
-    if Parent not in parent:
+    if Children not in parent:
         __no_hook = True
         parent[Children] = Children((entity,))
         __no_hook = False
index 6ea3d439156bb8dabe0a59538465206defb5cf59..510715ec31d902d277efb73ec160f2eadd56efd6 100644 (file)
@@ -46,11 +46,21 @@ class EntityThunk:
             self.world._entities[self.entity].add(ty)
         if ty not in self.world._components:
             self.world._components[ty] = {}
+        if self.entity in self.world._components[ty]:
+            del self[ty]
         self.world._components[ty][self.entity] = val
+        if ty in self.world._insert_hooks:
+            self.world._insert_hooks[ty](self.world, self.entity, val)
 
     def __delitem__[T](self, ty: type[T]) -> None:
         if ty not in self.world._entities[self.entity]:
             return
+        if ty in self.world._remove_hooks:
+            self.world._remove_hooks[ty](
+                self.world,
+                self.entity,
+                self.world._components[ty][self.entity],
+            )
         self.world._entities[self.entity].remove(ty)
         del self.world._components[ty][self.entity]
 
@@ -79,7 +89,9 @@ class World:
             return
         sets: list[dict[Entity, Any]] = sorted(
             (
-                self._entities if arg is Entity else self._components[arg]
+                self._entities
+                if arg is Entity
+                else self._components.get(arg, {})
                 for arg in args
                 if arg is Entity or arg in self._components
             ),
@@ -88,7 +100,9 @@ class World:
         )
         neg_sets: list[dict[Entity, Any]] = sorted(
             (
-                self._entities if arg is Entity else self._components[arg]
+                self._entities
+                if arg is Entity
+                else self._components.get(arg, {})
                 for arg in neg_args
                 if arg is Entity or arg in self._components
             ),
@@ -193,11 +207,16 @@ class World:
             self.tick(schedule)
 
     def with_systems(
-        self, schedule: ScheduleLabel, *systems: "System | Systems"
+        self,
+        schedule: ScheduleLabel,
+        *systems: "System | Systems",
+        sets: set[SystemSet] | None = None,
     ) -> "World":
+        if sets is None:
+            sets = set()
         if schedule not in self._schedules:
             self._schedules[schedule] = Schedule()
-        Systems(*systems)._apply(self._schedules[schedule])
+        Systems(*systems).in_set(*sets)._apply(self._schedules[schedule])
         return self
 
     def with_hook[T](
@@ -210,7 +229,7 @@ class World:
             case "insert":
                 d = self._insert_hooks
             case "remove":
-                d = self._insert_hooks
+                d = self._remove_hooks
         if ty not in d:
             d[ty] = hook
         else: