from __future__ import annotations
import math
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from arcade import Texture, load_texture
from arcade.hitbox import HitBox, RotatableHitBox
from arcade.texture import get_default_texture
from arcade.types import PathOrTexture, Point
from arcade.gl.types import OpenGlFilter, BlendFunction
from .base import BasicSprite
from .mixins import PymunkMixin
if TYPE_CHECKING: # handle import cycle caused by type hinting
from arcade.sprite_list import SpriteList
__all__ = ["Sprite"]
[docs]
class Sprite(BasicSprite, PymunkMixin):
"""
Sprites are used to render image data to the screen & perform collisions.
Most games center around sprites. They are most frequently used as follows:
1. Create ``Sprite`` instances from image data
2. Add the sprites to a :py:class:`~arcade.SpriteList` instance
3. Call :py:meth:`SpriteList.draw() <arcade.SpriteList.draw>` on the
instance inside your ``on_draw`` method.
For runnable examples of how to do this, please see arcade's
:ref:`built-in Sprite examples <sprites>`.
.. tip:: Advanced users should see :py:class:`~arcade.BasicSprite`
It uses fewer resources at the cost of having fewer features.
:param path_or_texture: Path to an image file, or a texture object.
:param center_x: Location of the sprite in pixels.
:param center_y: Location of the sprite in pixels.
:param scale: Show the image at this many times its original size.
:param angle: The initial rotation of the sprite in degrees
"""
__slots__ = (
"_velocity",
"change_angle",
"_properties",
"boundary_left",
"boundary_right",
"boundary_top",
"boundary_bottom",
"textures",
"cur_texture_index",
"_hit_box",
"physics_engines",
"_sprite_list",
"guid",
"force",
)
def __init__(
self,
path_or_texture: Optional[PathOrTexture] = None,
scale: float = 1.0,
center_x: float = 0.0,
center_y: float = 0.0,
angle: float = 0.0,
**kwargs: Any,
):
if isinstance(path_or_texture, Texture):
_texture = path_or_texture
_textures = [_texture]
elif isinstance(path_or_texture, (str, Path)):
_texture = load_texture(path_or_texture)
_textures = [_texture]
else:
_texture = get_default_texture()
# Backwards compatibility:
# When applying default texture we don't want
# it part of the animating ones
_textures = []
super().__init__(
_texture,
scale=scale,
center_x=center_x,
center_y=center_y,
**kwargs,
)
PymunkMixin.__init__(self)
self._angle = angle
# Movement
self._velocity = 0.0, 0.0
self.change_angle: float = 0.0
# Custom sprite properties
self._properties: Optional[Dict[str, Any]] = None
# Boundaries for moving platforms in tilemaps
self.boundary_left: Optional[float] = None
self.boundary_right: Optional[float] = None
self.boundary_top: Optional[float] = None
self.boundary_bottom: Optional[float] = None
self.cur_texture_index: int = 0
self.textures: List[Texture] = _textures
self.physics_engines: List[Any] = []
self._sprite_list: Optional[SpriteList] = None
# Debug properties
self.guid: Optional[str] = None
self._hit_box: RotatableHitBox = self._hit_box.create_rotatable(
angle=self._angle
)
self._width = self._texture.width * scale
self._height = self._texture.height * scale
# --- Properties ---
@property
def angle(self) -> float:
"""
Get or set the rotation or the sprite.
The value is in degrees and is clockwise.
"""
return self._angle
@angle.setter
def angle(self, new_value: float):
if new_value == self._angle:
return
self._angle = new_value
self._hit_box.angle = new_value
for sprite_list in self.sprite_lists:
sprite_list._update_angle(self)
self.update_spatial_hash()
@property
def radians(self) -> float:
"""
Get or set the rotation of the sprite in radians.
The value is in radians and is clockwise.
"""
return self._angle / 180.0 * math.pi
@radians.setter
def radians(self, new_value: float):
self.angle = new_value * 180.0 / math.pi
@property
def velocity(self) -> Point:
"""
Get or set the velocity of the sprite.
The x and y velocity can also be set separately using the
``sprite.change_x`` and ``sprite.change_y`` properties.
Example::
sprite.velocity = 1.0, 0.0
Returns:
Tuple[float, float]
"""
return self._velocity
@velocity.setter
def velocity(self, new_value: Point):
self._velocity = new_value
@property
def change_x(self) -> float:
"""Get or set the velocity in the x plane of the sprite."""
return self.velocity[0]
@change_x.setter
def change_x(self, new_value: float):
self._velocity = new_value, self._velocity[1]
@property
def change_y(self) -> float:
"""Get or set the velocity in the y plane of the sprite."""
return self.velocity[1]
@change_y.setter
def change_y(self, new_value: float):
self._velocity = self._velocity[0], new_value
@property
def hit_box(self) -> HitBox:
"""
Get or set the hit box for this sprite.
"""
return self._hit_box
@hit_box.setter
def hit_box(self, hit_box: Union[HitBox, RotatableHitBox]):
if type(hit_box) == HitBox:
self._hit_box = hit_box.create_rotatable(self.angle)
else:
# Mypy doesn't seem to understand the type check above
# It still thinks hit_box can be a union here
self._hit_box = hit_box # type: ignore
@property
def texture(self) -> Texture:
"""Get or set the active texture for this sprite"""
return self._texture
@texture.setter
def texture(self, texture: Texture):
if texture == self._texture:
return
if __debug__ and not isinstance(texture, Texture):
raise TypeError(
f"The 'texture' parameter must be an instance of arcade.Texture,"
f" but is an instance of '{type(texture)}'."
)
# If sprite is using default texture, update the hit box
if self._texture is get_default_texture():
self.hit_box = RotatableHitBox(
texture.hit_box_points,
position=self._position,
angle=self.angle,
scale=self._scale,
)
self._texture = texture
self._width = texture.width * self._scale[0]
self._height = texture.height * self._scale[1]
self.update_spatial_hash()
for sprite_list in self.sprite_lists:
sprite_list._update_texture(self)
@property
def properties(self) -> Dict[str, Any]:
"""
Get or set custom sprite properties.
"""
if self._properties is None:
self._properties = {}
return self._properties
@properties.setter
def properties(self, value: Dict[str, Any]):
self._properties = value
# --- Movement methods -----
[docs]
def forward(self, speed: float = 1.0) -> None:
"""
Adjusts a Sprites forward.
:param speed: speed
"""
angle_rad = math.radians(self.angle)
self.center_x += math.sin(angle_rad) * speed
self.center_y += math.cos(angle_rad) * speed
[docs]
def reverse(self, speed: float = 1.0) -> None:
"""
Adjusts a Sprite backwards.
:param speed: speed
"""
self.forward(-speed)
[docs]
def strafe(self, speed: float = 1.0) -> None:
"""
Adjusts a Sprite sideways.
:param speed: speed
"""
angle_rad = math.radians(self.angle + 90)
self.center_x += math.sin(angle_rad) * speed
self.center_y += math.cos(angle_rad) * speed
[docs]
def turn_right(self, theta: float = 90.0) -> None:
"""
Rotate the sprite right by the passed number of degrees.
:param theta: change in angle, in degrees
"""
self.angle = self._angle + theta
[docs]
def turn_left(self, theta: float = 90.0) -> None:
"""
Rotate the sprite left by the passed number of degrees.
:param theta: change in angle, in degrees
"""
self.angle = self._angle - theta
[docs]
def stop(self) -> None:
"""
Stop the Sprite's motion by setting the velocity and angle change to 0.
"""
self.velocity = 0, 0
self.change_angle = 0
# ---- Draw Methods ----
[docs]
def draw(
self,
*,
filter: Optional[OpenGlFilter] = None,
pixelated: Optional[bool] = None,
blend_function: Optional[BlendFunction] = None
) -> None:
"""
A debug method which draws the sprite into the current OpenGL context.
.. warning:: You are probably looking for :py:meth:`SpriteList.draw() <arcade.SpriteList.draw>`!
Drawing individual sprites is slow compared to using :py:class:`~arcade.SpriteList`.
See :ref:`pg_spritelists_why` for more information.
This method should not be relied on. It may be removed one day.
:param filter: Optional parameter to set OpenGL filter, such as
`gl.GL_NEAREST` to avoid smoothing.
:param pixelated: ``True`` for pixelated and ``False`` for smooth interpolation.
Shortcut for setting filter=GL_NEAREST.
:param blend_function: Optional parameter to set the OpenGL blend function used for drawing the sprite list,
such as 'arcade.Window.ctx.BLEND_ADDITIVE' or 'arcade.Window.ctx.BLEND_DEFAULT'
"""
if self._sprite_list is None:
from arcade import SpriteList
self._sprite_list = SpriteList(capacity=1)
self._sprite_list.append(self)
self._sprite_list.draw(
filter=filter, pixelated=pixelated, blend_function=blend_function
)
self._sprite_list.remove(self)
# ----Update Methods ----
[docs]
def update(self) -> None:
"""
The default update method for a Sprite. Can be overridden by a subclass.
This method moves the sprite based on its velocity and angle change.
"""
self.position = (
self._position[0] + self.change_x,
self._position[1] + self.change_y,
)
self.angle += self.change_angle
# ----Utility Methods----
[docs]
def update_spatial_hash(self) -> None:
"""
Update the sprites location in the spatial hash.
"""
# self._hit_box._adjusted_cache_dirty = True
# super().update_spatial_hash()
for sprite_list in self.sprite_lists:
if sprite_list.spatial_hash is not None:
sprite_list.spatial_hash.move(self)
[docs]
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 to add to the list of available textures
"""
self.textures.append(texture)
[docs]
def set_texture(self, texture_no: int) -> None:
"""
Set the current texture by texture number.
The number is the index into ``self.textures``.
:param texture_no: Index into ``self.textures``
"""
texture = self.textures[texture_no]
self.texture = texture
[docs]
def remove_from_sprite_lists(self) -> None:
"""
Remove this sprite from all sprite lists it is in
including registered physics engines.
"""
super().remove_from_sprite_lists()
for engine in self.physics_engines:
engine.remove_sprite(self)
self.physics_engines.clear()
[docs]
def register_physics_engine(self, physics_engine: Any) -> None:
"""
Register a physics engine on the sprite.
This is only needed if you actually need a reference
to your physics engine in the sprite itself.
It has no other purposes.
The registered physics engines can be accessed
through the ``physics_engines`` attribute.
It can for example be the pymunk physics engine
or a custom one you made.
"""
self.physics_engines.append(physics_engine)
[docs]
def sync_hit_box_to_texture(self):
"""
Update the sprite's hit box to match the current texture's hit box.
"""
self.hit_box = RotatableHitBox(
self.texture.hit_box_points,
position=self._position,
angle=self.angle,
scale=self._scale,
)