Quellcode für miniworlds.worlds.manager.camera_manager

import pygame
from typing import 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. """ 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 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"]: actor_rect_pairs = [ (actor, actor.position_manager.get_global_rect()) for actor in self.world.actors ] current_frame_actors = { actor for actor, rect in actor_rect_pairs if self.rect.colliderect(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
[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 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_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()