"""
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, Image
from ... import Vector, Time, get_path
if TYPE_CHECKING:
from . import Spritesheet
from .. import Camera
[docs]class Animation(Component):
"""
Animations are a series of images that update automatically in accordance with parameters.
Args:
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.
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.
visible: Whether the animation is visible. Defaults to True.
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.
visible (bool): Whether or not the animation is visible.
"""
def __init__(
self,
offset: Vector = Vector(),
rot_offset: float = 0,
scale: Vector = Vector(1, 1),
fps: int = 24,
anti_aliasing: bool = False,
flipx: bool = False,
flipy: bool = False,
visible: bool = True
):
super().__init__(offset=offset, rot_offset=rot_offset)
self._fps: int = fps
self.singular = False
self._states: Dict[str, List[Image]] = {}
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.visible: bool = visible
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, fps):
self._fps = fps
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) -> Image:
"""The current animation frame."""
img = self._states[self.current_state][self.current_frame]
img.aa = self.aa
img.flipx = self.flipx
img.flipy = self.flipy
img.scale = self.scale
img.offset = self.offset
img.visible = self.visible
img.rotation_offset = self.rotation_offset
return img
[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[Image]):
"""
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 = Image(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 = Image(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 draw(self, camera: Camera):
"""Draws the animation frame and steps the animation forward."""
self._time_count += 1000 * Time.delta_time
while self._time_count > self._time_step:
self.anim_tick()
self._time_count -= self._time_step
self.anim_frame.draw(camera)
[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,
"visible": self.visible
}
)
# 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