Area2D

a trigger zone that reports bodies entering and leaving.

▶ Run in browser

Tags: 2d physics area trigger signals

An Area2D is a sensor: it has a shape but no mass, pushes nothing, and tells you when a physics body starts or stops overlapping it. Give it geometry with the shape= keyword (here a RectangleShape2D; a CollisionShape2D child works too) and put it under a PhysicsRoot2D beside the bodies it should see.

Connect to its two signals, which pass the body as their argument:

  • body_entered(body) fires once when a body begins to overlap the area.

  • body_exited(body) fires once when it stops overlapping, including when it is destroyed.

get_overlapping_bodies() answers the same question on demand, when a snapshot suits better than events.

Here a PhysicsBody2D in DYNAMIC mode walks left and right at a constant velocity in a world with no gravity, turning back at the edges of the window, and so keeps crossing the zone. The handlers count the entries and track whether the walker is inside: the zone turns green while it is occupied and blue when it is empty, and the label keeps the count. A Camera2D keeps the zone at the centre of the window at any size.

Source

 1# /// simvx
 2# tags = ["2d", "physics", "area", "trigger", "signals"]
 3# web = { width = 800, height = 600, responsive = true }
 4# ///
 5"""Area2D: a trigger zone that reports bodies entering and leaving.
 6
 7An `Area2D` is a sensor: it has a shape but no mass, pushes nothing, and tells you when a physics
 8body starts or stops overlapping it. Give it geometry with the `shape=` keyword (here a
 9`RectangleShape2D`; a `CollisionShape2D` child works too) and put it under a `PhysicsRoot2D`
10beside the bodies it should see.
11
12Connect to its two signals, which pass the body as their argument:
13
14- `body_entered(body)` fires once when a body begins to overlap the area.
15- `body_exited(body)` fires once when it stops overlapping, including when it is destroyed.
16
17`get_overlapping_bodies()` answers the same question on demand, when a snapshot suits better than
18events.
19
20Here a `PhysicsBody2D` in `DYNAMIC` mode walks left and right at a constant velocity in a world with
21no gravity, turning back at the edges of the window, and so keeps crossing the zone. The handlers
22count the entries and track whether the walker is inside: the zone turns green while it is
23occupied and blue when it is empty, and the label keeps the count. A `Camera2D` keeps the zone at
24the centre of the window at any size.
25"""
26
27from simvx.core import (
28    Anchor,
29    Area2D,
30    BodyMode,
31    Camera2D,
32    CanvasLayer,
33    CircleShape2D,
34    Label,
35    Node2D,
36    PhysicsBody2D,
37    PhysicsRoot2D,
38    RectangleShape2D,
39    Vec2,
40)
41from simvx.graphics import App
42
43ZONE_HALF = Vec2(90, 130)  # half-extents of the rectangular sensor
44WALKER_RADIUS = 22.0
45SPEED = 260.0
46
47
48class TriggerZoneDemo(Node2D):
49    dynamic = True  # the walker moves every frame
50
51    def on_ready(self):
52        self.entries = 0
53        self.inside = False
54        self.add_child(Camera2D())  # world origin at the centre of the window
55
56        world = self.add_child(PhysicsRoot2D(name="World", gravity=Vec2(0, 0)))
57        self.zone = world.add_child(Area2D(name="Zone", shape=RectangleShape2D(half_extents=ZONE_HALF)))
58        self.zone.body_entered.connect(self._on_body_entered)
59        self.zone.body_exited.connect(self._on_body_exited)
60        self.walker = world.add_child(
61            PhysicsBody2D(name="Walker", mode=BodyMode.DYNAMIC, position=(-250, 0), shape=CircleShape2D(WALKER_RADIUS))
62        )
63        self.walker.velocity = (SPEED, 0)
64
65        self.status = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=16))
66        self._show_status()
67
68    def _on_body_entered(self, body):
69        self.entries += 1
70        self.inside = True
71        self._show_status()
72
73    def _on_body_exited(self, body):
74        self.inside = False
75        self._show_status()
76
77    def _show_status(self):
78        state = "inside" if self.inside else "outside"
79        self.status.text = f"Walker: {state}   Entries: {self.entries}"
80
81    def on_fixed_update(self, dt):
82        # Turn back at the window's edges so the walker keeps crossing the zone.
83        edge = self.viewport_size.x / 2 - WALKER_RADIUS
84        x, vx = float(self.walker.position.x), float(self.walker.velocity.x)
85        if abs(x) > edge and x * vx > 0:
86            self.walker.velocity = (-vx, 0)
87
88    def on_draw(self, renderer):
89        fill = (0.2, 0.7, 0.3, 0.45) if self.inside else (0.2, 0.45, 0.9, 0.35)
90        renderer.rect(-ZONE_HALF, ZONE_HALF * 2, fill=fill, stroke=(0.85, 0.9, 1.0, 1.0))
91        renderer.circle(self.walker.position, WALKER_RADIUS, fill=(1.0, 0.75, 0.2, 1.0))
92
93
94if __name__ == "__main__":
95    App(title="Area2D Trigger Zone", width=800, height=600).run(TriggerZoneDemo())