Area2D

A trigger zone that fires body_entered / body_exited.

▶ Run in browser

Tags: 2d physics area trigger signals

A PhysicsBody2D shuttles left and right across the screen and repeatedly passes through a stationary Area2D sensor. The Area2D emits body_entered when the walker overlaps it and body_exited when it leaves; the demo recolours the zone while occupied and counts total entries.

What it demonstrates

  • Area2D as a broadphase sensor zone with a CollisionShape2D child (rectangle).

  • Connecting to the body_entered / body_exited signals (payload is the body).

  • A PhysicsBody2D inside a PhysicsRoot2D driven by velocity each fixed step.

  • Reacting to overlap state: recolour the zone, count entries, live HUD.

Controls: ESC - Quit

Source

  1"""Area2D: A trigger zone that fires body_entered / body_exited.
  2
  3A PhysicsBody2D shuttles left and right across the screen and repeatedly
  4passes through a stationary Area2D sensor. The Area2D emits body_entered when
  5the walker overlaps it and body_exited when it leaves; the demo recolours the
  6zone while occupied and counts total entries.
  7
  8# /// simvx
  9# tags = ["2d", "physics", "area", "trigger", "signals"]
 10# web = { root = "TriggerZoneDemo", width = 800, height = 600, responsive = true }
 11# ///
 12
 13## What it demonstrates
 14- Area2D as a broadphase sensor zone with a CollisionShape2D child (rectangle).
 15- Connecting to the body_entered / body_exited signals (payload is the body).
 16- A PhysicsBody2D inside a PhysicsRoot2D driven by velocity each fixed step.
 17- Reacting to overlap state: recolour the zone, count entries, live HUD.
 18
 19Controls:
 20  ESC - Quit
 21"""
 22
 23from simvx.core import (
 24    Area2D,
 25    BodyMode,
 26    CircleShape2D,
 27    CollisionShape2D,
 28    Input,
 29    InputMap,
 30    Key,
 31    Node2D,
 32    PhysicsBody2D,
 33    RectangleShape2D,
 34    Vec2,
 35)
 36from simvx.core.physics.root import PhysicsRoot2D
 37from simvx.graphics import App
 38
 39WIDTH, HEIGHT = 800, 600
 40ZONE = Vec2(WIDTH / 2, HEIGHT / 2)
 41ZONE_HALF = Vec2(90, 130)  # half-extents of the rectangular sensor
 42WALKER_R = 22.0
 43WALK_SPEED = 260.0
 44
 45
 46class TriggerZoneDemo(Node2D):
 47    """A PhysicsBody2D shuttling through an Area2D sensor zone."""
 48
 49    def on_ready(self):
 50        InputMap.add_action("quit", [Key.ESCAPE])
 51
 52        self._entry_count = 0
 53        self._inside = False
 54
 55        # One isolated 2D world (gravity off; the walker rides a horizontal line).
 56        # No Camera2D: the demo draws in screen pixels via on_draw, as before.
 57        self._root = self.add_child(PhysicsRoot2D(name="World", gravity=Vec2(0, 0)))
 58
 59        # Sensor zone: an Area2D with a rectangular CollisionShape2D child. The
 60        # broadphase detects overlapping bodies each fixed step. Build the shape
 61        # child BEFORE adding the area to the world so it registers on enter.
 62        self._zone = Area2D(name="Zone", position=Vec2(ZONE.x, ZONE.y))
 63        self._zone.add_child(CollisionShape2D(name="ZoneShape", shape=RectangleShape2D(half_extents=ZONE_HALF)))
 64        self._zone.body_entered.connect(self._on_body_entered)
 65        self._zone.body_exited.connect(self._on_body_exited)
 66        self._root.add_child(self._zone)
 67
 68        # The walker: a DYNAMIC PhysicsBody2D the broadphase can see, moved by
 69        # velocity. Its collider child is added before it enters the world.
 70        self._walker = PhysicsBody2D(name="Walker", mode=BodyMode.DYNAMIC, mass=1.0, position=Vec2(120, ZONE.y))
 71        self._walker.add_child(CollisionShape2D(shape=CircleShape2D(WALKER_R)))
 72        self._root.add_child(self._walker)
 73        self._walker.velocity = Vec2(WALK_SPEED, 0)
 74
 75    def _on_body_entered(self, body):
 76        # Payload is the body that entered. Count it and flag occupancy.
 77        self._entry_count += 1
 78        self._inside = True
 79
 80    def _on_body_exited(self, body):
 81        self._inside = False
 82
 83    def on_update(self, dt: float):
 84        if Input.is_action_just_pressed("quit"):
 85            self.app.quit()
 86
 87    def on_fixed_update(self, dt: float):
 88        # Bounce off the side walls so the walker keeps crossing the zone.
 89        p = self._walker.world_position
 90        if p.x < WALKER_R and self._walker.velocity.x < 0:
 91            self._walker.velocity = Vec2(WALK_SPEED, 0)
 92        elif p.x > WIDTH - WALKER_R and self._walker.velocity.x > 0:
 93            self._walker.velocity = Vec2(-WALK_SPEED, 0)
 94
 95    def on_draw(self, renderer):
 96        # Zone: green when occupied, blue when empty.
 97        zx, zy = self._zone.position.x, self._zone.position.y
 98        top_left = (zx - ZONE_HALF.x, zy - ZONE_HALF.y)
 99        size = (ZONE_HALF.x * 2, ZONE_HALF.y * 2)
100        fill = (0.2, 0.7, 0.3, 0.45) if self._inside else (0.2, 0.45, 0.9, 0.35)
101        renderer.draw_rect(top_left, size, colour=fill, filled=True)
102        renderer.draw_rect(top_left, size, colour=(0.85, 0.9, 1.0, 1.0), filled=False)
103
104        # Walker.
105        wp = self._walker.world_position
106        renderer.draw_circle((wp.x, wp.y), WALKER_R, colour=(1.0, 0.75, 0.2, 1.0), filled=True)
107
108        # HUD.
109        renderer.draw_text("Area2D Trigger Zone", (10, 10), colour=(1.0, 1.0, 1.0), scale=2)
110        state = "INSIDE" if self._inside else "outside"
111        renderer.draw_text(f"Walker: {state}   Entries: {self._entry_count}", (10, 40), colour=(0.75, 0.75, 0.75))
112        renderer.draw_text("ESC: quit", (10, HEIGHT - 28), colour=(0.6, 0.6, 0.6))
113
114
115if __name__ == "__main__":
116    App(title="Area2D Trigger Zone", width=WIDTH, height=HEIGHT).run(TriggerZoneDemo())