Quellcode für miniworlds.worlds.manager.camera_manager

import pygame
from typing import Optional, Tuple, Set, TYPE_CHECKING

import miniworlds.appearances.background as background

if TYPE_CHECKING:
    from miniworlds.actors.actor import Actor
    from miniworlds.worlds.world import World


[Doku] class CameraManager(pygame.sprite.Sprite): """ CameraManager defines a movable viewport into a 2D world and tracks visible actors. It is accessed via `world.camera` and is responsible for view positioning, actor visibility, and coordinate transformations. For public API compatibility, docking and world-switching helpers are also exposed through `world.camera` and delegated internally to the layout manager. """ def __init__(self, view_x: int, view_y: int, world: "World") -> None: # Initialize base class super().__init__() # Store reference to the world self.world: "World" = world # Initial top-left positions self.screen_topleft: Tuple[int, int] = (0, 0) self._topleft: Tuple[int, int] = (0, 0) # Set initial world and view dimensions self._world_size_x = view_x self._world_size_y = view_y self.view: Tuple[int, int] = (view_x, view_y) # Precompute rects for performance self._cached_rect = self.get_rect() self._cached_screen_rect = self.get_screen_rect() # Actor visibility tracking self.view_actors_last_frame: Set["Actor"] = set() self._view_actors_actual_frame: Set["Actor"] = set() self._view_active_actors: Set["Actor"] = set() self._view_update_frame: int = -1 # Resize control flags self._resize = True self._status_disable_resize = False self._strict = True # Prevent scrolling outside world bounds self.dirty = False def _disable_resize(self) -> None: """Disable automatic resize handling.""" self._status_disable_resize = True def _enable_resize(self) -> None: """Re-enable automatic resize handling.""" self._status_disable_resize = False self._reload_camera() @property def width(self) -> int: """Width of the camera viewport in pixels.""" return self.view[0] @width.setter def width(self, value: int) -> None: self._set_width(value) def _set_width(self, value: int) -> None: if value > self._world_size_x: self._world_size_x = value self.view = (value, self.view[1]) self._resize = True self.dirty = True self._reload_camera() @property def height(self) -> int: """Height of the camera viewport in pixels.""" return self.view[1] @height.setter def height(self, value: int) -> None: self._set_height(value) def _set_height(self, value: int) -> None: if value > self._world_size_y: self._world_size_y = value self.view = (self.view[0], value) self._resize = True self.dirty = True self._reload_camera() @property def world_size_x(self) -> int: """Width of the world in pixels.""" return self._world_size_x @world_size_x.setter def world_size_x(self, value: int) -> None: self.view = (min(self.view[0], value), self.view[1]) self._world_size_x = value self.dirty = True self._reload_camera() @property def world_size_y(self) -> int: """Height of the world in pixels.""" return self._world_size_y @world_size_y.setter def world_size_y(self, value: int) -> None: self.view = (self.view[0], min(self.view[1], value)) self._world_size_y = value self.dirty = True self._reload_camera() @property def world_size(self) -> Tuple[int, int]: """Returns world size as (width, height).""" return self._world_size_x, self._world_size_y @world_size.setter def world_size(self, value: Tuple[int, int]) -> None: self._world_size_x, self._world_size_y = value @property def x(self) -> int: """Camera x-position (top-left corner in world coordinates).""" return self._topleft[0] @x.setter def x(self, value: int) -> None: self.topleft = (value, self._topleft[1]) self.dirty = True @property def y(self) -> int: """Camera y-position (top-left corner in world coordinates).""" return self._topleft[1] @y.setter def y(self, value: int) -> None: self.topleft = (self._topleft[0], value) self.dirty = True @property def topleft(self) -> Tuple[int, int]: """Top-left corner of the camera in world coordinates.""" return self._topleft @topleft.setter def topleft(self, value: Tuple[int, int]) -> None: self._set_topleft(value) def _set_topleft(self, value: Tuple[int, int]) -> None: if self._strict: value = (self._limit_x(value[0]), self._limit_y(value[1])) if value != self._topleft: self._topleft = value self._reload_actors_in_view() self.dirty = True def _limit_x(self, value: int) -> int: return max(0, min(value, self._world_size_x - self.view[0])) def _limit_y(self, value: int) -> int: return max(0, min(value, self._world_size_y - self.view[1])) @property def rect(self) -> pygame.Rect: """Returns camera rect in world coordinates.""" return self.get_rect() if self.dirty else self._cached_rect @property def screen_rect(self) -> pygame.Rect: """Returns camera rect in screen coordinates.""" return self.get_screen_rect() if self.dirty else self._cached_screen_rect @property def world_rect(self) -> pygame.Rect: """Returns the full world rect.""" return self.get_world_rect() @property def window_docking_position(self) -> Optional[str]: """Returns the docking position of this world in the application window.""" return self.world._layout.window_docking_position @property def center(self) -> Tuple[float, float]: """Returns center of camera in world coordinates.""" return (self.topleft[0] + self.view[0] / 2, self.topleft[1] + self.view[1] / 2)
[Doku] def get_rect(self) -> pygame.Rect: """ Returns the camera rectangle in world coordinates. Returns: A pygame.Rect representing the current viewport's world area. Examples: >>> world.camera.get_rect() """ return pygame.Rect(*self._topleft, *self.view)
[Doku] def get_screen_rect(self) -> pygame.Rect: """ Returns the camera rectangle in screen coordinates. Returns: A pygame.Rect representing where the camera appears on screen. Examples: >>> world.camera.get_screen_rect() """ return pygame.Rect(*self.screen_topleft, *self.view)
[Doku] def get_world_rect(self) -> pygame.Rect: """ Returns the full world rectangle from the camera's current origin. Returns: A pygame.Rect representing the visible and scrolled world area. Examples: >>> world.camera.get_world_rect() """ return pygame.Rect(*self._topleft, self.world_size_x, self.world_size_y)
[Doku] def get_screen_position(self, pos: Tuple[int, int]) -> Tuple[int, int]: """ Convert world coordinates to screen coordinates. Args: pos: Position in world coordinates. Returns: Position in screen coordinates. Examples: >>> world.camera.get_screen_position((100, 200)) """ return ( self.screen_topleft[0] + pos[0] - self.topleft[0], self.screen_topleft[1] + pos[1] - self.topleft[1] )
[Doku] def get_local_position(self, pos: Tuple[int, int]) -> Tuple[int, int]: """ Convert world coordinates to camera-local coordinates. Args: pos: Global position in the world. Returns: Local position relative to the camera. Examples: >>> world.camera.get_local_position((500, 400)) """ return pos[0] - self.topleft[0], pos[1] - self.topleft[1]
[Doku] def get_global_coordinates_for_world(self, pos: Tuple[int, int]) -> Tuple[int, int]: """ Convert local camera position to global world position. Args: pos: Position relative to the camera. Returns: Global world position. Examples: >>> world.camera.get_global_coordinates_for_world((100, 50)) """ return pos[0] + self.topleft[0], pos[1] + self.topleft[1]
def _reload_actors_in_view(self) -> None: """Marks actors in view as dirty for redraw.""" for actor in self.get_actors_in_view(): actor.dirty = 1
[Doku] def get_actors_in_view(self) -> Set["Actor"]: if self._view_update_frame == self.world.frame: return self._view_active_actors current_frame_actors = { actor for actor in self.world.actors if self.rect.colliderect(actor.position_manager.get_global_rect()) } self.view_actors_last_frame = self._view_actors_actual_frame self._view_actors_actual_frame = current_frame_actors self._view_active_actors = current_frame_actors.union(self.view_actors_last_frame) self._view_update_frame = self.world.frame return self._view_active_actors
def _should_repaint_actor(self, actor: "Actor") -> bool: if self.world.frame == 0: return True if self._view_update_frame == self.world.frame: return actor in self._view_active_actors if actor in self.view_actors_last_frame: return True return self.is_actor_in_view(actor)
[Doku] def is_actor_in_view(self, actor: "Actor") -> bool: return actor.position_manager.get_global_rect().colliderect(self.rect)
[Doku] def from_actor(self, actor: "Actor") -> None: """ Move camera to center on a given actor. Examples: >>> world.camera.from_actor(actor) """ if actor.center: center = actor.center self.topleft = ( center[0] - self.view[0] // 2 - actor.width // 2, center[1] - self.view[1] // 2 - actor.height // 2 ) else: self.topleft = (0, 0)
[Doku] def add_right(self, world: "World", size: int = 100) -> "World": """Dock a helper world to the right side of the current world.""" return self.world._layout.add_right(world, size)
[Doku] def add_bottom(self, world: "World", size: int = 100) -> "World": """Dock a helper world below the current world.""" return self.world._layout.add_bottom(world, size)
[Doku] def remove_world(self, world: "World") -> None: """Remove a previously docked world.""" self.world._layout.remove_world(world)
[Doku] def switch_world(self, new_world: "World", reset: bool = False) -> None: """Switch to another world through the public camera API.""" self.world._layout.switch_world(new_world, reset)
[Doku] def is_in_screen(self, pixel: Tuple[int, int]) -> bool: return bool(pixel) and self.screen_rect.collidepoint(pixel)
def _reload_camera(self) -> None: """Internal: reloads geometry and triggers world resize.""" self._clear_camera_cache() if self._resize and not self._status_disable_resize: self.world.app.resize() self._resize = False self.world.background.set_dirty("all", background.Background.RELOAD_ACTUAL_IMAGE) def _clear_camera_cache(self) -> None: """Clears view tracking for actor visibility.""" self._view_actors_actual_frame.clear() self._view_active_actors.clear() self._view_update_frame = -1 def _update(self) -> None: """Called each frame to refresh geometry if needed.""" if self.dirty: self._cache_rects() self.dirty = False def _cache_rects(self) -> None: """Caches world and screen rects.""" self._cached_rect = self.get_rect() self._cached_screen_rect = self.get_screen_rect()