nodes/pipe.pyΒΆ
Part of Clumsy Bird.
1"""Pipe pair: top + bottom pipe sprites that scroll right to left and emit
2``passed`` the moment the pair's centre crosses the bird."""
3
4from config import (
5 ASSETS,
6 BIRD_X,
7 PIPE_HEIGHT,
8 PIPE_SCROLL_SPEED,
9 PIPE_WIDTH,
10)
11
12from simvx.core import CharacterBody2D, Node2D, RectangleShape2D, Signal, Sprite2D, Vec2
13
14
15class Pipe(CharacterBody2D):
16 """One half of a pipe pair (top or bottom). Owns its own sprite + collision."""
17
18 def __init__(self, *, flipped: bool, position: Vec2, **kwargs):
19 # Collision matches the visible pipe; the sprite is centred on position.
20 super().__init__(
21 shape=RectangleShape2D(half_extents=Vec2(PIPE_WIDTH / 2, PIPE_HEIGHT / 2)),
22 position=position,
23 **kwargs,
24 )
25 self.add_to_group("pipes")
26 self.flipped = flipped
27
28 # Top pipes are vertically flipped (-1 scale.y) so the rim appears at
29 # the bottom of the sprite. Sprite2D inherits parent transform.
30 sprite_scale = Vec2(1.0, -1.0 if flipped else 1.0)
31 self.sprite = self.add_child(
32 Sprite2D(
33 texture=str(ASSETS / "pipe.png"),
34 width=PIPE_WIDTH,
35 height=PIPE_HEIGHT,
36 scale=sprite_scale,
37 name="Sprite",
38 )
39 )
40
41
42class PipePair(Node2D):
43 """Container for a top + bottom pipe.
44
45 Spawns just off the right edge and scrolls left at PIPE_SCROLL_SPEED.
46 Emits ``passed`` once when its centre crosses the bird's x, then destroys
47 itself once it is fully off the left edge.
48 """
49
50 passed = Signal()
51
52 def __init__(self, *, gap_centre_y: float, gap_size: float, x: float, **kwargs):
53 super().__init__(position=Vec2(x, 0), **kwargs)
54 self.add_to_group("pipe_pairs")
55 self._scored = False
56
57 # Top pipe: rim at gap_centre_y - gap_size/2. Sprite is PIPE_HEIGHT
58 # tall and drawn centred on its position, so position the sprite so
59 # its bottom-after-flip aligns with the rim.
60 top_rim_y = gap_centre_y - gap_size / 2
61 bot_rim_y = gap_centre_y + gap_size / 2
62
63 self.top = self.add_child(
64 Pipe(
65 flipped=True,
66 position=Vec2(0, top_rim_y - PIPE_HEIGHT / 2),
67 name="Top",
68 )
69 )
70 self.bottom = self.add_child(
71 Pipe(
72 flipped=False,
73 position=Vec2(0, bot_rim_y + PIPE_HEIGHT / 2),
74 name="Bottom",
75 )
76 )
77
78 def on_fixed_update(self, dt: float):
79 self.position.x -= PIPE_SCROLL_SPEED * dt
80
81 # Score when the pair's centre crosses the bird's x position.
82 if not self._scored and self.position.x < BIRD_X:
83 self._scored = True
84 self.passed()
85
86 # Off-screen cleanup
87 if self.position.x < -PIPE_WIDTH:
88 self.destroy()