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()