Drawing System with Animations

This tutorial demonstrates how to create and manage drawing systems in your game engine using custom surfaces, sprites, and animations. You will learn how to:

  • Draw simple shapes or images.

  • Handle animations with sprite slicing.

  • Utilize directory-based sprite animations for more complex behaviors.

Surface and Sprite Overview

Surface: A Surface is a fundamental 2D object in graphical programming, primarily used to represent an image or a drawable area. It serves as a canvas where graphics can be rendered, including shapes, text, or other images. In frameworks like Pygame, surfaces are often the building blocks for visual elements.

  • Example: A surface might represent a game background or a texture for a game object.

Sprite: A Sprite is an abstraction representing a visual object in a 2D space, often associated with a surface for its appearance. Sprites typically include additional features such as position, movement, and collision detection, making them essential for managing interactive objects in games.

  • Example: A sprite could be a character, enemy, or collectible item in a game.

Surface Layer Draw System

The surface layer draw system is a class for organizing and rendering multiple surfaces in a specific order. Each surface is assigned to a layer, and the system ensures that surfaces are drawn sequentially from the background to the foreground based on their layer priority.

In this super clear sketch, you can see that the layer draw system is based on the window provided by the pygame. The individual surfaces are layered with a priority, while they can have individual sizes.

surface layer example image

To use the system, you have to create a new layer instance from the core object

self.surface = self.core.create_layer_surface(
                name=None, # used to identify and edit the surface
                width=32,
                height=32,
                x=0, # pos.x
                y=0,  # pos.y
                render_layer: int = 0,
                fill_after_draw=True # auto fill with core.background_color
        )

the surface is now automatically added to the system and displayed at the position (x,y) with (width,height). To display pixels or images on this surface, you can do this in the update method

def update(self):
    ...
    self.surface.blit(img, img_position) # shows the img on the surface,
                                         # if the img has the size 32x32 (same like the surface),
                                         # the img_position should be (0,0) to fill the surface perfect

Drawing a Simple Box

To start, create a system to draw a simple blue box on the screen. The box will be rendered onto a custom surface.

from core import *

class DrawSystem(Engine, Prefab):
    """
    A simple drawing system that draws a static colored box on the screen.

    All examples in this file can be found in `preview.py`.
    """

    def start(self):
        """
        Initializes the drawing surface and creates a simple sprite (blue box).

        - Retrieves the window size from the core system.
        - Creates a layer surface where the sprite will be drawn.
        - Initializes a `SimpleSprite` with a blue color and positions it at (50, 50).
        """
        size = self.core.window_size
        self.surface = self.core.create_layer_surface(
            name="_MainDrawSurface",
            width=size[0],
            height=size[1],
            x=0,
            y=0,
            render_layer=0,
            fill_after_draw=True
        )

        self.blue_box_sprite = SimpleSprite(color=(54, 117, 221))
        self.blue_box_sprite.update_rect(
            self.blue_box_sprite.get_rect().move((50, 50))
        )

    def update(self):
        """
        Draws the blue box sprite onto the surface.

        - Blits the sprite's image onto the surface at its current position.
        """
        self.surface.blit(self.blue_box_sprite.get_image(), self.blue_box_sprite.get_rect())

Drawing an Image Sprite

Next, draw an image sprite centered on the screen. Use SimpleImageSprite for this purpose.

class DrawSystemImage(Engine, Prefab):
    """
    A drawing system that draws an image sprite at the center of the screen.

    All examples in this file can be found in `preview.py`.
    """

    def start(self):
        """
        Initializes the drawing surface and loads an image sprite.

        - Retrieves the window size from the core system.
        - Creates a layer surface where the sprite will be drawn.
        - Loads an image sprite and positions it at the center of the window.
        """
        size = self.core.window_size
        self.surface = self.core.create_layer_surface(
            name="_MainDrawSurfaceImage",
            width=size[0],
            height=size[1],
            x=0,
            y=0,
            render_layer=0,
            fill_after_draw=True
        )
        self.image_sprite = SimpleImageSprite(os.path.join(LIB_DIR, "examples/assets/pixel-adventure/Main Characters/Ninja Frog/Idle/tile000.png"))
        coords = get_center(
            (size[0] / 2, size[1] / 2),
            self.image_sprite.get_rect().width,
            self.image_sprite.get_rect().height
        )
        self.image_sprite.update_rect(
            self.image_sprite.get_rect().move(coords)
        )

    def update(self):
        """
        Draws the image sprite onto the surface.

        - Blits the image onto the surface at its current position.
        """
        self.surface.blit(self.image_sprite.get_image(), self.image_sprite.get_rect())

Animating an Image

With the SpriteAnimator you can animate a surface. For example, to animate your player, the individual images of the animation must be entered into a List. If several images are required per frame, this can easily be solved with a sub-List. These methods can also be mixed. The state names are required to control the animation.

def start(self):
    anim = {
        "state-name-1": [full_body_1_img, full_body_2_img, full_body_3_img],
        "state-name-2": [[body_1_img, hair_1_img], [body_2_img, hair_2_img]],
        "state-name-3": [full_body_1_img, [body_2_img, hair_2_img]]
    }
    self.simpleSpriteAnimator = SimpleSpriteAnimator(
                            anim_sprites=anim,
                            start_state='state-name-1',
                            anim_state_decision=self.anim_sate_decision,
                            sprite_size=(32,32),
                            fps=.2
                    )
def anim_sate_decision(self): # Decide which animation should be played
    if(velocity < 1):
        return "state-name-1"
    else:
        if(jumping):
            return "state-name-2"
        else:
            return "state-name-3"
def update(self):
    surface = self.simpleSpriteAnimator.animate(self.core.delta_time)
    self.core.draw_surface(surface, draw_position)

Using multiple frames of an image

Create a sprite animation using multiple frames of an image. This example uses a sprite animator to animate an “idle” state.

class DrawSystemAnimationImage(Engine, Prefab):
    """
    A drawing system that animates an image sprite and displays it on the screen.

    All examples in this file can be found in `preview.py`.
    """

    def start(self):
        """
        Initializes the drawing surface and sets up an animated sprite.

        - Retrieves the window size from the core system.
        - Creates a layer surface where the animated sprite will be drawn.
        - Loads an animation with multiple frames for a sprite and positions it at the center of the window.
        """
        size = self.core.window_size
        self.surface = self.core.create_layer_surface(
            name="_MainDrawSurfaceAnimationImage",
            width=size[0],
            height=size[1],
            x=0,
            y=0,
            render_layer=0,
            fill_after_draw=True
        )
        anim = {
            "idle": [SimpleImageSprite(os.path.join(LIB_DIR, "examples/assets/pixel-adventure/Main Characters/Ninja Frog/Idle/tile{:03}.png".format(index))).get_image() for index in range(11)]
        }
        self.simple_sprite_animator = SimpleSpriteAnimator(
            anim_sprites=anim,
            start_state="idle",
            anim_state_decision=self.anim_sate_decision,
            sprite_size=(32, 32),
            fps=2
        )
        coords = get_center(
            (size[0] / 2, size[1] / 2),
            self.simple_sprite_animator.get_rect().width,
            self.simple_sprite_animator.get_rect().height
        )
        self.simple_sprite_animator.update_rect(
            self.simple_sprite_animator.get_rect().move(coords)
        )

    def update(self):
        """
        Updates and draws the animated sprite onto the surface.

        - Blits the current frame of the animation onto the surface at its current position.
        """
        self.surface.blit(
            self.simple_sprite_animator.animate(self.core.delta_time),
            self.simple_sprite_animator.get_rect()
        )

    def anim_sate_decision(self):
        """
        Defines the animation state decision logic for the sprite animator.

        - In this case, always returns the "idle" state for animation.
        """
        return "idle"

Slicing Sprites for Animation

Use sprite slicing to handle animations from a single sprite sheet.

"""
This module defines advanced drawing systems for rendering sliced animations,
animations with paddings, and directory-based sprite animations.
Each class provides unique functionality for managing sprite slicing and animation states.
"""

class DrawSystemAnimationImageSliced(Engine, Prefab):
    """
    A drawing system that displays an animated sprite using sliced frames from a single sprite sheet.

    Methods:
        start(): Initializes the drawing surface and configures the sprite animator with sliced frames.
        update(): Plays the animation and renders the current frame on the surface.
        anim_sate_decision(): Determines the animation state, always returning "idle."
    """
    def start(self):
        """
        Initializes the drawing surface and configures the sprite animator with a sliced animation.

        Creates a rendering surface and loads an animation from a sprite sheet, slicing it into frames
        using a predefined number of columns. Positions the sprite at the center of the screen.

        Parameters: None
        Returns: None
        """
        size = self.core.window_size
        self.surface = self.core.create_layer_surface(
            name="_DrawSystemAnimationImageSliced",
            width=size[0],
            height=size[1],
            x=0,
            y=0,
            render_layer=0,
            fill_after_draw=True
        )
        anim = {
            "idle": SimpleImageSprite(os.path.join(LIB_DIR, "examples/assets/pixel-adventure/Main Characters/Ninja Frog/Idle (32x32).png"), slicer=AmountSlicer(cols=11)).get_image()
        }
        self.simple_sprite_animator = SimpleSpriteAnimator(
            anim_sprites=anim,
            start_state="idle",
            anim_state_decision=self.anim_sate_decision,
            sprite_size=(32, 32),
            fps=1
        )
        coords = get_center(
            (size[0] / 2, size[1] / 2),
            self.simple_sprite_animator.get_rect().width,
            self.simple_sprite_animator.get_rect().height
        )
        self.simple_sprite_animator.update_rect(
            self.simple_sprite_animator.get_rect().move(coords)
        )

    def update(self):
        """
        Renders the current frame of the sliced animation onto the surface.

        Uses the sprite animator to retrieve the next frame based on elapsed time and draws it on the surface.

        Parameters: None
        Returns: None
        """
        self.surface.blit(
            self.simple_sprite_animator.animate(self.core.delta_time),
            self.simple_sprite_animator.get_rect()
        )

    def anim_sate_decision(self):
        """
        Determines the animation state for the sprite animator.

        Always returns "idle" in this implementation.

        Parameters: None
        Returns:
            str: The current animation state ("idle").
        """
        return "idle"

Handling Slices with Paddings

For sprite sheets with padding or gaps, include padding parameters in the slicing configuration.

class DrawSystemAnimationImageSlicedWithPaddings(Engine, Prefab):
    """
    A drawing system that displays an animated sprite using sliced frames from a sprite sheet with paddings.

    Methods:
        start(): Initializes the drawing surface and configures the sprite animator with sliced frames.
        update(): Plays the animation and renders the current frame on the surface.
        anim_sate_decision(): Determines the animation state, always returning "idle."
    """
    def start(self):
        """
        Initializes the drawing surface and configures the sprite animator with a sliced animation.

        Creates a rendering surface and loads an animation from a sprite sheet with paddings.
        The frames are sliced using specified paddings and gaps, and the sprite is centered on the screen.

        Parameters: None
        Returns: None
        """
        size = self.core.window_size
        self.surface = self.core.create_layer_surface(
            name="_DrawSystemAnimationImageSlicedWithPaddings",
            width=size[0],
            height=size[1],
            x=0,
            y=0,
            render_layer=0,
            fill_after_draw=True
        )
        anim = {
            "idle": SimpleImageSprite(os.path.join(LIB_DIR, "examples/assets/debug/image-with-padding.png"), slicer=AmountSlicer(
                cols=3,
                rows=3,
                top_padding=2,
                bottom_padding=2,
                left_padding=2,
                right_padding=2,
                x_gap=2,
                y_gap=2
            )).get_image()
        }
        self.simple_sprite_animator = SimpleSpriteAnimator(
            anim_sprites=anim,
            start_state="idle",
            anim_state_decision=self.anim_sate_decision,
            sprite_size=(32, 32),
            fps=.1
        )
        coords = get_center(
            (size[0] / 2, size[1] / 2),
            self.simple_sprite_animator.get_rect().width,
            self.simple_sprite_animator.get_rect().height
        )
        self.simple_sprite_animator.update_rect(
            self.simple_sprite_animator.get_rect().move(coords)
        )

    def update(self):
        """
        Renders the current frame of the sliced animation with paddings onto the surface.

        Uses the sprite animator to retrieve the next frame based on elapsed time and draws it on the surface.

        Parameters: None
        Returns: None
        """
        self.surface.blit(
            self.simple_sprite_animator.animate(self.core.delta_time),
            self.simple_sprite_animator.get_rect()
        )

    def anim_sate_decision(self):
        """
        Determines the animation state for the sprite animator.

        Always returns "idle" in this implementation.

        Parameters: None
        Returns:
            str: The current animation state ("idle").
        """
        return "idle"

Directory-Based Animation

SpriteDirectoryAnimation simplifies creating animations by organizing sprite images from a directory. Just provide the main folder path and optionally use a slicer to split sprite sheets into frames.

This file structure exemplifies the required structure for the SpriteDirectoryAnimation. Here, player-animation serves as the main directory, which should be passed as an absolute path. Additionally, a Slicer can be selected to map the files within the directory, as well as the files in its subdirectories relative to the folder.

player-animation
├── run.png
├── jump.png
└── idle
    ├── idle-frame-1.png
    └── idle-frame-2.png

Finally, automate animation handling with a directory-based system. This allows for managing multiple animations conveniently.

class DrawSystemDirectoryAnimationImageSliced(Engine, Prefab):
    """
    A drawing system that displays animations loaded from a directory containing sprite sheets.

    Methods:
        start(): Initializes the drawing surface and configures the sprite animator with animations from a directory.
        update(): Plays the animation and renders the current frame on the surface.
        anim_sate_decision(): Determines the animation state based on the current internal timer.
    """
    def start(self):
        """
        Initializes the drawing surface and configures the sprite animator with animations from a directory.

        Creates a rendering surface and loads animations from sprite sheets in a specified directory.
        Each animation is sliced using a size-based slicer. The sprite is positioned at the center of the screen.
        The system alternates between "idle" and "run" states based on an internal timer.

        Parameters: None
        Returns: None
        """
        size = self.core.window_size
        self.surface = self.core.create_layer_surface(
            name="_DrawSystemDirectoryAnimationImageSliced",
            width=size[0],
            height=size[1],
            x=0,
            y=0,
            render_layer=0,
            fill_after_draw=True
        )
        sprite_dir_anim = SpriteDirectoryAnimation(os.path.join(LIB_DIR, "examples/assets/pixel-adventure/Main Characters/Ninja Frog"), slicer=SizeSlicer(width=32, height=32))
        anim = sprite_dir_anim.parse()
        anim["idle"] = anim["Idle"]
        anim["run"] = anim["Run (32x32)"]
        self.simple_sprite_animator = SimpleSpriteAnimator(
            anim_sprites=anim,
            start_state="idle",
            anim_state_decision=self.anim_sate_decision,
            sprite_size=(32, 32),
            fps=2
        )
        coords = get_center(
            (size[0] / 2, size[1] / 2),
            self.simple_sprite_animator.get_rect().width,
            self.simple_sprite_animator.get_rect().height
        )
        self.simple_sprite_animator.update_rect(
            self.simple_sprite_animator.get_rect().move(coords)
        )
        self._is_idle = True
        self._timer = 0

    def update(self):
        """
        Renders the current frame of the directory-based animation onto the surface.

        Uses the sprite animator to retrieve the next frame based on elapsed time and draws it on the surface.
        Alternates between "idle" and "run" states based on the internal timer.

        Parameters: None
        Returns: None
        """
        self.surface.blit(
            self.simple_sprite_animator.animate(self.core.delta_time),
            self.simple_sprite_animator.get_rect()
        )
        if self._is_idle:
            self._timer += self.core.delta_time
            if self._timer >= 5000:
                self._is_idle = False
        else:
            self._timer -= self.core.delta_time
            if self._timer <= 0:
                self._is_idle = True

    def anim_sate_decision(self):
        """
        Determines the animation state for the sprite animator.

        Alternates between "idle" and "run" states based on the internal timer.

        Parameters: None
        Returns:
            str: The current animation state ("idle" or "run").
        """
        if self._is_idle:
            return "idle"
        return "run"