Source code for rubato.struct.gameobject.sprites.animation

"""
This is the animation component module for game objects.
"""
from __future__ import annotations
from typing import List, Dict, TYPE_CHECKING
from os import path, walk
import sdl2

from .. import Component
from ... import Sprite
from .... import Vector, Time, get_path, Draw, Camera

if TYPE_CHECKING:
    from . import Spritesheet


[docs]class Animation(Component): """ Animations are a series of images that update automatically in accordance with parameters. Args: scale: The scale of the animation. Defaults to Vector(1, 1). fps: The frames per second of the animation. Defaults to 24. anti_aliasing: Whether to use anti-aliasing on the animation. Defaults to False. flipx: Whether to flip the animation horizontally. Defaults to False. flipy: Whether to flip the animation vertically. Defaults to False. offset: The offset of the animation from the game object. Defaults to Vector(0, 0). rot_offset: The rotation offset of the animation from the game object. Defaults to 0. z_index: The z-index of the animation. Defaults to 0. Attributes: default_state (Optional[str]): The key of the default state. Defaults to None. current_state (str): The key of the current state. Defaults to "". animation_frames_left (int): The number of animation frames left. loop (bool): Whether the animation should loop. Defaults to False. aa (bool): Whether or not to enable anti aliasing. flipx (bool): Whether or not to flip the animation along the x axis. flipy (bool): Whether or not to flip the animation along the y axis. """ def __init__( self, scale: Vector = Vector(1, 1), fps: int = 24, anti_aliasing: bool = False, flipx: bool = False, flipy: bool = False, offset: Vector = Vector(), rot_offset: float = 0, z_index: int = 0 ): super().__init__(offset=offset, rot_offset=rot_offset, z_index=z_index) self._fps: int = fps self.singular = False self._states: Dict[str, List[Sprite]] = {} self._freeze: int = -1 self.default_state: str = None self.current_state: str = "" self.animation_frames_left: int = 0 self._current_frame: int = 0 self.loop: bool = True self.scale: Vector = scale self.aa: bool = anti_aliasing self.flipx: bool = flipx self.flipy: bool = flipy self._time_step = 1000 / self._fps self._time_count = 0 @property def fps(self): """The fps of the animation.""" return self._fps @fps.setter def fps(self, new): self._fps = new self._time_step = 1000 / self._fps @property def current_frame(self) -> int: """The current frame that the animation is on.""" return self._current_frame @current_frame.setter def current_frame(self, new: int): self._current_frame = new self.animation_frames_left = len(self._states[self.current_state]) - (1 + self._current_frame) @property def image(self) -> sdl2.surface.SDL_Surface: """The current SDL Surface holding the image.""" return self._states[self.current_state][self.current_frame].image @property def anim_frame(self) -> Sprite: """The current animation frame.""" sprite = self._states[self.current_state][self.current_frame] sprite.aa = self.aa sprite.rotation = self.gameobj.rotation + self.rot_offset calculated_scale = self.scale.clone() if self.flipx: calculated_scale.x *= -1 if self.flipy: calculated_scale.y *= -1 sprite.scale = calculated_scale # pylint: disable=protected-access sprite._update_rotozoom() return sprite
[docs] def set_current_state(self, new_state: str, loop: bool = False, freeze: int = -1): """ Set the current animation state. Args: new_state: The key of the new current state. loop: Whether to loop the state. Defaults to False. freeze: Freezes the animation once the specified frame is reached (No animation). Use -1 to never freeze. Defaults to -1. Raises: KeyError: The new_state key is not in the initialized states. """ if new_state != self.current_state: if new_state in self._states: self.loop = loop self.current_state = new_state self.reset() self._freeze = freeze else: raise KeyError(f"The given state {new_state} is not in the initialized states")
[docs] def resize(self, new_size: Vector): """ Resize the Animation to a given size in pixels. Args: new_size: The new size of the Animation in pixels. """ for value in self._states.values(): for anim_frame in value: anim_frame.resize(new_size)
[docs] def reset(self): """Reset the animation state back to the first frame.""" self.current_frame = 0
[docs] def add(self, state_name: str, images: List[Sprite]): """ Adds a state to the animation. Args: state_name: The key used to reference this state. images: A list of images to use as the animation. """ self._states[state_name] = images if len(self._states) == 1: self.default_state = state_name self.current_state = state_name self.reset()
[docs] def add_folder(self, state_name: str, rel_path: str, recursive: bool = True): """ Adds a state from a folder of images. Directory must be solely comprised of images. Args: state_name: The key used to reference this state. rel_path: The relative path to the folder you wish to import recursive: Whether it will import an animation shallowly or recursively. Defaults to True. """ ret_list = [] p = get_path(rel_path) if not recursive: _, _, files = next(walk(p)) files.sort() for image_path in files: try: path_to_image = path.join(p, image_path) image = Sprite(rel_path=path_to_image) ret_list.append(image) except TypeError: continue else: for _, _, files in walk(p): # walk to directory path and ignore name and subdirectories files.sort() for image_path in files: try: path_to_image = path.join(p, image_path) image = Sprite(rel_path=path_to_image) ret_list.append(image) except TypeError: continue self.add(state_name, ret_list)
[docs] def add_spritesheet( self, state_name: str, spritesheet: Spritesheet, from_coord: Vector = Vector(), to_coord: Vector = Vector() ): """ Adds a state from a spritesheet. Will include all sprites from the from_coord to the to_coord. Args: state_name: The key used to reference this state. spritesheet: The spritesheet to use. from_coord: The grid coordinate of the first frame. Defaults to Vector(). to_coord: The grid coordinate of the last coord. Defaults to Vector(). Example: .. code-block:: python animation.add_spritesheet("idle", spritesheet, Vector(0, 0), Vector(1, 3)) # This will add the frames (0, 0) to (0, size) and (1, 0) to (1, 3) inclusive to the animation # with the state name "idle". animation.add_spritesheet("idle", spritesheet, to_coord=spritesheet.end) # This will just load from the start to the end of the spritesheet. """ state = [] x = from_coord.x y = from_coord.y while True: state.append(spritesheet.get(x, y)) if y == to_coord.y and x == to_coord.x: break x += 1 if x >= spritesheet.grid_size.x: x = 0 if y >= spritesheet.grid_size.y: break y += 1 self.add(state_name, state)
[docs] def setup(self): """Sets up the animation component.""" for images in self._states.values(): for image in images: image.gameobj = self.gameobj
[docs] def update(self): if self.hidden: return self.anim_frame.update()
[docs] def draw(self, camera: Camera): """Draws the animation frame and steps the animation forward.""" if self.hidden: return self._time_count += 1000 * Time.delta_time while self._time_count > self._time_step: self.anim_tick() self._time_count -= self._time_step Draw.queue_sprite( self.anim_frame, camera.transform((self.gameobj.pos + self.offset) - self.anim_frame.get_size() / 2), self.true_z )
[docs] def anim_tick(self): """An animation processing tick.""" if self.current_frame != self._freeze: if self.animation_frames_left > 0: # still frames left self.current_frame += 1 elif self.loop: # we reached the end of our state self.reset() else: self.set_current_state(self.default_state, True)
[docs] def delete(self): """Deletes the animation component""" for state in self._states.values(): for image in state: image.delete() self._states = {}
[docs] def clone(self) -> Animation: """Clones the animation.""" new = Animation( scale=self.scale, fps=self.fps, anti_aliasing=self.aa, flipx=self.flipx, flipy=self.flipy, offset=self.offset, rot_offset=self.rot_offset, z_index=self.z_index, ) # pylint: disable=protected-access new._states = self._states new.default_state = self.default_state new.current_state = self.current_state new.loop = self.loop new.current_frame = self.current_frame new._freeze = self._freeze return new