Entities, Sprites and Animations - Madour/pyNasNas GitHub Wiki

In a game, there are multiple Entities that interract with other Entities. Each Entity is represented by a Sprite. A Sprite can have multiple Animations composed of Animation Frames.

If we take the example of a platformer game : the player can walk, jump, crouch, die. Those are Animations of the player Sprite. The player can also hit monsters, collect coins, etc. Player, monsters and coins are Entities, and each one has a sprite with multiple animations.

Entities

An Entity is an object with specific properties, it has :

  • a name
  • a sprite
  • animations
  • an animation state
  • a position (x and y)
  • a collision box
  • an update() method

All of these are implemented in the class ns.BaseEntity which should be inherited by your custom entities.

The framework also provides a basic ns.PlatformerEntity class with gravity and simple collision detection that you can use, other type of entities will be added in the future.

Your are encouraged to write your own entities for your specific needs.

Sprites

ns.Sprite is a very simple class to describe a spritesheet. It has a name, a texture (sf.Texture) and animations (a dictionary of Anim objects).

Animations

The class ns.Anim is used to describe one animation. Thus, a ns.Sprite can have multiple ns.Anim. In each ns.Anim, you can have multiple ns.AnimFrame which describe one frame of an animation.

An animation frame is described by :

  • A texture rectangle : a ns.Rect of the texture
  • A frame duration : duration of the frame in milliseconds
  • A frame origin (optional) : the origin the frame (top left corner by default)

Example by code

For this example, I will use a free sprite : https://rvros.itch.io/animated-pixel-hero. Let's focus on idle and run animations: img/adventurer.png Download the sprite sheet and save it as 'adventurer.png' in the assets folder

First, let's describe the sprite:

adventurer_spr = ns.Sprite(
    name="Adventurer",
    texture=ns.Res.adventurer,
    anims= {
        "idle": ns.Anim(),
        "run": ns.Anim()
    }
)

Note: every Sprite must have a "idle" animation, it is the default anim_state when the entity is created

Now, you need to describe the two animations. img/adventurer_frames.png

Black rectangles are idle animation frames, and blue ones are for run animation. The red dots are the origin of each frame. We need all of these informations to describe an animation.

A way to find the positions and sizes of the rectangle is to use the selection tool of an image editor: img/sprite_frame_trick.png

This way, you can directly read the position and size of the selection.

Now, we can fill the two animations in our code :

import NasNas as ns

adventurer_spr = ns.Sprite(
    name="Adventurer",
    texture=ns.Res.adventurer,
    anims= {
        "idle": ns.Anim(
            frames=[
                ns.AnimFrame(ns.Rect((14, 7), (19, 29)), 300, (11, 29)),
                ns.AnimFrame(ns.Rect((66, 6), (17, 30)), 300, (9, 30)),
                ns.AnimFrame(ns.Rect((115, 6), (19, 30)), 300, (10, 30)),
                ns.AnimFrame(ns.Rect((163, 7), (20, 29)), 300, (12, 29))
            ],
            loop=True   # loop is True by default
        ),
        "run": ns.Anim(
            [
                ns.AnimFrame(ns.Rect((17, 45), (20, 28)), 100, (12, 28)),
                ns.AnimFrame(ns.Rect((66, 46), (20, 27)), 100, (15, 27)),
                ns.AnimFrame(ns.Rect((116, 48), (20, 25)), 100, (15, 25)),
                ns.AnimFrame(ns.Rect((167, 45), (23, 28)), 100, (13, 28)),
                ns.AnimFrame(ns.Rect((216, 46), (20, 27)), 100, (14, 27)),
                ns.AnimFrame(ns.Rect((266, 48), (20, 25)), 100, (15, 25)),
            ],
        )
    }
)

Finally, let's create an Entity with this sprite and add it to the scene. Don't forget to update() your entities ! Otherwise you will not see the animation.

Here is the full code:

import NasNas as ns
from sfml import sf

class Game(ns.App):
    def __init__(self):
        super().__init__(title="My SFML Project", w_width=800, w_height=450,
                        v_width=250, v_height=140.6, fps=60)
        ns.Res.load()
        # creating a sprite
        adventurer_spr = ns.Sprite(
            name="Adventurer",
            texture=ns.Res.adventurer,
            anims= {
                "idle": ns.Anim(
                    frames=[
                        ns.AnimFrame(ns.Rect((14, 7), (19, 29)), 300, (11, 29)),
                        ns.AnimFrame(ns.Rect((66, 6), (17, 30)), 300, (9, 30)),
                        ns.AnimFrame(ns.Rect((115, 6), (19, 30)), 300, (10, 30)),
                        ns.AnimFrame(ns.Rect((163, 7), (20, 29)), 300, (12, 29))
                    ],
                    loop=True   # loop is True by default
                ),
                "run": ns.Anim(
                    [
                        ns.AnimFrame(ns.Rect((17, 45), (20, 28)), 100, (12, 28)),
                        ns.AnimFrame(ns.Rect((66, 46), (20, 27)), 100, (15, 27)),
                        ns.AnimFrame(ns.Rect((116, 48), (20, 25)), 100, (15, 25)),
                        ns.AnimFrame(ns.Rect((167, 45), (23, 28)), 100, (13, 28)),
                        ns.AnimFrame(ns.Rect((216, 46), (20, 27)), 100, (14, 27)),
                        ns.AnimFrame(ns.Rect((266, 48), (20, 25)), 100, (15, 25)),
                    ],
                )
            }
        )
        self.player = ns.BaseEntity(adventurer_spr)
        self.player.position = (50, 50)

        self.scene.add_layer(ns.Layer("ent", self.player), order=0)

        self.game_camera.scene = self.scene

        self.debug = False

    def event_handler(self, event):
        if event == sf.Event.KEY_PRESSED:
            if event['code'] == ns.Keyboard.ESCAPE:
                self.window.close()

    def update(self):
        if ns.Keyboard.is_key_pressed(ns.Keyboard.SPACE):
            self.player.anim_state = "run"
        else:
            self.player.anim_state = "idle"
        # dt is the delta time, Entities need dt to update
        self.player.update(self.dt)

As you can see, it is not very readable to define all your sprites in Game.__init__, it is better to define them in another file (e.g. sprites.py ) and import them.


<- [Scenes, Layers and Camera]] ](/Madour/pyNasNas/wiki/[[Tiled-Maps) ->