Source code for arcade.camera

"""
Camera class
"""
from __future__ import annotations

import math
from typing import TYPE_CHECKING, List, Optional, Tuple, Union

from pyglet.math import Mat4, Vec2, Vec3

import arcade
from arcade.types import Point
from arcade.math import get_distance

if TYPE_CHECKING:
    from arcade import Sprite, SpriteList

# type aliases
FourIntTuple = Tuple[int, int, int, int]
FourFloatTuple = Tuple[float, float, float, float]

__all__ = [
    "SimpleCamera",
    "Camera"
]


[docs] class SimpleCamera: """ A simple camera that allows to change the viewport, the projection and can move around. That's it. See arcade.Camera for more advance stuff. :param viewport: Size of the viewport: (left, bottom, width, height) :param projection: Space to allocate in the viewport of the camera (left, right, bottom, top) """ def __init__( self, *, viewport: Optional[FourIntTuple] = None, projection: Optional[FourFloatTuple] = None, window: Optional["arcade.Window"] = None, ) -> None: # Reference to Context, used to update projection matrix self._window: "arcade.Window" = window or arcade.get_window() # store the viewport and projection tuples # viewport is the space the camera will hold on the screen (left, bottom, width, height) self._viewport: FourIntTuple = viewport or (0, 0, self._window.width, self._window.height) # projection is what you want to project into the camera viewport (left, right, bottom, top) self._projection: FourFloatTuple = projection or (0, self._window.width, 0, self._window.height) if viewport is not None and projection is None: # if viewport is provided but projection is not, projection # will match the provided viewport self._projection = (viewport[0], viewport[2], viewport[1], viewport[3]) # Matrices # Projection Matrix is used to apply the camera viewport size self._projection_matrix: Mat4 = Mat4() # View Matrix is what the camera is looking at(position) self._view_matrix: Mat4 = Mat4() # We multiply projection and view matrices to get combined, # this is what actually gets sent to GL context self._combined_matrix: Mat4 = Mat4() # Position self.position: Vec2 = Vec2(0, 0) # Camera movement self.goal_position: Vec2 = Vec2(0, 0) self.move_speed: float = 1.0 # 1.0 is instant self.moving: bool = False # Init matrixes # This will pre-compute the projection, view and combined matrixes self._set_projection_matrix(update_combined_matrix=False) self._set_view_matrix() @property def viewport_width(self) -> int: """ Returns the width of the viewport """ return self._viewport[2] @property def viewport_height(self) -> int: """ Returns the height of the viewport """ return self._viewport[3] @property def viewport(self) -> FourIntTuple: """ The space the camera will hold on the screen (left, bottom, width, height) """ return self._viewport @viewport.setter def viewport(self, viewport: FourIntTuple) -> None: """ Sets the viewport """ self.set_viewport(viewport)
[docs] def set_viewport(self, viewport: FourIntTuple) -> None: """ Sets the viewport """ self._viewport = viewport or (0, 0, self._window.width, self._window.height) # the viewport affects the view matrix self._set_view_matrix()
@property def projection(self) -> FourFloatTuple: """ The dimensions of the space to project in the camera viewport (left, right, bottom, top). The projection is what you want to project into the camera viewport. """ return self._projection @projection.setter def projection(self, new_projection: FourFloatTuple) -> None: """ Update the projection of the camera. This also updates the projection matrix with an orthogonal projection based on the projection size of the camera and the zoom applied. """ self._projection = new_projection or (0, self._window.width, 0, self._window.height) self._set_projection_matrix() @property def viewport_to_projection_width_ratio(self): """ The ratio of viewport width to projection width """ return self.viewport_width / (self._projection[1] - self._projection[0]) @property def viewport_to_projection_height_ratio(self): """ The ratio of viewport height to projection height """ return self.viewport_height / (self._projection[3] - self._projection[2]) @property def projection_to_viewport_width_ratio(self): """ The ratio of projection width to viewport width """ return (self._projection[1] - self._projection[0]) / self.viewport_width @property def projection_to_viewport_height_ratio(self): """ The ratio of projection height to viewport height """ return (self._projection[3] - self._projection[2]) / self.viewport_height def _set_projection_matrix(self, *, update_combined_matrix: bool = True) -> None: """ Helper method. This will just pre-compute the projection and combined matrix :param update_combined_matrix: if True will also update the combined matrix (projection @ view) """ self._projection_matrix = Mat4.orthogonal_projection(*self._projection, -100, 100) if update_combined_matrix: self._set_combined_matrix() def _set_view_matrix(self, *, update_combined_matrix: bool = True) -> None: """ Helper method. This will just pre-compute the view and combined matrix :param update_combined_matrix: if True will also update the combined matrix (projection @ view) """ # Figure out our 'real' position result_position = Vec3( (self.position[0] / (self.viewport_width / 2)), (self.position[1] / (self.viewport_height / 2)), 0 ) self._view_matrix = ~(Mat4.from_translation(result_position)) if update_combined_matrix: self._set_combined_matrix() def _set_combined_matrix(self) -> None: """ Helper method. This will just pre-compute the combined matrix""" self._combined_matrix = self._view_matrix @ self._projection_matrix
[docs] def move_to(self, vector: Union[Vec2, tuple], speed: float = 1.0) -> None: """ Sets the goal position of the camera. The camera will lerp towards this position based on the provided speed, updating its position every time the use() function is called. :param vector: Vector to move the camera towards. :param speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly """ self.goal_position = Vec2(*vector) self.move_speed = speed self.moving = True
[docs] def move(self, vector: Union[Vec2, tuple]) -> None: """ Moves the camera with a speed of 1.0, aka instant move This is equivalent to calling move_to(my_pos, 1.0) """ self.move_to(vector, 1.0)
[docs] def center(self, vector: Union[Vec2, tuple], speed: float = 1.0) -> None: """ Centers the camera on coordinates """ if not isinstance(vector, Vec2): vector2: Vec2 = Vec2(*vector) else: vector2 = vector # get the center of the camera viewport center = Vec2(self.viewport_width, self.viewport_height) / 2 # adjust vector to projection ratio vector2 = Vec2(vector2.x * self.viewport_to_projection_width_ratio, vector2.y * self.viewport_to_projection_height_ratio) # move to the vector subtracting the center target = (vector2 - center) self.move_to(target, speed)
[docs] def get_map_coordinates(self, camera_vector: Union[Vec2, tuple]) -> Vec2: """ Returns map coordinates in pixels from screen coordinates based on the camera position :param camera_vector: Vector captured from the camera viewport """ return Vec2(*self.position) + Vec2(*camera_vector)
[docs] def resize(self, viewport_width: int, viewport_height: int, *, resize_projection: bool = True) -> None: """ Resize the camera's viewport. Call this when the window resizes. :param viewport_width: Width of the viewport :param viewport_height: Height of the viewport :param resize_projection: if True the projection will also be resized """ new_viewport = (self._viewport[0], self._viewport[1], viewport_width, viewport_height) self.set_viewport(new_viewport) if resize_projection: self.projection = (self._projection[0], viewport_width, self._projection[2], viewport_height)
[docs] def update(self): """ Update the camera's viewport to the current settings. """ if self.moving: # Apply Goal Position self.position = self.position.lerp(self.goal_position, self.move_speed) if self.position == self.goal_position: self.moving = False self._set_view_matrix() # this will also set the combined matrix
[docs] def use(self) -> None: """ Select this camera for use. Do this right before you draw. """ self._window.current_camera = self # update camera position and calculate matrix values if needed self.update() # set Viewport / projection self._window.ctx.viewport = self._viewport # sets viewport of the camera self._window.projection = self._combined_matrix # sets projection position and zoom self._window.view = Mat4() # Set to identity matrix for now
[docs] class Camera(SimpleCamera): """ The Camera class is used for controlling the visible viewport, the projection, zoom and rotation. It is very useful for separating a scrolling screen of sprites, and a GUI overlay. For an example of this in action, see :ref:`sprite_move_scrolling`. :param viewport: (left, bottom, width, height) size of the viewport. If None the window size will be used. :param projection: (left, right, bottom, top) size of the projection. If None the window size will be used. :param zoom: the zoom to apply to the projection :param rotation: the angle in degrees to rotate the projection :param anchor: the x, y point where the camera rotation will anchor. Default is the center of the viewport. :param window: Window to associate with this camera, if working with a multi-window program. """ def __init__( self, *, viewport: Optional[FourIntTuple] = None, projection: Optional[FourFloatTuple] = None, zoom: float = 1.0, rotation: float = 0.0, anchor: Optional[Tuple[float, float]] = None, window: Optional["arcade.Window"] = None, ): # scale and zoom # zoom it's just x scale value. Setting zoom will set scale x, y to the same value self._scale: Tuple[float, float] = (zoom, zoom) # Near and Far self._near: int = -1 self._far: int = 1 # Shake self.shake_velocity: Vec2 = Vec2() self.shake_offset: Vec2 = Vec2() self.shake_speed: float = 0.0 self.shake_damping: float = 0.0 self.shaking: bool = False # Call init from superclass here, previous attributes are needed before this call super().__init__(viewport=viewport, projection=projection, window=window) # Rotation self._rotation: float = rotation # in degrees self._anchor: Optional[Tuple[float, float]] = anchor # (x, y) to anchor the camera rotation # Matrixes # Rotation matrix holds the matrix used to compute the # rotation set in window.ctx.view_matrix_2d self._rotation_matrix: Mat4 = Mat4() # Init matrixes # This will pre-compute the rotation matrix self._set_rotation_matrix()
[docs] def set_viewport(self, viewport: FourIntTuple) -> None: """ Sets the viewport """ super().set_viewport(viewport) # the viewport affects the rotation matrix if the rotation anchor is not set if self._anchor is None: self._set_rotation_matrix()
def _set_projection_matrix(self, *, update_combined_matrix: bool = True) -> None: """ Helper method. This will just pre-compute the projection and combined matrix :param update_combined_matrix: if True will also update the combined matrix (projection @ view) """ # apply zoom left, right, bottom, top = self._projection if self._scale != (1.0, 1.0): right *= self._scale[0] # x axis scale top *= self._scale[1] # y axis scale self._projection_matrix = Mat4.orthogonal_projection(left, right, bottom, top, self._near, self._far) if update_combined_matrix: self._set_combined_matrix() def _set_view_matrix(self, *, update_combined_matrix: bool = True) -> None: """ Helper method. This will just pre-compute the view and combined matrix :param update_combined_matrix: if True will also update the combined matrix (projection @ view) """ # Figure out our 'real' position plus the shake result_position = self.position + self.shake_offset result_position = Vec3( (result_position[0] / ((self.viewport_width * self._scale[0]) / 2)), (result_position[1] / ((self.viewport_height * self._scale[1]) / 2)), 0 ) self._view_matrix = ~(Mat4.from_translation(result_position) @ Mat4().scale( Vec3(self._scale[0], self._scale[1], 1.0))) if update_combined_matrix: self._set_combined_matrix() def _set_rotation_matrix(self) -> None: """ Helper method that computes the rotation_matrix every time is needed """ rotate = Mat4.from_rotation(math.radians(self._rotation), Vec3(0, 0, 1)) # If no anchor is set, use the center of the screen if self._anchor is None: offset = Vec3(self.position.x, self.position.y, 0) offset += Vec3(self.viewport_width / 2, self.viewport_height / 2, 0) else: offset = Vec3(self._anchor[0], self._anchor[1], 0) translate_pre = Mat4.from_translation(offset) translate_post = Mat4.from_translation(-offset) self._rotation_matrix = translate_post @ rotate @ translate_pre @property def scale(self) -> Tuple[float, float]: """ Returns the x, y scale. """ return self._scale @scale.setter def scale(self, new_scale: Tuple[float, float]) -> None: """ Sets the x, y scale (zoom property just sets scale to the same value). This also updates the projection matrix with an orthogonal projection based on the projection size of the camera and the zoom applied. """ if new_scale[0] <= 0 or new_scale[1] <= 0: raise ValueError("Scale must be greater than zero") self._scale = (float(new_scale[0]), float(new_scale[1])) # Changing the scale (zoom) affects both projection_matrix and view_matrix self._set_projection_matrix( update_combined_matrix=False) # combined matrix will be set in the next call self._set_view_matrix() @property def zoom(self) -> float: """ The zoom applied to the projection. Just returns the x scale value. """ return self._scale[0] @zoom.setter def zoom(self, zoom: float) -> None: """ Apply a zoom to the projection """ self.scale = zoom, zoom @property def near(self) -> int: """ The near applied to the projection""" return self._near @near.setter def near(self, near: int) -> None: """ Update the near of the camera. This also updates the projection matrix with an orthogonal projection based on the projection size of the camera and the zoom applied. """ self._near = near self._set_projection_matrix() @property def far(self) -> int: """ The far applied to the projection""" return self._far @far.setter def far(self, far: int) -> None: """ Update the far of the camera. This also updates the projection matrix with an orthogonal projection based on the projection size of the camera and the zoom applied. """ self._far = far self._set_projection_matrix() @property def rotation(self) -> float: """ Get or set the rotation in degrees. This will rotate the camera clockwise meaning the contents will rotate counter-clockwise. """ return self._rotation @rotation.setter def rotation(self, value: float) -> None: self._rotation = value self._set_rotation_matrix() @property def anchor(self) -> Optional[Tuple[float, float]]: """ Get or set the rotation anchor for the camera. By default, the anchor is the center of the screen and the anchor value is `None`. Assigning a custom anchor point will override this behavior. The anchor point is in world / global coordinates. Example:: # Set the anchor to the center of the world camera.anchor = 0, 0 # Set the anchor to the center of the player camera.anchor = player.position """ return self._anchor @anchor.setter def anchor(self, anchor: Optional[Tuple[float, float]]) -> None: if anchor is None: self._anchor = None else: self._anchor = anchor[0], anchor[1] self._set_rotation_matrix()
[docs] def update(self) -> None: """ Update the camera's viewport to the current settings. """ update_view_matrix = False if self.moving: # Apply Goal Position self.position = self.position.lerp(self.goal_position, self.move_speed) if self.position == self.goal_position: self.moving = False update_view_matrix = True if self.shaking: # Apply Camera Shake # Move our offset based on shake velocity self.shake_offset += self.shake_velocity # Get x and ys vx = self.shake_velocity[0] vy = self.shake_velocity[1] ox = self.shake_offset[0] oy = self.shake_offset[1] # Calculate the angle our offset is at, and how far out angle = math.atan2(ox, oy) distance = get_distance(0, 0, ox, oy) velocity_mag = get_distance(0, 0, vx, vy) # Ok, what's the reverse? Pull it back in. reverse_speed = min(self.shake_speed, distance) opposite_angle = angle + math.pi opposite_vector = Vec2( math.sin(opposite_angle) * reverse_speed, math.cos(opposite_angle) * reverse_speed, ) # Shaking almost done? Zero it out if velocity_mag < self.shake_speed and distance < self.shake_speed: self.shake_velocity = Vec2(0, 0) self.shake_offset = Vec2(0, 0) self.shaking = False # Come up with a new velocity, pulled by opposite vector and damped self.shake_velocity += opposite_vector self.shake_velocity *= self.shake_damping update_view_matrix = True if update_view_matrix: self._set_view_matrix() # this will also set the combined matrix
[docs] def shake(self, velocity: Union[Vec2, tuple], speed: float = 1.5, damping: float = 0.9) -> None: """ Add a camera shake. :param velocity: Vector to start moving the camera :param speed: How fast to shake :param damping: How fast to stop shaking """ if not isinstance(velocity, Vec2): velocity = Vec2(*velocity) self.shake_velocity += velocity self.shake_speed = speed self.shake_damping = damping self.shaking = True
[docs] def use(self) -> None: """ Select this camera for use. Do this right before you draw. """ super().use() # call SimpleCamera.use() # set rotation matrix self._window.ctx.view_matrix_2d = self._rotation_matrix # sets rotation and rotation anchor
[docs] def get_sprites_at_point(self, point: "Point", sprite_list: "SpriteList") -> List["Sprite"]: """ Get a list of sprites at a particular point when This function sees if any sprite overlaps the specified point. If a sprite has a different center_x/center_y but touches the point, this will return that sprite. :param point: Point to check :param sprite_list: SpriteList to check against :returns: List of sprites colliding, or an empty list. """ raise NotImplementedError()