Source code for arcade.camera

"""
Camera class
"""
import math
from typing import TYPE_CHECKING, List, Optional, Tuple, Union

from pyglet.math import Mat4, Vec2, Vec3

import arcade

if TYPE_CHECKING:
    from arcade import Point, Sprite, SpriteList

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


[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]) # Matrixes # 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 precompute 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]) def _set_projection_matrix(self, *, update_combined_matrix: bool = True) -> None: """ Helper method. This will just precompute the projection and combined matrix :param bool update_combined_matrix: if True will also update the combined matrix (projection @ view) """ self._projection_matrix = Mat4.orthogonal_projection(*self._projection, -1, 1) if update_combined_matrix: self._set_combined_matrix() def _set_view_matrix(self, *, update_combined_matrix: bool = True) -> None: """ Helper method. This will just precompute the view and combined matrix :param bool 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 precompute 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 Vec2 vector: Vector to move the camera towards. :param Vec2 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): vector = Vec2(*vector) # get the center of the camera viewport center = Vec2(self.viewport_width / 2, self.viewport_height / 2) # move to the vector substracting the center target = vector - 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 Vec2 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 int viewport_width: Width of the viewport :param int viewport_height: Height of the viewport :param bool 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 alse 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.ctx.projection_2d_matrix = self._combined_matrix # sets projection position and zoom
[docs]class Camera(SimpleCamera): """ The Camera class is used for controlling the visible viewport, 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 tuple viewport: (left, bottom, width, height) size of the viewport. If None the window size will be used. :param tuple projection: (left, right, bottom, top) size of the projection. If None the window size will be used. :param float zoom: the zoom to apply to the projection :param float rotation: the angle in degrees to rotate the projection :param tuple anchor: the x, y point where the camera rotation will anchor. Default is the center of the viewport. :param Window 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, ): # Zoom self._zoom: float = 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 precompute the rotaion 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 precompute the projection and combined matrix :param bool update_combined_matrix: if True will also update the combined matrix (projection @ view) """ # apply zoom left, right, bottom, top = self._projection if self._zoom != 1.0: right *= self._zoom top *= self._zoom 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 precompute the view and combined matrix :param bool 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._zoom) / 2)), (result_position[1] / ((self.viewport_height * self._zoom) / 2)), 0 ) self._view_matrix = ~(Mat4.from_translation(result_position) @ Mat4().scale( Vec3(self._zoom, self._zoom, 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 zoom(self) -> float: """ The zoom applied to the projection""" return self._zoom @zoom.setter def zoom(self, zoom: float) -> None: """ Update the zoom 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._zoom = zoom # Changing the 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 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 = arcade.get_distance(0, 0, ox, oy) velocity_mag = arcade.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 Vec2 velocity: Vector to start moving the camera :param float speed: How fast to shake :param float 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() # 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: Point to check :param SpriteList sprite_list: SpriteList to check against :returns: List of sprites colliding, or an empty list. :rtype: list """ raise NotImplementedError()