Source code for gale.physics.node

"""
This file contains the implementation of the class Node: a lightweight
scene graph node for organizing physics entities, optionally driven by
a Body (whose position/angle then take over as the source of truth),
with children for grouping/attachment.

Author: Alejandro Mujica (aledrums@gmail.com)
"""

from typing import Optional, Sequence

import pygame

from .body import Body


[docs] class Node: """ A lightweight scene graph node for organizing physics entities: every node has an absolute position (matching how everything else in gale already works), optionally owns a Body (in which case its position/angle are pulled from the body every update, since physics is the source of truth for anything simulated), and can have children for grouping/attachment — a decorative or dependent node that should follow its parent's body around, or just a way to destroy a group of related nodes together. This mirrors gale.ui.Container's parent/children/add_order-render pattern, applied to physics entities instead of widgets. Usage example: chassis_node = Node(body=chassis_body) exhaust_node = Node(offset=(-20, 0)) # a decoration, no body of its own chassis_node.add_child(exhaust_node) # Every frame, after world.update(dt): chassis_node.update(dt) chassis_node.render(surface) """ def __init__( self, x: float = 0, y: float = 0, body: Optional[Body] = None, children: Optional[Sequence["Node"]] = None, ) -> None: """ :param x: Initial x position. If body is set, overwritten every update() with the body's own absolute position (Box2D bodies have no concept of a parent-relative transform). Otherwise, this is a fixed offset from this node's parent (or an absolute position, if it has none). The default value is 0. :param y: Initial y position, same rules as x. The default value is 0. :param body: The Body driving this node's transform. The default value is None, meaning this node's position is either fixed (no parent) or a fixed offset from its parent. :param children: Child nodes to add immediately. The default value is None. """ self.x: float = x self.y: float = y self.angle: float = 0.0 self.body: Optional[Body] = body self.parent: Optional["Node"] = None self.children: list = [] self.visible: bool = True for child in children or []: self.add_child(child)
[docs] def add_child(self, node: "Node") -> None: """ :param node: The child to add. Rendered/updated after every previously added child (add-order is z-order, as in gale.ui.Container). """ node.parent = self self.children.append(node)
[docs] def remove_child(self, node: "Node") -> None: """ :param node: The child to remove. """ if node in self.children: node.parent = None self.children.remove(node)
@property def world_position(self) -> pygame.Vector2: """ :returns: This node's absolute position. A node with a body already has an absolute position (Box2D bodies are always in world coordinates); otherwise, this composes every ancestor's position with this node's own offset. """ if self.body is not None or self.parent is None: return pygame.Vector2(self.x, self.y) return pygame.Vector2(self.x, self.y) + self.parent.world_position
[docs] def update(self, dt: float) -> None: """ :param dt: Time elapsed, in seconds, since the last call. """ if self.body is not None: self.x, self.y = self.body.position self.angle = self.body.angle for child in self.children: child.update(dt)
[docs] def render(self, surface: pygame.Surface) -> None: """ :param surface: The surface to draw this node (and its children) on. The base Node itself draws nothing — subclasses/games draw whatever they want at self.world_position. """ if not self.visible: return for child in self.children: child.render(surface)