class Sprite: """ Class that represents a 'sprite' on-screen. Most games center around sprites. For examples on how to use this class, see: http://arcade.academy/examples/index.html#sprites Attributes: :alpha: Transparency of sprite. 0 is invisible, 255 is opaque. :angle: Rotation angle in degrees. :radians: Rotation angle in radians. :bottom: Set/query the sprite location by using the bottom coordinate. \ This will be the 'y' of the bottom of the sprite. :boundary_left: Used in movement. Left boundary of moving sprite. :boundary_right: Used in movement. Right boundary of moving sprite. :boundary_top: Used in movement. Top boundary of moving sprite. :boundary_bottom: Used in movement. Bottom boundary of moving sprite. :center_x: X location of the center of the sprite :center_y: Y location of the center of the sprite :change_x: Movement vector, in the x direction. :change_y: Movement vector, in the y direction. :change_angle: Change in rotation. :color: Color tint the sprite :collision_radius: Used as a fast-check to see if this item is close \ enough to another item. If this check works, we do a slower more accurate check. \ You probably don't want to use this field. Instead, set points in the \ hit box. :cur_texture_index: Index of current texture being used. :guid: Unique identifier for the sprite. Useful when debugging. :height: Height of the sprite. :force: Force being applied to the sprite. Useful when used with Pymunk \ for physics. :left: Set/query the sprite location by using the left coordinate. This \ will be the 'x' of the left of the sprite. :points: Points, in relation to the center of the sprite, that are used \ for collision detection. Arcade defaults to creating points for a rectangle \ that encompass the image. If you are creating a ramp or making better \ hit-boxes, you can custom-set these. :position: A list with the (x, y) of where the sprite is. :repeat_count_x: Unused :repeat_count_y: Unused :right: Set/query the sprite location by using the right coordinate. \ This will be the 'y=x' of the right of the sprite. :sprite_lists: List of all the sprite lists this sprite is part of. :texture: `Texture` class with the current texture. :textures: List of textures associated with this sprite. :top: Set/query the sprite location by using the top coordinate. This \ will be the 'y' of the top of the sprite. :scale: Scale the image up or down. Scale of 1.0 is original size, 0.5 \ is 1/2 height and width. :velocity: Change in x, y expressed as a list. (0, 0) would be not moving. :width: Width of the sprite It is common to over-ride the `update` method and provide mechanics on movement or other sprite updates. """ def __init__(self, filename: str = None, scale: float = 1, image_x: float = 0, image_y: float = 0, image_width: float = 0, image_height: float = 0, center_x: float = 0, center_y: float = 0, repeat_count_x: int = 1, repeat_count_y: int = 1, flipped_horizontally: bool = False, flipped_vertically: bool = False, flipped_diagonally: bool = False, mirrored: bool = None, hit_box_algorithm: str = "Simple", hit_box_detail: float = 4.5): """ Create a new sprite. :param str filename: Filename of an image that represents the sprite. :param float scale: Scale the image up or down. Scale of 1.0 is none. :param float image_x: X offset to sprite within sprite sheet. :param float image_y: Y offset to sprite within sprite sheet. :param float image_width: Width of the sprite :param float image_height: Height of the sprite :param float center_x: Location of the sprite :param float center_y: Location of the sprite :param bool flipped_horizontally: Mirror the sprite image. Flip left/right across vertical axis. :param bool flipped_vertically: Flip the image up/down across the horizontal axis. :param bool flipped_diagonally: Transpose the image, flip it across the diagonal. :param mirrored: Deprecated. :param str hit_box_algorithm: One of 'None', 'Simple' or 'Detailed'. \ Defaults to 'Simple'. Use 'Simple' for the :data:`PhysicsEngineSimple`, \ :data:`PhysicsEnginePlatformer` \ and 'Detailed' for the :data:`PymunkPhysicsEngine`. .. figure:: images/hit_box_algorithm_none.png :width: 40% hit_box_algorithm = "None" .. figure:: images/hit_box_algorithm_simple.png :width: 55% hit_box_algorithm = "Simple" .. figure:: images/hit_box_algorithm_detailed.png :width: 75% hit_box_algorithm = "Detailed" :param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box """ if image_width < 0: raise ValueError("Width of image can't be less than zero.") if image_height < 0: raise ValueError( "Height entered is less than zero. Height must be a positive float." ) if image_width == 0 and image_height != 0: raise ValueError("Width can't be zero.") if image_height == 0 and image_width != 0: raise ValueError("Height can't be zero.") if mirrored is not None: from warnings import warn warn( "In Sprite, the 'mirrored' parameter is deprecated. Use 'flipped_horizontally' instead.", DeprecationWarning) flipped_horizontally = mirrored if hit_box_algorithm != "Simple" and \ hit_box_algorithm != "Detailed" and \ hit_box_algorithm != "None": raise ValueError( "hit_box_algorithm must be 'Simple', 'Detailed', or 'None'.") self._hit_box_algorithm = hit_box_algorithm self._hit_box_detail = hit_box_detail self.sprite_lists: List[Any] = [] self.physics_engines: List[Any] = [] self._texture: Optional[Texture] self._points: Optional[PointList] = None self._hit_box_shape: Optional[ShapeElementList] = None if filename is not None: try: self._texture = load_texture( filename, image_x, image_y, image_width, image_height, flipped_horizontally=flipped_horizontally, flipped_vertically=flipped_vertically, flipped_diagonally=flipped_diagonally, hit_box_algorithm=hit_box_algorithm, hit_box_detail=hit_box_detail) except Exception as e: raise FileNotFoundError( f"Unable to load image file {filename} {e}") if self._texture: self.textures = [self._texture] # Ignore the texture's scale and use ours self._width = self._texture.width * scale self._height = self._texture.height * scale else: self.textures = [] self._width = 0 self._height = 0 else: self.textures = [] self._texture = None self._width = 0 self._height = 0 self.cur_texture_index = 0 self._scale = scale self._position: Point = (center_x, center_y) self._angle = 0.0 self.velocity = [0.0, 0.0] self.change_angle = 0.0 self.boundary_left = None self.boundary_right = None self.boundary_top = None self.boundary_bottom = None self.properties: Dict[str, Any] = {} self._alpha = 255 self._collision_radius: Optional[float] = None self._color: RGB = (255, 255, 255) if self._texture and not self._points: self._points = self._texture.hit_box_points self._point_list_cache: Optional[PointList] = None self.force = [0, 0] self.guid: Optional[str] = None self.repeat_count_x = repeat_count_x self.repeat_count_y = repeat_count_y self._texture_transform = Matrix3x3() # Used if someone insists on doing a sprite.draw() self._sprite_list = None self.pymunk = PyMunk() def append_texture(self, texture: Texture): """ Appends a new texture to the list of textures that can be applied to this sprite. :param arcade.Texture texture: Texture to add ot the list of available textures """ self.textures.append(texture) def _get_position(self) -> Point: """ Get the center x and y coordinates of the sprite. Returns: (center_x, center_y) """ return self._position def _set_position(self, new_value: Point): """ Set the center x and y coordinates of the sprite. :param Point new_value: New position. """ if new_value[0] != self._position[0] or new_value[1] != self._position[ 1]: self.clear_spatial_hashes() self._point_list_cache = None self._position = new_value self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_location(self) position = property(_get_position, _set_position) def set_position(self, center_x: float, center_y: float): """ Set a sprite's position :param float center_x: New x position of sprite :param float center_y: New y position of sprite """ self._set_position((center_x, center_y)) def set_points(self, points: PointList): """ Set a sprite's hitbox """ from warnings import warn warn('set_points has been deprecated. Use set_hit_box instead.', DeprecationWarning) self._points = points def get_points(self) -> PointList: """ Get the points that make up the hit box for the rect that makes up the sprite, including rotation and scaling. """ from warnings import warn warn('get_points has been deprecated. Use get_hit_box instead.', DeprecationWarning) return self.get_adjusted_hit_box() points = property(get_points, set_points) def set_hit_box(self, points: PointList): """ Set a sprite's hit box. Hit box should be relative to a sprite's center, and with a scale of 1.0. Points will be scaled with get_adjusted_hit_box. """ self._point_list_cache = None self._hit_box_shape = None self._points = points def get_hit_box(self) -> PointList: """ Get a sprite's hit box, unadjusted for translation, rotation, or scale. """ # If there is no hitbox, use the width/height to get one if self._points is None and self._texture: self._points = self._texture.hit_box_points if self._points is None and self._width: x1, y1 = -self._width / 2, -self._height / 2 x2, y2 = +self._width / 2, -self._height / 2 x3, y3 = +self._width / 2, +self._height / 2 x4, y4 = -self._width / 2, +self._height / 2 self._points = ((x1, y1), (x2, y2), (x3, y3), (x4, y4)) if self._points is None and self.texture is not None: self._points = self.texture.hit_box_points if self._points is None: raise ValueError( "Error trying to get the hit box of a sprite, when no hit box is set.\nPlease make sure the " "Sprite.texture is set to a texture before trying to draw or do collision testing.\n" "Alternatively, manually call Sprite.set_hit_box with points for your hitbox." ) return self._points hit_box = property(get_hit_box, set_hit_box) def get_adjusted_hit_box(self) -> PointList: """ Get the points that make up the hit box for the rect that makes up the sprite, including rotation and scaling. """ # If we've already calculated the adjusted hit box, use the cached version if self._point_list_cache is not None: return self._point_list_cache # Adjust the hitbox point_list = [] for point in self.hit_box: # Get a copy of the point point = [point[0], point[1]] # Scale the point if self.scale != 1: point[0] *= self.scale point[1] *= self.scale # Rotate the point if self.angle: point = rotate_point(point[0], point[1], 0, 0, self.angle) # Offset the point point = [point[0] + self.center_x, point[1] + self.center_y] point_list.append(point) # Cache the results self._point_list_cache = point_list # if self.texture: # print(self.texture.name, self._point_list_cache) return self._point_list_cache def forward(self, speed: float = 1.0): """ Set a Sprite's position to speed by its angle :param speed: speed factor """ self.change_x += math.cos(self.radians) * speed self.change_y += math.sin(self.radians) * speed def reverse(self, speed: float = 1.0): """ Set a new speed, but in reverse. :param speed: speed factor """ self.forward(-speed) def strafe(self, speed: float = 1.0): """ Set a sprites position perpendicular to its angle by speed :param speed: speed factor """ self.change_x += -math.sin(self.radians) * speed self.change_y += math.cos(self.radians) * speed def turn_right(self, theta: float = 90): """ Rotate the sprite right a certain number of degrees. :param theta: change in angle """ self.angle -= theta def turn_left(self, theta: float = 90): """ Rotate the sprite left a certain number of degrees. :param theta: change in angle """ self.angle += theta def stop(self): """ Stop the Sprite's motion """ self.change_x = 0 self.change_y = 0 self.change_angle = 0 def _set_collision_radius(self, collision_radius: float): """ Set the collision radius. .. note:: Final collision checking is done via geometry that was set in the hit_box property. These points are used in the check_for_collision function. This collision_radius variable is used as a "pre-check." We do a super-fast check with collision_radius and see if the sprites are close. If they are, then we look at the geometry and figure if they really are colliding. :param float collision_radius: Collision radius """ self._collision_radius = collision_radius def _get_collision_radius(self): """ Get the collision radius. .. note:: Final collision checking is done via geometry that was set in get_points/set_points. These points are used in the check_for_collision function. This collision_radius variable is used as a "pre-check." We do a super-fast check with collision_radius and see if the sprites are close. If they are, then we look at the geometry and figure if they really are colliding. """ if not self._collision_radius: self._collision_radius = max(self.width, self.height) return self._collision_radius collision_radius = property(_get_collision_radius, _set_collision_radius) def __lt__(self, other): return self._texture.texture_id.value < other.texture.texture_id.value def clear_spatial_hashes(self): """ Search the sprite lists this sprite is a part of, and remove it from any spatial hashes it is a part of. """ for sprite_list in self.sprite_lists: if sprite_list._use_spatial_hash and sprite_list.spatial_hash is not None: try: sprite_list.spatial_hash.remove_object(self) except ValueError: print( "Warning, attempt to remove item from spatial hash that doesn't exist in the hash." ) def add_spatial_hashes(self): """ Add spatial hashes for this sprite in all the sprite lists it is part of. """ for sprite_list in self.sprite_lists: if sprite_list._use_spatial_hash: sprite_list.spatial_hash.insert_object_for_box(self) def _get_bottom(self) -> float: """ Return the y coordinate of the bottom of the sprite. """ points = self.get_adjusted_hit_box() # This happens if our point list is empty, such as a completely # transparent sprite. if len(points) == 0: return self.center_x my_min = points[0][1] for point in range(1, len(points)): my_min = min(my_min, points[point][1]) return my_min def _set_bottom(self, amount: float): """ Set the location of the sprite based on the bottom y coordinate. """ lowest = self._get_bottom() diff = lowest - amount self.center_y -= diff bottom = property(_get_bottom, _set_bottom) def _get_top(self) -> float: """ Return the y coordinate of the top of the sprite. """ points = self.get_adjusted_hit_box() # This happens if our point list is empty, such as a completely # transparent sprite. if len(points) == 0: return self.center_x my_max = points[0][1] for i in range(1, len(points)): my_max = max(my_max, points[i][1]) return my_max def _set_top(self, amount: float): """ The highest y coordinate. """ highest = self._get_top() diff = highest - amount self.center_y -= diff top = property(_get_top, _set_top) def _get_width(self) -> float: """ Get the width of the sprite. """ return self._width def _set_width(self, new_value: float): """ Set the width in pixels of the sprite. """ if new_value != self._width: self.clear_spatial_hashes() self._point_list_cache = None # If there is a hit box, rescale it to the new width if self._points: scale = new_value / self._width old_points = self._points self._points = [(point[0] * scale, point[1]) for point in old_points] self._width = new_value self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_size(self) width = property(_get_width, _set_width) def _get_height(self) -> float: """ Get the height in pixels of the sprite. """ return self._height def _set_height(self, new_value: float): """ Set the center x coordinate of the sprite. """ if new_value != self._height: self.clear_spatial_hashes() self._point_list_cache = None # If there is a hit box, rescale it to the new width if self._points: scale = new_value / self._height old_points = self._points self._points = [(point[0], point[1] * scale) for point in old_points] self._height = new_value self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_height(self) height = property(_get_height, _set_height) def _get_scale(self) -> float: """ Get the scale of the sprite. """ return self._scale def _set_scale(self, new_value: float): """ Set the center x coordinate of the sprite. """ if new_value != self._scale: self.clear_spatial_hashes() self._point_list_cache = None self._scale = new_value if self._texture: self._width = self._texture.width * self._scale self._height = self._texture.height * self._scale self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_size(self) scale = property(_get_scale, _set_scale) def rescale_relative_to_point(self, point: Point, factor: float) -> None: """ Rescale the sprite relative to a different point than its center. """ self.scale *= factor self.center_x = (self.center_x - point[0]) * factor + point[0] self.center_y = (self.center_y - point[1]) * factor + point[1] def _get_center_x(self) -> float: """ Get the center x coordinate of the sprite. """ return self._position[0] def _set_center_x(self, new_value: float): """ Set the center x coordinate of the sprite. """ if new_value != self._position[0]: self.clear_spatial_hashes() self._point_list_cache = None self._position = (new_value, self._position[1]) self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_location(self) center_x = property(_get_center_x, _set_center_x) def _get_center_y(self) -> float: """ Get the center y coordinate of the sprite. """ return self._position[1] def _set_center_y(self, new_value: float): """ Set the center y coordinate of the sprite. """ if new_value != self._position[1]: self.clear_spatial_hashes() self._point_list_cache = None self._position = (self._position[0], new_value) self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_location(self) center_y = property(_get_center_y, _set_center_y) def _get_change_x(self) -> float: """ Get the velocity in the x plane of the sprite. """ return self.velocity[0] def _set_change_x(self, new_value: float): """ Set the velocity in the x plane of the sprite. """ self.velocity[0] = new_value change_x = property(_get_change_x, _set_change_x) def _get_change_y(self) -> float: """ Get the velocity in the y plane of the sprite. """ return self.velocity[1] def _set_change_y(self, new_value: float): """ Set the velocity in the y plane of the sprite. """ self.velocity[1] = new_value change_y = property(_get_change_y, _set_change_y) def _get_angle(self) -> float: """ Get the angle of the sprite's rotation. """ return self._angle def _set_angle(self, new_value: float): """ Set the angle of the sprite's rotation. """ if new_value != self._angle: self.clear_spatial_hashes() self._angle = new_value self._point_list_cache = None for sprite_list in self.sprite_lists: sprite_list.update_angle(self) self.add_spatial_hashes() angle = property(_get_angle, _set_angle) def _to_radians(self) -> float: """ Converts the degrees representation of self.angle into radians. :return: float """ return self.angle / 180.0 * math.pi def _from_radians(self, new_value: float): """ Converts a radian value into degrees and stores it into angle. """ self.angle = new_value * 180.0 / math.pi radians = property(_to_radians, _from_radians) def _get_left(self) -> float: """ Return the x coordinate of the left-side of the sprite's hit box. """ points = self.get_adjusted_hit_box() # This happens if our point list is empty, such as a completely # transparent sprite. if len(points) == 0: return self.center_x my_min = points[0][0] for i in range(1, len(points)): my_min = min(my_min, points[i][0]) return my_min def _set_left(self, amount: float): """ The left most x coordinate. """ leftmost = self._get_left() diff = amount - leftmost self.center_x += diff left = property(_get_left, _set_left) def _get_right(self) -> float: """ Return the x coordinate of the right-side of the sprite's hit box. """ points = self.get_adjusted_hit_box() # This happens if our point list is empty, such as a completely # transparent sprite. if len(points) == 0: return self.center_x my_max = points[0][0] for point in range(1, len(points)): my_max = max(my_max, points[point][0]) return my_max def _set_right(self, amount: float): """ The right most x coordinate. """ rightmost = self._get_right() diff = rightmost - amount self.center_x -= diff right = property(_get_right, _set_right) def set_texture(self, texture_no: int): """ Sets texture by texture id. Should be renamed because it takes a number rather than a texture, but keeping this for backwards compatibility. """ if self.textures[texture_no] == self._texture: return texture = self.textures[texture_no] self.clear_spatial_hashes() self._point_list_cache = None self._texture = texture self._width = texture.width * self.scale self._height = texture.height * self.scale self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_texture(self) def _set_texture2(self, texture: Texture): """ Sets texture by texture id. Should be renamed but keeping this for backwards compatibility. """ if texture == self._texture: return assert (isinstance(texture, Texture)) self.clear_spatial_hashes() self._point_list_cache = None self._texture = texture self._width = texture.width * self.scale self._height = texture.height * self.scale self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_texture(self) def _get_texture(self): return self._texture texture = property(_get_texture, _set_texture2) def _get_texture_transform(self) -> Matrix3x3: return self._texture_transform def _set_texture_transform(self, m: Matrix3x3): self._texture_transform = m texture_transform = property(_get_texture_transform, _set_texture_transform) def _get_color(self) -> RGB: """ Return the RGB color associated with the sprite. """ return self._color def _set_color(self, color: Color): """ Set the current sprite color as a RGB value """ if color is None: raise ValueError("Color must be three or four ints from 0-255") if len(color) == 3: if self._color[0] == color[0] \ and self._color[1] == color[1] \ and self._color[2] == color[2]: return elif len(color) == 4: color = cast(List, color) # Prevent typing error if self._color[0] == color[0] \ and self._color[1] == color[1] \ and self._color[2] == color[2]\ and self.alpha == color[3]: return self.alpha = color[3] else: raise ValueError("Color must be three or four ints from 0-255") self._color = color[0], color[1], color[2] for sprite_list in self.sprite_lists: sprite_list.update_color(self) color = property(_get_color, _set_color) def _get_alpha(self) -> int: """ Return the alpha associated with the sprite. """ return self._alpha def _set_alpha(self, alpha: int): """ Set the current sprite color as a value """ if alpha < 0 or alpha > 255: raise ValueError( f"Invalid value for alpha. Must be 0 to 255, received {alpha}") self._alpha = alpha for sprite_list in self.sprite_lists: sprite_list.update_color(self) alpha = property(_get_alpha, _set_alpha) def register_sprite_list(self, new_list): """ Register this sprite as belonging to a list. We will automatically remove ourselves from the the list when kill() is called. """ self.sprite_lists.append(new_list) def register_physics_engine(self, physics_engine): """ Called by the Pymunk physics engine when this sprite is added to that physics engine. Lets the sprite know about the engine and remove itself if it gets deleted. """ self.physics_engines.append(physics_engine) def pymunk_moved(self, physics_engine, dx, dy, d_angle): """ Called by the pymunk physics engine if this sprite moves. """ pass def draw(self): """ Draw the sprite. """ if self._sprite_list is None: from arcade import SpriteList self._sprite_list = SpriteList() self._sprite_list.append(self) self._sprite_list.draw() def draw_hit_box(self, color: Color = BLACK, line_thickness: float = 1): """ Draw a sprite's hit-box. The 'hit box' drawing is cached, so if you change the color/line thickness later, it won't take. :param color: Color of box :param line_thickness: How thick the box should be """ if self._hit_box_shape is None: # Adjust the hitbox point_list = [] for point in self.hit_box: # Get a copy of the point point = [point[0], point[1]] # Scale the point if self.scale != 1: point[0] *= self.scale point[1] *= self.scale # Rotate the point (Don't, should already be rotated.) # if self.angle: # point = rotate_point(point[0], point[1], 0, 0, self.angle) point_list.append(point) shape = create_line_loop(point_list, color, line_thickness) self._hit_box_shape = ShapeElementList() self._hit_box_shape.append(shape) self._hit_box_shape.center_x = self.center_x self._hit_box_shape.center_y = self.center_y self._hit_box_shape.angle = self.angle self._hit_box_shape.draw() # point_list = self.get_adjusted_hit_box() # draw_polygon_outline(point_list, color, line_thickness) def update(self): """ Update the sprite. """ self.position = [ self._position[0] + self.change_x, self._position[1] + self.change_y ] self.angle += self.change_angle def on_update(self, delta_time: float = 1 / 60): """ Update the sprite. Similar to update, but also takes a delta-time. """ pass def update_animation(self, delta_time: float = 1 / 60): """ Override this to add code that will change what image is shown, so the sprite can be animated. :param float delta_time: Time since last update. """ pass def remove_from_sprite_lists(self): """ Remove the sprite from all sprite lists. """ if len(self.sprite_lists) > 0: # We can't modify a list as we iterate through it, so create a copy. sprite_lists = self.sprite_lists.copy() else: # If the list is a size 1, we don't need to copy sprite_lists = self.sprite_lists for sprite_list in sprite_lists: if self in sprite_list: sprite_list.remove(self) for engine in self.physics_engines: engine.remove_sprite(self) self.physics_engines.clear() self.sprite_lists.clear() def kill(self): """ Alias of `remove_from_sprite_lists` """ self.remove_from_sprite_lists() def collides_with_point(self, point: Point) -> bool: """Check if point is within the current sprite. :param Point point: Point to check. :return: True if the point is contained within the sprite's boundary. :rtype: bool """ from arcade.geometry import is_point_in_polygon x, y = point return is_point_in_polygon(x, y, self.get_adjusted_hit_box()) def collides_with_sprite(self, other: 'Sprite') -> bool: """Will check if a sprite is overlapping (colliding) another Sprite. :param Sprite other: the other sprite to check against. :return: True or False, whether or not they are overlapping. :rtype: bool """ from arcade import check_for_collision return check_for_collision(self, other) def collides_with_list(self, sprite_list: 'SpriteList') -> list: """Check if current sprite is overlapping with any other sprite in a list :param SpriteList sprite_list: SpriteList to check against :return: SpriteList of all overlapping Sprites from the original SpriteList :rtype: SpriteList """ from arcade import check_for_collision_with_list # noinspection PyTypeChecker return check_for_collision_with_list(self, sprite_list)
class Texture: """ Class that represents a texture. Usually created by the ``load_texture`` or ``load_textures`` commands. Attributes: :name: :image: :scale: :width: Width of the texture image in pixels :height: Height of the texture image in pixels """ def __init__(self, name, image=None): self.name = name self.image = image self.scale = 1 if image: self.width = image.width self.height = image.height else: self.width = 0 self.height = 0 self._sprite = None def draw(self, center_x: float, center_y: float, width: float, height: float, angle: float = 0, alpha: float = 1, transparent: bool = True, repeat_count_x=1, repeat_count_y=1): """ Args: center_x: center_y: width: height: angle: alpha: Currently unused. transparent: Currently unused. repeat_count_x: Currently unused. repeat_count_y: Currently unused. Returns: """ from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if self._sprite is None: self._sprite = Sprite() self._sprite._texture = self self._sprite.textures = [self] self._sprite_list = SpriteList() self._sprite_list.append(self._sprite) self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.width = width self._sprite.height = height self._sprite.angle = angle self._sprite_list.draw()
class Texture: """ Class that represents a texture. Usually created by the ``load_texture`` or ``load_textures`` commands. Attributes: :name: Unique name of the texture. Used by load_textures for caching. If you are manually creating a texture, you can just set this to whatever. :image: PIL image :width: Width of the texture image in pixels :height: Height of the texture image in pixels """ def __init__(self, name: str, image: PIL.Image = None): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList self.name = name self.image = image self._sprite: Optional[Sprite] = None self._sprite_list: Optional[SpriteList] = None self.hit_box_points = None @property def width(self) -> int: """ Width of the texture in pixels """ if self.image: return self.image.width else: return 0 @property def height(self) -> int: """ Height of the texture in pixels """ if self.image: return self.image.height else: return 0 def _create_cached_sprite(self): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if self._sprite is None: self._sprite = Sprite() self._sprite.texture = self self._sprite.textures = [self] self._sprite_list = SpriteList() self._sprite_list.append(self._sprite) def draw_sized(self, center_x: float, center_y: float, width: float, height: float, angle: float = 0, alpha: int = 255): self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.height = height self._sprite.width = width self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw() def draw_transformed(self, left: float, bottom: float, width: float, height: float, angle: float = 0, alpha: int = 255, texture_transform: Matrix3x3 = Matrix3x3()): self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = left + width / 2 self._sprite.center_y = bottom + height / 2 self._sprite.width = width self._sprite.height = height self._sprite.angle = angle self._sprite.alpha = alpha self._sprite.texture_transform = texture_transform self._sprite_list.draw() def draw_scaled(self, center_x: float, center_y: float, scale: float = 1.0, angle: float = 0, alpha: int = 255): """ Draw the texture :param center_x: x location of where to draw the texture :param center_y: y location of where to draw the texture :param scale: Scale to draw rectangle. If none, defaults to 1 :param angle: angle to rotate the texture :param alpha: transparency of texture. 0-255 """ self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.scale = scale self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw()
class Texture: """ Class that represents a texture. Usually created by the :class:`load_texture` or :class:`load_textures` commands. Attributes: :name: Unique name of the texture. Used by load_textures for caching. If you are manually creating a texture, you can just set this to whatever. :image: A :py:class:`PIL.Image.Image` object. :width: Width of the texture in pixels. :height: Height of the texture in pixels. """ def __init__(self, name: str, image: PIL.Image.Image = None, hit_box_algorithm: str = "Simple", hit_box_detail: float = 4.5): """ Create a texture, given a PIL Image object. :param str name: Name of texture. Used for caching, so must be unique for each texture. :param PIL.Image.Image image: Image to use as a texture. :param str hit_box_algorithm: One of 'None', 'Simple' or 'Detailed'. \ Defaults to 'Simple'. Use 'Simple' for the :data:`PhysicsEngineSimple`, \ :data:`PhysicsEnginePlatformer` \ and 'Detailed' for the :data:`PymunkPhysicsEngine`. .. figure:: images/hit_box_algorithm_none.png :width: 40% hit_box_algorithm = "None" .. figure:: images/hit_box_algorithm_simple.png :width: 55% hit_box_algorithm = "Simple" .. figure:: images/hit_box_algorithm_detailed.png :width: 75% hit_box_algorithm = "Detailed" :param float hit_box_detail: Float, defaults to 4.5. Used with 'Detailed' to hit box """ from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if image: assert isinstance(image, PIL.Image.Image) self.name = name self.image = image self._sprite: Optional[Sprite] = None self._sprite_list: Optional[SpriteList] = None self._hit_box_points = None if hit_box_algorithm != "Simple" and \ hit_box_algorithm != "Detailed" and \ hit_box_algorithm != "None": raise ValueError( "hit_box_algorithm must be 'Simple', 'Detailed', or 'None'.") self._hit_box_algorithm = hit_box_algorithm self._hit_box_detail = hit_box_detail @property def width(self) -> int: """ Width of the texture in pixels. """ if self.image: return self.image.width else: return 0 @property def height(self) -> int: """ Height of the texture in pixels. """ if self.image: return self.image.height else: return 0 @property def hit_box_points(self): if self._hit_box_points is not None: return self._hit_box_points else: if self._hit_box_algorithm == "Simple": self._hit_box_points = calculate_hit_box_points_simple( self.image) elif self._hit_box_algorithm == "Detailed": self._hit_box_points = calculate_hit_box_points_detailed( self.image, self._hit_box_detail) else: p1 = (-self.image.width / 2, -self.image.height / 2) p2 = (self.image.width / 2, -self.image.height / 2) p3 = (self.image.width / 2, self.image.height / 2) p4 = (-self.image.width / 2, self.image.height / 2) self._hit_box_points = p1, p2, p3, p4 return self._hit_box_points def _create_cached_sprite(self): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if self._sprite is None: self._sprite = Sprite() self._sprite.texture = self self._sprite.textures = [self] self._sprite_list = SpriteList() self._sprite_list.append(self._sprite) def draw_sized(self, center_x: float, center_y: float, width: float, height: float, angle: float = 0, alpha: int = 255): self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.height = height self._sprite.width = width self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw() def draw_transformed(self, left: float, bottom: float, width: float, height: float, angle: float = 0, alpha: int = 255, texture_transform: Matrix3x3 = Matrix3x3()): self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = left + width / 2 self._sprite.center_y = bottom + height / 2 self._sprite.width = width self._sprite.height = height self._sprite.angle = angle self._sprite.alpha = alpha self._sprite.texture_transform = texture_transform self._sprite_list.draw() def draw_scaled(self, center_x: float, center_y: float, scale: float = 1.0, angle: float = 0, alpha: int = 255): """ Draw the texture. :param float center_x: X location of where to draw the texture. :param float center_y: Y location of where to draw the texture. :param float scale: Scale to draw rectangle. Defaults to 1. :param float angle: Angle to rotate the texture by. :param int alpha: The transparency of the texture `(0-255)`. """ self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.scale = scale self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw()
class ForestKnightView(arcade.View): """ The main View class that runs the actual game code. """ def __init__(self): super().__init__() # Used to keep track of our scrolling self.view_bottom = 0 self.view_left = 0 # Sprites self.knight = None self.character_sprites = None self.enemy_sprites = None self.platforms = None self.foregrounds = None self.backgrounds = None self.ladders = None self.dont_touch = None self.collectibles = None self.collectibles_to_omit = None # Sounds self.collectible_sound = None self.gameover_sound = None # Images self.background_image = None # Physics Engine self.physics_engine = None # Level self.level = None # Other variables self.cur_viewport_coords = None def setup(self, level: int, load_game: bool = False, loaded_game_data: dict = None): """ Method that sets up the given level of the game. It also calls other setup methods used in the game. Send `load_game_data` only if the game is being loaded from the hard disk """ self.character_sprites = SpriteList() self.enemy_sprites = SpriteList() self.collectibles_to_omit = [] self.cur_viewport_coords = () self.level = level self.setup_characters() # We'll only load the game if this is NOT the first time playing it if load_game: self.load_game_data(loaded_game_data) self.setup_correct_viewport() self.setup_sprites(self.level) self.update_viewport() self.setup_physics_engine() self.setup_sounds() self.setup_images() def setup_sprites(self, level: int): """Method that sets up all the Sprites (except for Knight and other Enemies)""" loaded_sprites = level_loader(level, self.collectibles_to_omit) self.platforms = loaded_sprites["Platforms"] self.foregrounds = loaded_sprites["Foregrounds"] self.backgrounds = loaded_sprites["Backgrounds"] self.ladders = loaded_sprites["Ladders"] self.dont_touch = loaded_sprites["Dont-Touch"] self.collectibles = loaded_sprites["Collectibles"] def setup_characters(self): """Method to set up the Knight and other Enemies""" self.knight = Knight(pos_x=KNIGHT_X, pos_y=KNIGHT_Y) self.character_sprites.append(self.knight) self.character_sprites.preload_textures(self.knight.textures) for pos in ZOMBIE_MALE_LEVEL_1_POSITIONS: pos_x = pos[0] pos_y = pos[1] enemy = ZombieMale(pos_x, pos_y) self.enemy_sprites.append(enemy) for enemy in self.enemy_sprites: self.enemy_sprites.preload_textures(enemy.textures) def setup_physics_engine(self): """Method to set up arcade.PhysicsEnginePlatformer""" self.physics_engine = PhysicsEnginePlatformer(self.knight, self.platforms, gravity_constant=GRAVITY, ladders=self.ladders) def setup_sounds(self): """Method that loads all the sounds required in the game""" self.collectible_sound = load_sound(f"{AUDIO_DIR}/coin1.wav") self.gameover_sound = load_sound(f"{AUDIO_DIR}/lose1.wav") self.background_music = load_sound(f"{AUDIO_DIR}/backgroundMusic2.mp3") # We'll play the background music during initial setup # self.background_play = Sound.play(self.background_music, volume=0.2) self.knight.setup_sounds() def setup_images(self, level: int = 1): """Method to set up background image of the current level""" if level == 1: self.background_image = arcade.load_texture( f"{IMAGES_DIR}/backgrounds/BG.png") def setup_correct_viewport(self): """Method that sets up the viewports that was saved""" arcade.set_viewport(*self.cur_viewport_coords) def delete_all_sprites(self): """Deletes all the game sprites when switching to Main Menu to save on resources""" self.knight.kill() def on_key_press(self, symbol: int, modifiers: int): """Method that handles what happens when a key is pressed down""" # Knight movement and attack if symbol == arcade.key.RIGHT: self.knight.change_x = self.knight.speed elif symbol == arcade.key.LEFT: self.knight.change_x = -(self.knight.speed) elif symbol == arcade.key.UP: if (self.physics_engine.can_jump() and not self.physics_engine.is_on_ladder()): self.knight.change_y = self.knight.jump_speed self.knight.jump_sound.play() elif self.physics_engine.is_on_ladder(): self.knight.change_y = self.knight.speed elif symbol == arcade.key.DOWN: if self.physics_engine.is_on_ladder(): self.knight.change_y = -(self.knight.speed) elif symbol == arcade.key.SPACE: self.knight.is_attacking = True if symbol in [ arcade.key.DOWN, arcade.key.LEFT, arcade.key.RIGHT, ]: self.knight.is_moving = True # Other key-based actions if symbol == arcade.key.ESCAPE: self.pause() if symbol == arcade.key.V: print(self.knight.position) return super().on_key_press(symbol, modifiers) def on_key_release(self, symbol: int, modifiers: int): """Method that handles what happens when a key is released""" if symbol == arcade.key.RIGHT: self.knight.change_x = 0 elif symbol == arcade.key.LEFT: self.knight.change_x = 0 elif (symbol in [arcade.key.UP, arcade.key.DOWN] and self.physics_engine.is_on_ladder()): self.knight.change_y = 0 elif symbol == arcade.key.SPACE: self.knight.is_attacking = False if symbol in [ arcade.key.DOWN, arcade.key.LEFT, arcade.key.RIGHT, ]: self.knight.is_moving = False return super().on_key_release(symbol, modifiers) def pause(self): """Method that will bring up a screen that pauses the game""" pause_view = PauseView(self) self.window.show_view(pause_view) def load_game_data(self, data: dict): """ Method that takes all the data from the loader function from the game saving utility and correctly sets up the game. This method will only be called if the game is NOT being run for the first time """ self.knight.health = data["health"] self.knight.position = data["position"] self.knight.state = data["knight_state"] self.knight.score = data["score"] self.knight.texture = data["texture"] self.collectibles_to_omit = data["collectibles_to_omit"] self.cur_viewport_coords = data["viewport_coords"] def update_viewport(self): """Method that manages and updates the viewport according to where the Knight is""" # Track if we need to change the viewport changed = False # Scroll left left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN if self.knight.left < left_boundary: self.view_left -= left_boundary - self.knight.left changed = True # Scroll right right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN if self.knight.right > right_boundary: self.view_left += self.knight.right - right_boundary changed = True # Scroll up top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN if self.knight.top > top_boundary: self.view_bottom += self.knight.top - top_boundary changed = True # Scroll down bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN if self.knight.bottom < bottom_boundary: self.view_bottom -= bottom_boundary - self.knight.bottom changed = True if changed: # Only scroll to integers. Otherwise we end up with pixels that # don't line up on the screen self.view_bottom = int(self.view_bottom) self.view_left = int(self.view_left) # Do the scrolling arcade.set_viewport( self.view_left, SCREEN_WIDTH + self.view_left, self.view_bottom, SCREEN_HEIGHT + self.view_bottom, ) def on_update(self, delta_time: float): """Method that is the main game loop and contains most game logic""" self.character_sprites.update_animation() self.character_sprites.update() self.enemy_sprites.update_animation() self.enemy_sprites.update() self.physics_engine.update() if self.knight.is_dying: self.knight.die() if self.knight.is_attacking: self.knight.attack() if (not self.knight.is_moving and not self.knight.is_attacking and not self.knight.is_dying): self.knight.idle_animation() # Managing viewport self.update_viewport() # Collecting coins logic coins_collected = check_for_collision_with_list( self.knight, self.collectibles) for coin in coins_collected: self.collectibles_to_omit.append(coin.position) coin.kill() self.knight.score += 1 self.collectible_sound.play() for enemy in self.enemy_sprites: # Enemies will always be on the lookout for the Knight enemy.detect_knight(self.knight) return super().on_update(delta_time) def on_draw(self, draw_stats=True): """ We actually have to draw to the display to show anything on the screen. This method handles all the drawing in the game """ start_render() # Drawing our loaded background images and setting it arcade.draw_texture_rectangle( (SCREEN_WIDTH // 2) + self.view_left, (SCREEN_HEIGHT // 2) + self.view_bottom, SCREEN_WIDTH, SCREEN_HEIGHT, self.background_image, ) self.platforms.draw() self.backgrounds.draw() self.ladders.draw() self.collectibles.draw() self.character_sprites.draw() self.enemy_sprites.draw() self.dont_touch.draw() self.foregrounds.draw() # Drawing the Knight's stats if draw_stats: arcade.draw_text( f"Score: {self.knight.score}", self.view_left, self.view_bottom + 15, arcade.color.CHROME_YELLOW, 15, ) arcade.draw_text( f"Health: {self.knight.health}", self.view_left, self.view_bottom, arcade.color.ROSE_RED, 15, ) return super().on_draw()
class Texture: """ Class that represents a texture. Usually created by the ``load_texture`` or ``load_textures`` commands. Attributes: :name: :image: :scale: :width: Width of the texture image in pixels :height: Height of the texture image in pixels """ def __init__(self, name: str, image=None): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList self.name = name self.texture = None self.image = image # self.scale = 1 self._sprite: Optional[Sprite] = None self._sprite_list: Optional[SpriteList] = None self.unscaled_hitbox_points = None # @property # def scaled_width(self) -> float: # return self.image.width * self.scale # # @property # def scaled_height(self) -> float: # return self.image.height * self.scale @property def unscaled_width(self) -> int: return self.image.width * self.scale @property def unscaled_height(self) -> int: return self.image.height * self.scale # noinspection PyUnusedLocal def draw(self, center_x: float, center_y: float, width: float = None, height: float = None, angle: float = 0, alpha: int = 255): """ Draw the texture :param center_x: x location of where to draw the texture :param center_y: y location of where to draw the texture :param width: width to draw rectangle. If none, calculated from image size and scale :param height: height to draw rectangle. If none, calculated from image size and scale :param angle: angle to rotate the texture :param alpha: transparency of texture. 0-255 """ from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if self._sprite is None: self._sprite = Sprite() self._sprite._texture = self self._sprite.textures = [self] self._sprite_list = SpriteList() self._sprite_list.append(self._sprite) self._sprite.center_x = center_x self._sprite.center_y = center_y if width: self._sprite.width = width else: self._sprite.width = self.image.width * self.scale if height: self._sprite.height = height else: self._sprite.height = self.image.height * self.scale self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw()
class Sprite: """ Class that represents a 'sprite' on-screen. Most games center around sprites. For examples on how to use this class, see: http://arcade.academy/examples/index.html#sprites Attributes: :alpha: Transparency of sprite. 0 is invisible, 255 is opaque. :angle: Rotation angle in degrees. :radians: Rotation angle in radians. :bottom: Set/query the sprite location by using the bottom coordinate. \ This will be the 'y' of the bottom of the sprite. :boundary_left: Used in movement. Left boundary of moving sprite. :boundary_right: Used in movement. Right boundary of moving sprite. :boundary_top: Used in movement. Top boundary of moving sprite. :boundary_bottom: Used in movement. Bottom boundary of moving sprite. :center_x: X location of the center of the sprite :center_y: Y location of the center of the sprite :change_x: Movement vector, in the x direction. :change_y: Movement vector, in the y direction. :change_angle: Change in rotation. :color: Color tint the sprite :collision_radius: Used as a fast-check to see if this item is close \ enough to another item. If this check works, we do a slower more accurate check. \ You probably don't want to use this field. Instead, set points in the \ hit box. :cur_texture_index: Index of current texture being used. :guid: Unique identifier for the sprite. Useful when debugging. :height: Height of the sprite. :force: Force being applied to the sprite. Useful when used with Pymunk \ for physics. :left: Set/query the sprite location by using the left coordinate. This \ will be the 'x' of the left of the sprite. :points: Points, in relation to the center of the sprite, that are used \ for collision detection. Arcade defaults to creating points for a rectangle \ that encompass the image. If you are creating a ramp or making better \ hit-boxes, you can custom-set these. :position: A list with the (x, y) of where the sprite is. :repeat_count_x: Unused :repeat_count_y: Unused :right: Set/query the sprite location by using the right coordinate. \ This will be the 'y=x' of the right of the sprite. :rotation_point: The point relative to the center of the sprite which the \ sprite rotates around, default is (0, 0). :sprite_lists: List of all the sprite lists this sprite is part of. :texture: `Texture` class with the current texture. :textures: List of textures associated with this sprite. :top: Set/query the sprite location by using the top coordinate. This \ will be the 'y' of the top of the sprite. :scale: Scale the image up or down. Scale of 1.0 is original size, 0.5 \ is 1/2 height and width. :velocity: Change in x, y expressed as a list. (0, 0) would be not moving. :width: Width of the sprite It is common to over-ride the `update` method and provide mechanics on movement or other sprite updates. """ def __init__(self, filename: str = None, scale: float = 1, image_x: float = 0, image_y: float = 0, image_width: float = 0, image_height: float = 0, center_x: float = 0, center_y: float = 0, repeat_count_x: int = 1, repeat_count_y: int = 1): """ Create a new sprite. Args: filename (str): Filename of an image that represents the sprite. scale (float): Scale the image up or down. Scale of 1.0 is none. image_x (float): X offset to sprite within sprite sheet. image_y (float): Y offset to sprite within sprite sheet. image_width (float): Width of the sprite image_height (float): Height of the sprite center_x (float): Location of the sprite center_y (float): Location of the sprite """ if image_width < 0: raise ValueError("Width of image can't be less than zero.") if image_height < 0: raise ValueError( "Height entered is less than zero. Height must be a positive float." ) if image_width == 0 and image_height != 0: raise ValueError("Width can't be zero.") if image_height == 0 and image_width != 0: raise ValueError("Height can't be zero.") self.sprite_lists: List[Any] = [] self._texture: Optional[Texture] if filename is not None: try: self._texture = load_texture(filename, image_x, image_y, image_width, image_height) except Exception as e: print(f"Unable to load {filename} {e}") self._texture = None if self._texture: self.textures = [self._texture] # Ignore the texture's scale and use ours self._width = self._texture.width * scale self._height = self._texture.height * scale else: self.textures = [] self._width = 0 self._height = 0 else: self.textures = [] self._texture = None self._width = 0 self._height = 0 self.cur_texture_index = 0 self._scale = scale self._position = (center_x, center_y) self._angle = 0.0 self.rotation_point = [0.0, 0.0] self.velocity = [0.0, 0.0] self.change_angle = 0.0 self.boundary_left = None self.boundary_right = None self.boundary_top = None self.boundary_bottom = None self.properties: Dict[str, Any] = {} self._alpha = 255 self._collision_radius: Optional[float] = None self._color: RGB = (255, 255, 255) self._points: Optional[List[List[float]]] = None if self._texture: self._points = self._texture.hit_box_points self._point_list_cache: Optional[List[List[float]]] = None self.force = [0, 0] self.guid: Optional[str] = None self.repeat_count_x = repeat_count_x self.repeat_count_y = repeat_count_y # Used if someone insists on doing a sprite.draw() self._sprite_list = None def append_texture(self, texture: Texture): """ Appends a new texture to the list of textures that can be applied to this sprite. :param Texture texture: Texture to add ot the list of available textures """ self.textures.append(texture) def _get_position(self) -> Tuple[float, float]: """ Get the center x and y coordinates of the sprite. Returns: (center_x, center_y) """ return self._position def _set_position(self, new_value: Tuple[float, float]): """ Set the center x and y coordinates of the sprite. Args: new_value: Returns: """ if new_value[0] != self._position[0] or new_value[1] != self._position[ 1]: self.clear_spatial_hashes() self._point_list_cache = None self._position = new_value self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_location(self) position = property(_get_position, _set_position) def set_position(self, center_x: float, center_y: float): """ Set a sprite's position :param float center_x: New x position of sprite :param float center_y: New y position of sprite """ self._set_position((center_x, center_y)) def set_points(self, points: List[List[float]]): """ Set a sprite's hitbox """ from warnings import warn warn('set_points has been deprecated. Use set_hit_box instead.', DeprecationWarning) self._points = points def get_points(self) -> List[List[float]]: """ Get the points that make up the hit box for the rect that makes up the sprite, including rotation and scaling. """ from warnings import warn warn('get_points has been deprecated. Use get_hit_box instead.', DeprecationWarning) return self.get_adjusted_hit_box() points = property(get_points, set_points) def set_hit_box(self, points: List[List[float]]): """ Set a sprite's hit box. When setting the hitbox, assume a sprite scaling of 1.0. Points will be scaled with get_adjusted_hit_box. """ self._points = points def get_hit_box(self) -> Optional[List[List[float]]]: """ Get a sprite's hit box """ return self._points hit_box = property(get_hit_box, set_hit_box) def get_adjusted_hit_box(self) -> List[List[float]]: """ Get the points that make up the hit box for the rect that makes up the sprite, including rotation and scaling. """ # If we've already calculated the adjusted hit box, use the cached version if self._point_list_cache is not None: return self._point_list_cache # If there is no hitbox, use the width/height to get one if self._points is None and self._texture: self._points = self._texture.hit_box_points if self._points is None and self._width: x1, y1 = -self._width / 2, -self._height / 2 x2, y2 = +self._width / 2, -self._height / 2 x3, y3 = +self._width / 2, +self._height / 2 x4, y4 = -self._width / 2, +self._height / 2 self._points = [[x1, y1], [x2, y2], [x3, y3], [x4, y4]] if self._points is None and self.texture is not None: self._points = self.texture.hit_box_points if self._points is None: raise ValueError( "Error trying to get the hit box of a sprite, when no hit box is set.\nPlease make sure the " "Sprite.texture is set to a texture before trying to draw or do collision testing.\n" "Alternatively, manually call Sprite.set_hit_box with points for your hitbox." ) # Adjust the hitbox point_list = [] for point_idx in range(len(self._points)): # Get the point point = [self._points[point_idx][0], self._points[point_idx][1]] # Scale the point if self.scale != 1: point[0] *= self.scale point[1] *= self.scale # Rotate the point if self.angle: point = rotate_point(point[0], point[1], 0, 0, self.angle) # Offset the point point = [point[0] + self.center_x, point[1] + self.center_y] point_list.append(point) # Cache the results self._point_list_cache = point_list # if self.texture: # print(self.texture.name, self._point_list_cache) return self._point_list_cache def forward(self, speed: float = 1.0): """ Set a Sprite's position to speed by its angle :param speed: speed factor """ self.change_x += math.cos(self.radians) * speed self.change_y += math.sin(self.radians) * speed def reverse(self, speed: float = 1.0): self.forward(-speed) def strafe(self, speed: float = 1.0): """ Set a sprites position perpendicular to its angle by speed :param speed: speed factor """ self.change_x += -math.sin(self.radians) * speed self.change_y += math.cos(self.radians) * speed def turn_right(self, theta: float = 90): self.angle -= theta def turn_left(self, theta: float = 90): self.angle += theta def stop(self): """ Stop the Sprite's motion """ self.change_x = 0 self.change_y = 0 self.change_angle = 0 def _set_collision_radius(self, collision_radius: float): """ Set the collision radius. .. note:: Final collision checking is done via geometry that was set in the hit_box property. These points are used in the check_for_collision function. This collision_radius variable is used as a "pre-check." We do a super-fast check with collision_radius and see if the sprites are close. If they are, then we look at the geometry and figure if they really are colliding. :param float collision_radius: Collision radius """ self._collision_radius = collision_radius def _get_collision_radius(self): """ Get the collision radius. .. note:: Final collision checking is done via geometry that was set in get_points/set_points. These points are used in the check_for_collision function. This collision_radius variable is used as a "pre-check." We do a super-fast check with collision_radius and see if the sprites are close. If they are, then we look at the geometry and figure if they really are colliding. """ if not self._collision_radius: self._collision_radius = max(self.width, self.height) return self._collision_radius collision_radius = property(_get_collision_radius, _set_collision_radius) def __lt__(self, other): return self._texture.texture_id.value < other.texture.texture_id.value def clear_spatial_hashes(self): """ Search the sprite lists this sprite is a part of, and remove it from any spatial hashes it is a part of. """ for sprite_list in self.sprite_lists: if sprite_list.use_spatial_hash and sprite_list.spatial_hash is not None: try: sprite_list.spatial_hash.remove_object(self) except ValueError: print( "Warning, attempt to remove item from spatial hash that doesn't exist in the hash." ) def add_spatial_hashes(self): for sprite_list in self.sprite_lists: if sprite_list.use_spatial_hash: sprite_list.spatial_hash.insert_object_for_box(self) def _get_bottom(self) -> float: """ Return the y coordinate of the bottom of the sprite. """ points = self.get_adjusted_hit_box() my_min = points[0][1] for point in range(1, len(points)): my_min = min(my_min, points[point][1]) return my_min def _set_bottom(self, amount: float): """ Set the location of the sprite based on the bottom y coordinate. """ lowest = self._get_bottom() diff = lowest - amount self.center_y -= diff bottom = property(_get_bottom, _set_bottom) def _get_top(self) -> float: """ Return the y coordinate of the top of the sprite. """ points = self.get_adjusted_hit_box() my_max = points[0][1] for i in range(1, len(points)): my_max = max(my_max, points[i][1]) return my_max def _set_top(self, amount: float): """ The highest y coordinate. """ highest = self._get_top() diff = highest - amount self.center_y -= diff top = property(_get_top, _set_top) def _get_width(self) -> float: """ Get the width of the sprite. """ return self._width def _set_width(self, new_value: float): """ Set the width in pixels of the sprite. """ if new_value != self._width: self.clear_spatial_hashes() self._point_list_cache = None self._width = new_value self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_size(self) width = property(_get_width, _set_width) def _get_height(self) -> float: """ Get the height in pixels of the sprite. """ return self._height def _set_height(self, new_value: float): """ Set the center x coordinate of the sprite. """ if new_value != self._height: self.clear_spatial_hashes() self._point_list_cache = None self._height = new_value self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_height(self) height = property(_get_height, _set_height) def _get_scale(self) -> float: """ Get the scale of the sprite. """ return self._scale def _set_scale(self, new_value: float): """ Set the center x coordinate of the sprite. """ if new_value != self._scale: self.clear_spatial_hashes() self._point_list_cache = None self._scale = new_value if self._texture: self._width = self._texture.width * self._scale self._height = self._texture.height * self._scale self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_size(self) scale = property(_get_scale, _set_scale) def rescale_relative_to_point(self, point: Point, factor: float) -> None: """ Rescale the sprite relative to a different point than its center. """ self.scale *= factor self.center_x = (self.center_x - point[0]) * factor + point[0] self.center_y = (self.center_y - point[1]) * factor + point[1] def _get_center_x(self) -> float: """ Get the center x coordinate of the sprite. """ return self._position[0] def _set_center_x(self, new_value: float): """ Set the center x coordinate of the sprite. """ if new_value != self._position[0]: self.clear_spatial_hashes() self._point_list_cache = None self._position = (new_value, self._position[1]) self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_location(self) center_x = property(_get_center_x, _set_center_x) def _get_center_y(self) -> float: """ Get the center y coordinate of the sprite. """ return self._position[1] def _set_center_y(self, new_value: float): """ Set the center y coordinate of the sprite. """ if new_value != self._position[1]: self.clear_spatial_hashes() self._point_list_cache = None self._position = (self._position[0], new_value) self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_location(self) center_y = property(_get_center_y, _set_center_y) def _get_change_x(self) -> float: """ Get the velocity in the x plane of the sprite. """ return self.velocity[0] def _set_change_x(self, new_value: float): """ Set the velocity in the x plane of the sprite. """ self.velocity[0] = new_value change_x = property(_get_change_x, _set_change_x) def _get_change_y(self) -> float: """ Get the velocity in the y plane of the sprite. """ return self.velocity[1] def _set_change_y(self, new_value: float): """ Set the velocity in the y plane of the sprite. """ self.velocity[1] = new_value change_y = property(_get_change_y, _set_change_y) def _get_angle(self) -> float: """ Get the angle of the sprite's rotation. """ return self._angle def _set_angle(self, new_value: float): """ Set the angle of the sprite's rotation. """ if new_value != self._angle: self.clear_spatial_hashes() self._angle = new_value self._point_list_cache = None self.add_spatial_hashes() rotate_x, rotate_y = self.rotation_point if rotate_x or rotate_y: sprite_rotate = rotate_point(self.center_x, self.center_y, rotate_x, rotate_y, new_value) self.set_position(sprite_rotate[0], sprite_rotate[1]) for sprite_list in self.sprite_lists: sprite_list.update_angle(self) angle = property(_get_angle, _set_angle) def _to_radians(self) -> float: """ Converts the degrees representation of self.angle into radians. :return: float """ return self.angle / 180.0 * math.pi def _from_radians(self, new_value: float): """ Converts a radian value into degrees and stores it into angle. """ self.angle = new_value * 180.0 / math.pi radians = property(_to_radians, _from_radians) def _get_left(self) -> float: """ Return the x coordinate of the left-side of the sprite's hit box. """ points = self.get_adjusted_hit_box() my_min = points[0][0] for i in range(1, len(points)): my_min = min(my_min, points[i][0]) return my_min def _set_left(self, amount: float): """ The left most x coordinate. """ leftmost = self._get_left() diff = amount - leftmost self.center_x += diff left = property(_get_left, _set_left) def _get_right(self) -> float: """ Return the x coordinate of the right-side of the sprite's hit box. """ points = self.get_adjusted_hit_box() my_max = points[0][0] for point in range(1, len(points)): my_max = max(my_max, points[point][0]) return my_max def _set_right(self, amount: float): """ The right most x coordinate. """ rightmost = self._get_right() diff = rightmost - amount self.center_x -= diff right = property(_get_right, _set_right) def get_rotation_point(self): """ Return the x and y offset of the rotation point. """ return self.rotation_point def set_rotation_point(self, new_value: List[float]): """ Set the x and y offset of the rotation point to new_value. """ if new_value != self.rotation_point: self.rotation_point = new_value def set_texture(self, texture_no: int): """ Sets texture by texture id. Should be renamed because it takes a number rather than a texture, but keeping this for backwards compatibility. """ if self.textures[texture_no] == self._texture: return texture = self.textures[texture_no] self.clear_spatial_hashes() self._point_list_cache = None self._texture = texture self._width = texture.width * self.scale self._height = texture.height * self.scale self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_texture(self) def _set_texture2(self, texture: Texture): """ Sets texture by texture id. Should be renamed but keeping this for backwards compatibility. """ if texture == self._texture: return self.clear_spatial_hashes() self._point_list_cache = None self._texture = texture self._width = texture.width * self.scale self._height = texture.height * self.scale self.add_spatial_hashes() for sprite_list in self.sprite_lists: sprite_list.update_texture(self) def _get_texture(self): return self._texture texture = property(_get_texture, _set_texture2) def _get_color(self) -> RGB: """ Return the RGB color associated with the sprite. """ return self._color def _set_color(self, color: RGB): """ Set the current sprite color as a RGB value """ self._color = color for sprite_list in self.sprite_lists: sprite_list.update_color(self) color = property(_get_color, _set_color) def _get_alpha(self) -> int: """ Return the alpha associated with the sprite. """ return self._alpha def _set_alpha(self, alpha: int): """ Set the current sprite color as a value """ if alpha < 0 or alpha > 255: raise ValueError( f"Invalid value for alpha. Must be 0 to 255, received {alpha}") self._alpha = alpha for sprite_list in self.sprite_lists: sprite_list.update_color(self) alpha = property(_get_alpha, _set_alpha) def register_sprite_list(self, new_list): """ Register this sprite as belonging to a list. We will automatically remove ourselves from the the list when kill() is called. """ self.sprite_lists.append(new_list) def draw(self): """ Draw the sprite. """ if self._sprite_list is None: from arcade import SpriteList self._sprite_list = SpriteList() self._sprite_list.append(self) self._sprite_list.draw() def draw_hit_box(self, color, line_thickness): points = self.get_adjusted_hit_box() draw_polygon_outline(points, color, line_thickness) def update(self): """ Update the sprite. """ self.position = [ self._position[0] + self.change_x, self._position[1] + self.change_y ] self.angle += self.change_angle def on_update(self, delta_time: float = 1 / 60): """ Update the sprite. Similar to update, but also takes a delta-time. """ pass def update_animation(self, delta_time: float = 1 / 60): """ Override this to add code that will change what image is shown, so the sprite can be animated. """ pass def remove_from_sprite_lists(self): """ Remove the sprite from all sprite lists. """ for sprite_list in self.sprite_lists: if self in sprite_list: sprite_list.remove(self) self.sprite_lists.clear() def kill(self): """ Alias of `remove_from_sprite_lists` """ self.remove_from_sprite_lists() def collides_with_point(self, point: Point) -> bool: """Check if point is within the current sprite. Args: self: Current sprite point: Point to check. Returns: True if the point is contained within the sprite's boundary. """ from arcade.geometry import is_point_in_polygon x, y = point return is_point_in_polygon(x, y, self.get_adjusted_hit_box()) def collides_with_sprite(self, other: 'Sprite') -> bool: """Will check if a sprite is overlapping (colliding) another Sprite. Args: self: Current Sprite. other: The other sprite to check against. Returns: True or False, whether or not they are overlapping. """ from arcade import check_for_collision return check_for_collision(self, other) def collides_with_list(self, sprite_list: 'SpriteList') -> list: """Check if current sprite is overlapping with any other sprite in a list Args: self: current Sprite sprite_list: SpriteList to check against Returns: SpriteList of all overlapping Sprites from the original SpriteList """ from arcade import check_for_collision_with_list # noinspection PyTypeChecker return check_for_collision_with_list(self, sprite_list)
class Texture: """ Class that represents a texture. Usually created by the ``load_texture`` or ``load_textures`` commands. Attributes: :id: ID of the texture as assigned by OpenGL :width: Width of the texture image in pixels :height: Height of the texture image in pixels """ def __init__(self, texture_id: int, width: float, height: float, file_name: str): """ Args: :texture_id (str): Id of the texture. :width (int): Width of the texture. :height (int): Height of the texture. Raises: :ValueError: >>> texture_id = Texture(0, 10, -10) Traceback (most recent call last): ... ValueError: Height entered is less than zero. Height must be a positive float. >>> texture_id = Texture(0, -10, 10) Traceback (most recent call last): ... ValueError: Width entered is less than zero. Width must be a positive float. """ # Check values before attempting to create Texture object if height < 0: raise ValueError("Height entered is less than zero. Height must " "be a positive float.") if width < 0: raise ValueError("Width entered is less than zero. Width must be " "a positive float.") # Values seem to be clear, create object self.texture_id = texture_id self.width = width self.height = height self.texture_name = file_name self._sprite = None self._sprite_list = None def draw(self, center_x: float, center_y: float, width: float, height: float, angle: float=0, alpha: float=1, transparent: bool=True, repeat_count_x=1, repeat_count_y=1): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if self._sprite == None: self._sprite = Sprite() self._sprite.texture = self self._sprite.textures = [self] self._sprite_list = SpriteList() self._sprite_list.append(self._sprite) self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.width = width self._sprite.height = height self._sprite.angle = angle self._sprite_list.draw()
class Texture: """ Class that represents a texture. Usually created by the ``load_texture`` or ``load_textures`` commands. Attributes: :name: :image: :width: Width of the texture image in pixels :height: Height of the texture image in pixels """ def __init__(self, name: str, image=None): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList self.name = name self.texture = None self.image = image self._sprite: Optional[Sprite] = None self._sprite_list: Optional[SpriteList] = None self.hit_box_points = None @property def width(self) -> int: """ Width of the texture in pixels """ return self.image.width @property def height(self) -> int: """ Height of the texture in pixels """ return self.image.height def _create_cached_sprite(self): from arcade.sprite import Sprite from arcade.sprite_list import SpriteList if self._sprite is None: self._sprite = Sprite() self._sprite._texture = self self._sprite.textures = [self] self._sprite_list = SpriteList() self._sprite_list.append(self._sprite) def draw_sized(self, center_x: float, center_y: float, width: float, height: float, angle: float, alpha: int = 255): self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.height = height self._sprite.width = width self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw() def draw_scaled(self, center_x: float, center_y: float, scale: float = 1.0, angle: float = 0, alpha: int = 255): """ Draw the texture :param center_x: x location of where to draw the texture :param center_y: y location of where to draw the texture :param scale: Scale to draw rectangle. If none, defaults to 1 :param angle: angle to rotate the texture :param alpha: transparency of texture. 0-255 """ self._create_cached_sprite() if self._sprite and self._sprite_list: self._sprite.center_x = center_x self._sprite.center_y = center_y self._sprite.scale = scale self._sprite.angle = angle self._sprite.alpha = alpha self._sprite_list.draw()