Area2D¶
A trigger zone that fires body_entered / body_exited.
▶ Run in browserTags: 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())