Part 2: Sprites and nodes

In a previous chapter we have covered some properties of engine, window and renderer and were able to run a game showing empty screen. Let’s start drawing some actual objects in our game!

Loading images from files

In order to draw anything, we need to load an image file first. For this demo we will use a prepared package of assets, available here which includes full set of images, sounds and fonts for the tutorial. Download the file and unpack it inside the folder with main.py. You should get the following folder structure:

my_game/
    assets/
        gfx/
            arrow.png
            ..... other image files ....
        sfx/
            .... sound effect files ....
        music/
            .... music files ....
        fonts/
            .... font files ....
    main.py

Let’s now load the first image (arrow.png). Add the following imports at the top of the main.py

from kaa.sprites import Sprite
import os

Then add __init__() method to MyScene and load our image there:

def __init__(self):
    super().__init__()
    arrow_sprite = Sprite(os.path.join('assets', 'gfx', 'arrow.png'))

Kaa engine loads images to objects called Sprites. With the image loaded, we can create the first few in-game objects (which will use the same Sprite).

Drawing objects on the screen

Object instances present on the scene are called Nodes. Let’s create three arrow objects (three Nodes) using the arrow sprite.

from kaa.nodes import Node
def __init__(self):
    super().__init__()
    self.arrow_sprite = Sprite(os.path.join('assets', 'gfx', 'arrow.png'))
    self.arrow1 = Node(sprite=self.arrow_sprite, position=Vector(200, 200))  # default position is Vector(0,0)
    self.arrow2 = Node(sprite=self.arrow_sprite, position=Vector(400, 300))
    self.arrow3 = Node(sprite=self.arrow_sprite, position=Vector(600, 500))

Run the game and… No objects are visible on the screen! It’s because we created them but did not add them to the Scene. A shameful display! Let’s fix it. The Scene holds a tree-like structure of Nodes, and always has the “root” Node. Let’s add our objects as children of the root node:

def __init__(self):
    super().__init__()
    self.arrow_sprite = Sprite(os.path.join('assets', 'gfx', 'arrow.png'))
    self.arrow1 = Node(sprite=self.arrow_sprite, position=Vector(200, 200))
    self.arrow2 = Node(sprite=self.arrow_sprite, position=Vector(400, 300))
    self.arrow3 = Node(sprite=self.arrow_sprite, position=Vector(600, 500))
    self.root.add_child(self.arrow1)
    self.root.add_child(self.arrow2)
    self.root.add_child(self.arrow3)

Run the game again. Looks much better doesn’t it? The arrows appear exactly where we put them.

Note: Sprites are immutable. Think of them as wrapper objects for image files.

Moving objects around

To move an object to a different position, simply set a new position:

def __init__(self):
    # ... previous code...
    self.arrow1.position = Vector(360, 285)

Run the game and check out the results!

Note

position’s x and y can be floats, e.g. arrow1.position = Vector(360.45, 285.998) they can also be negative e.g. arrow1.position = Vector(-50, -10)

Using z-index

Hmm, arrow1 now overlaps arrow2, but what decides which one is displayed on top? Long story short: nothing decides, it is unpredictable. Let’s take control by assigning objects a z-index. Object with a bigger z-index will always be rendered on top of the objects with smaller z-index.

def __init__(self):
    # ... previous code...
    self.arrow1.z_index = 1  # note: default z_index is 0

Run the game and see that arrow1 is always drawn on top of arrow2.

Rotating objects

To rotate an object, simply set the rotation_degrees property.

def __init__(self):
    # ... previous code...
    self.arrow1.rotation_degrees = 45  # note: default rotation_degrees is 0

Notice that you can set rotation_degrees to more than 360 degrees or to negative values.

Those more mathematically inclined can use radians. 45 degrees should be pi/ 4, right? Use rotation property on a node:

import math
self.arrow1.rotation = math.pi / 4

Run the game and check for yourself - arrow1 rotated 45 degrees!

Scaling objects

To scale an object in X or Y axis (or both), use the scale property. Pass a Vector object, where vector’s x,y values are scaling factors for x and y axis respectively. 1 is the default scale, 2 will enlarge it twice, passing 0.5 will shrink it 50%, etc.

self.arrow1.scale = Vector(0.5, 1)  # note: default is Vector(1,1)

Re-run the game and see how X axis of the arrow was scaled down.

Aligning object’s ‘origin’ (the anchor point)

Let’s ask a curious question. Our ‘arrow’ object has spatial dimentions: 100px width and 50px height. We tell the game to draw it at some specific position e.g. (300, 200). But what does this actually mean? Which pixel of the arrow will really be drawn at position (300, 200)? The top-left pixel? Or the central pixel? Or maybe some other pixel?

By default it’s the central pixel. That anchor point of a node is called ‘origin’. Let’s visualize the idea by drawing a ‘pixel marker’ image in position of arrow2 and arrow3

def __init__(self):
    ... previous code...
    # create pixel marker sprite
    self.pixel_marker_sprite = Sprite(os.path.join('assets', 'gfx', 'pixel-marker.png'))
    # create pixel_marker 1 in the same spot as arrow2 (but with bigger z-index so we can see it)
    self.pixel_marker1 = Node(sprite=self.pixel_marker_sprite, position=Vector(400, 300), z_index=100)
    # create pixel_marker 2 in the same spot as arrow3
    self.pixel_marker2 = Node(sprite=self.pixel_marker_sprite, position=Vector(600, 500), z_index=100)
    # add pixel markers to the scene
    self.root.add_child(self.pixel_marker1)
    self.root.add_child(self.pixel_marker2)

Run the game and see the markers appear on top of arrows in the central position.

Now, let’s change just one thing: origin_alignment of arrow 3

from kaa.geometry import Alignment
def __init__(self):
    # ... previous code...
    self.arrow3.origin_alignment = Alignment.right  # default is Alignment.center

Re-run the game and see how arrow3 is now drawn in a different place! We did not change its position, just the origin alignment. Not surprisingly, we can see that origin marker is to the right of the node’s rectangle.

You can set the origin to be in one of the 9 standard positions: top-left, top, top-right, left, central (default), right, bottom-left, bottom and bottom-right. The node’s rectangular shape will be drawn according to origin position.

All transformations such as positioning, scaling or rotating are applied in relation to the origin. We’ll see that in practice in the next section.

Note

What if you need a non-standard position for node’s origin? You can achieve that by using two nodes in a parent - child relation. It’s described in more detail in one of the next sections.

Updating state of objects

So far, we’ve been writing our code in the Scene’s __init__ method. This is a standard practice to create an initial state of the scene. Let’s now try to update our objects in real-time, as the game is running!

Every scene has update(dt) function which will be called by the engine in a loop (with maximum frequency of 60 times per second). The dt parameter is an integer value how many milliseconds had passed since the last update call. You will implement most of your game logic inside the update function.

Let’s get to it. Modify the update function in MyScene class:

def update(self, dt):
    #  .... previous code ....
    self.arrow2.rotation_degrees += 1  # rotating 1 degree PER FRAME (not the best design)
    self.arrow3.rotation_degrees += 90 * dt / 1000  # rotating 90 degrees PER SECOND (good design!)

Run the game and notice how the arrows rotate around their respective origin points. It’s also worth noting that it’s generally better to include dt in all formulas which transform game objects. Rotating, moving, or generally applying any other transformation by a fixed value per frame can lead to problems because it is not guaranteed that frame time (dt) will always be identical. Some frames may take longer to process than others and the visible transformations would suddenly speed up or slow down, confusing the player. Thus it’s usually better to apply transformations per second.

Objects can have child objects

So far we’ve been adding objects (Nodes) to the root Node of the scene. But each node we create can have its own child nodes, those child nodes can have their own children and so on.

All transformations applied to a node are automatically applied to all its child nodes. Let’s check this out in practice. Add the following code to the __init__ function of the Scene.

def __init__(self):
    # .... previous code .....
    self.green_arrow_sprite = Sprite(os.path.join('assets', 'gfx', 'arrow-green.png'))
    self.child_arrow1 = Node(sprite=self.green_arrow_sprite, position=Vector(0,0), rotation_degrees=90, z_index=1)
    self.arrow3.add_child(self.child_arrow1)

Run the game and check out the result. First thing you have probably noticed is that we set child_arrow1’s position to (0,0) yet the green arrow is being shown at (600, 500)! This is because child node’s position value is not absolute but relative to the parent. Since parent’s position is (600, 500) and child’s offset is (0, 0) therefore calculated child position is (600, 500). As you have noticed the child arrow is rotating together with the parent, rotated (again, relatively) by +90 degrees.

It is very important to remember that position, scale and rotation of each node are always relative to their parent node. There is a way to get an absolute position, scale or rotation of a Node:

print(self.arrow3.absolute_position)
print(self.arrow3.absolute_rotation)
print(self.arrow3.absolute_rotation_degrees)
print(self.arrow3.absolute_scale)

Take some time to experiment with the parent-child system. Try changing child and parent node’s properties such as position, origin_alignment, rotation, scaling etc., try updating both nodes properties inside update() function and observe the results.

Note

You can add an empty Node (without image, just Node(position=Vector(x, y)) just to hold a position and then add a child with any desired position offset. This simple trick allows for a node to have a custom origin alignment, not limited to the 9 standard origin_alignment values.

Showing and hiding objects

If you need to hide or show a node, use visible property:

my_node.visible = False #  default is True

Hiding a node will automatically hide all its child nodes.

Introducing animations

So far we’ve been using single-frame images. Kaa engine supports frame-by-frame sprite animations. Take a look at assets/gfx/explosion.png file. It is a frame by frame animation of an explosion, frame size is 100x100 and there are 75 actual frames in the file.

Creating animation is a two step process:

First, we need to ‘cut’ each frame from the explosion.png file and make it a separate Sprite. In other words we need to have 75 Sprites, one for each frame. Fortunately we don’t need to do that manually, there’s a helper function for slicing spritesheets: split_spritesheet. Let’s use it.

from kaa.sprites import Sprite, split_spritesheet

def __init__(self):
    # .... previous code .....
    self.explosion_spritesheet = Sprite(os.path.join('assets', 'gfx', 'explosion.png')) # laod the whole spritesheet
    self.explosion_frames = split_spritesheet(self.explosion_spritesheet, frame_dimensions=Vector(100,100),
        frames_count=75)  # create 75 separate <Sprite> objects

The function is rather self-explanatory, it takes a sprite, goes through it left to right and top to bottom, cutting out frames using specified frame dimensions. It stops after frames_count frames.

The second step is to create an animation, and assign it to a node. We then add the Node to the scene:

from kaa.transitions import NodeSpriteTransition

def __init__(self):
    # .... previous code .....
    explosion_animation = NodeSpriteTransition(self.explosion_frames, duration=1000, loops=0)  # create animation
    self.explosion = Node(position=Vector(600, 150), transition=explosion_animation)  # create node
    self.root.add_child(self.explosion)  # add node to scene

Few things demand explanation here. First, the seemingly weird name of the animation object. Why is it called NodeSpriteTransition, not just SpriteAnimation or something similar? Why is it imported from kaa.transitions namespace ? The reason is because it’s a part of much more general mechanism called… transitions! Transitions are recipes how node’s property should evolve over time. In this case the evolving property is a sprite, but as you will see in the Part 9 of the tutorial there are also transitions for properties such as position, rotation, scale, color and others. The mechanism allows to ‘change’ those properties over time just like we change the sprite over time. That also explains why node property is called transition.

Let’s look at the NodeSpriteTransition parameters. First one is obviously a list of frames, the duration tells how long the animation should take (in miliseconds). The loops parameter tells how many times the animation should repeat. 0 means infinite number of repetitions.

Run the game and behold the animated explosion, if you haven’t yet!

Note: All transitions, including NodeSpriteTransition are immutable which means if you need to create a transition with different parameters, you need to create a new transition object. You can re-use the same transition on multiple nodes though.

Let’s illustrate this on example. Let’s use the same set of frames to create a new animation: longer duration, with 3 loops instead of the infinite loop, and running back and forth. Then let’s add two explosion Nodes using that animation

def __init__(self):
    # .... previous code .....
    explosion_animation_long =  NodeSpriteTransition(self.explosion_frames, duration=4000, loops=3,
                                                     back_and_forth=True)  # create animation
    self.explosion2 = Node(position=Vector(100, 400), transition=explosion_animation_long)
    self.explosion3 = Node(position=Vector(200, 500), transition=explosion_animation_long)
    self.root.add_child(self.explosion2)
    self.root.add_child(self.explosion3)

Run the game and check out the new explosions. We’ve also learned about back_and_forth flag on the NodeSpriteTransition!

How to crop a Sprite

What if you want to crop the Sprite manually? Use crop() method on Sprite object, getting a new Sprite

new_sprite = self.arrow1.crop(Vector(5,5), Vector(10,20))

The example above will create a new, 10x20 px Sprite from arrow1, starting the crop from position (5,5).

Controlling animations manually

If you want to take full control of the animation you need to set each frame manually (set the sprite on given Node manually). It’s entirely up to you how you do that, let’s just say that there’s something like custom transitions. We’ll learn more about transitions in Chapter 10 of the tutorual

Setting a lifetime of an object

For every Node you create you can set a lifetime property. It is a number of miliseconds after which the node will be automatically removed from the scene. Just remember that the timer starts ticking from the moment of adding node to the scene, not from the moment of creating the Node object. If a node is already added to the Scene, the timer starts immediately.

Let’s set lifetime property on one of the nodes:

self.explosion3.lifetime = 5000

Run the game, and observe that the node gets removed after 5 seconds.

Deleting objects from the scene

You will of course need to remove Nodes from the scene programmatically as well. It is very easy, just use the delete() method on the Node you wish to remove.

some_node.delete()

The node will get removed from the scene immediately. If it has child nodes, they will be removed as well, together with their child nodes and so on, recursively.

IMPORTANT: after deleting a node you should not call any of its method or access any of its properties, even the read-only properties. It will cause non deterministic efects as the game runs, eventually leading to a segmentation fault and a crash to desktop.

End of Part 2 - full code

We end this part of tutorial with a lot of code inside Scene’s __init__. It starts looking messy but don’t worry, we’ll start the Part 3 with a cleanup, and then we’ll get to writing the actual game!

Anyway, here’s the full listing of main.py after Part 2:

from kaa.engine import Engine, Scene
from kaa.geometry import Vector
from kaa.sprites import Sprite, split_spritesheet
from kaa.nodes import Node
from kaa.geometry import Alignment
from kaa.transitions import NodeSpriteTransition
import os

class MyScene(Scene):

    def __init__(self):
        super().__init__()
        self.arrow_sprite = Sprite(os.path.join('assets', 'gfx', 'arrow.png'))
        self.arrow1 = Node(sprite=self.arrow_sprite, position=Vector(200, 200))
        self.arrow2 = Node(sprite=self.arrow_sprite, position=Vector(400, 300))
        self.arrow3 = Node(sprite=self.arrow_sprite, position=Vector(600, 500))
        self.root.add_child(self.arrow1)
        self.root.add_child(self.arrow2)
        self.root.add_child(self.arrow3)
        self.arrow1.position = Vector(360, 285)
        self.arrow1.z_index = 1  # note: default z_index is 0
        self.arrow1.rotation_degrees = 45
        self.arrow1.scale = Vector(0.5, 1)  # note: default is Vector(1,1)
        # create pixel marker sprite
        self.pixel_marker_sprite = Sprite(os.path.join('assets', 'gfx', 'pixel-marker.png'))
        # create pixel_marker 1 in the same spot as arrow2 (but with bigger z-index so we can see it)
        self.pixel_marker1 = Node(sprite=self.pixel_marker_sprite, position=Vector(400, 300), z_index=100)
        # create pixel_marker 2 in the same spot as arrow3
        self.pixel_marker2 = Node(sprite=self.pixel_marker_sprite, position=Vector(600, 500), z_index=100)
        # add pixel markers to the scene
        self.root.add_child(self.pixel_marker1)
        self.root.add_child(self.pixel_marker2)
        self.arrow3.origin_alignment = Alignment.right  # default is Alignment.center
        self.green_arrow_sprite = Sprite(os.path.join('assets', 'gfx', 'arrow-green.png'))
        self.child_arrow1 = Node(sprite=self.green_arrow_sprite, position=Vector(0,0), rotation_degrees=90, z_index=1)
        self.arrow3.add_child(self.child_arrow1)
        self.explosion_spritesheet = Sprite(os.path.join('assets', 'gfx', 'explosion.png')) # laod the whole spritesheet
        self.explosion_frames = split_spritesheet(self.explosion_spritesheet, frame_dimensions=Vector(100,100),
            frames_count=75)  # create 75 separate <Sprite> objects
        explosion_animation = NodeSpriteTransition(self.explosion_frames, duration=1000, loops=0)  # create animation
        self.explosion = Node(position=Vector(600, 150), transition=explosion_animation)  # create node with animation
        self.root.add_child(self.explosion)  # add node to scene

        explosion_animation_long =  NodeSpriteTransition(self.explosion_frames, duration=4000, loops=3,
                                                         back_and_forth=True)  # create animation
        self.explosion2 = Node(position=Vector(100, 400), transition=explosion_animation_long)
        self.explosion3 = Node(position=Vector(200, 500), transition=explosion_animation_long)
        self.root.add_child(self.explosion2)
        self.root.add_child(self.explosion3)
        self.explosion3.lifetime = 5000


    def update(self, dt):
        #  .... previous code ....
        self.arrow2.rotation_degrees += 1  # rotating 1 degree PER FRAME (not the best design)
        self.arrow3.rotation_degrees += 90 * dt / 1000  # rotating 90 degrees PER SECOND (good design!)


with Engine(virtual_resolution=Vector(800, 600)) as engine:
    # set  window properties
    engine.window.size = Vector(800, 600)
    engine.window.title = "My first kaa game!"
    # initialize and run the scene
    my_scene = MyScene()
    engine.run(my_scene)