Part 3: Organizing the game code

We’ve learned how to add objects to the nodes tree (draw them on the screen), how to transform them (move, rotate, scale), add child nodes to other nodes and how to use animations. Let’s start writing the actual game!

The game will be a top-down shooter with 3 weapons: machine gun, grenade launcher and force gun (will shoot non-lethal bullets which will push enemies away) and one type of enemy (a zombie). Enemies will have a basic AI with two behavior patterns: walk towards the player or just walk towards randomly selected point. We will implement some animations such as explosions and blood splatters. We’ll use kaa’s physics system to detect collisions between bullets and enemies as well as between characters in the game (player and enemies). We’ll add some sound effects and music for a better experience. We will also learn how to draw text and how to control a camera. Finally, we’ll learn how to add more scenes, such as main screen or pause screen and how to switch between them.

It would not look good if we put all that stuff in main.py, so let’s create a better structure for the game files and folders first. We’ll also clean up the code we wrote before.

Before we begin

From this point on we’re writing the actual game and the tutorial will have a lot of code in form of snippets.

Be aware that there will be two types of code examples:

A general example that explains a mechanism existing in the code. You don’t need to put that code anywhere.

def foo()
    print('Hello world')

An actual code of the game we’re creating. Those code snippets will have a blue header bar telling you which file you should put the code in. For example, this code should be put in folder/subfolder/foo.py

folder/subfolder/foo.py
def bar():
    print('hello sailor!')

Structure of directories & files

Let’s start with creating the proper folders and files hierarchy structure for our game. Create the following python packages and files structure.

my_game/
    assets/
        ... all assets folder content here...
    common/
        __init__.py
        enums.py
    controllers/
        __init__.py
        assets_controller.py
        enemies_controller.py
        explosions_controller.py
        collisions_controller.py
        player_controller.py
    objects/
        weapons/
            __init__.py
            base.py
            force_gun.py
            grenade_launcher.py
            machine_gun.py
        bullets/
            __init__.py
            force_gun_bullet.py
            grenade_launcher_bullet.py
            machine_gun_bullet.py
        __init__.py
        player.py
        enemy.py
        explosion.py
    scenes/
        __init__.py
        gameplay.py
        pause.py
        title_screen.py
    main.py
    registry.py
    settings.py

Controllers package will store classes to handle the game logic and manage objects.

Objects package will hold classes for different types of objects that will appear in the game. To keep it clean we’ll have one .py file for one object type.

Scenes package will hold scenes. Yes, our game will eventually have many scenes, we will get there later in the tutorial.

settings.py is a config file for our game

registry.py is a module to store global variables - we’ll be able to import them from anywhere in our code.

Note

The organization above is just a suggestion, not some rigid convention required by the kaa engine. You can work out your own patterns for organizing the game files and folders, and use whatever works best for you. You don’t need to follow naming conventions used in this tutorial. You can call controllers ‘managers’, or re-name the entry module main.py to something else. Whatever works for you.

Storing global variables and objects

Let’s start with settings.py:

settings.py
# Let's use full HD as a base resolution for our game!
VIEWPORT_WIDTH = 1920
VIEWPORT_HEIGHT = 1080

Then registry.py:

registry.py
class Registry: # serious name, to look like a pro. In fact won't do anything - will just serve as a bag for objects :))
    pass

global_controllers = Registry()
scenes = Registry()

Keep scenes in separate .py files

Let’s create a stub of a Gameplay scene in scenes/gameplay.py

scenes/gameplay.py
from kaa.engine import Scene

class GameplayScene(Scene):

    def __init__(self):
        super().__init__()

    def update(self, dt):
        pass

Keep the main.py clean

Finally, let’s now clean up the main.py. Generally, the main module should have as little lines as possible because we want the entire game logic to be in controllers, objects and scenes classes.

main.py
from kaa.engine import Engine
from kaa.geometry import Vector
import settings
from scenes.gameplay import GameplayScene

with Engine(virtual_resolution=Vector(settings.VIEWPORT_WIDTH, settings.VIEWPORT_HEIGHT)) as engine:
    # set window to fullscreen mode
    engine.window.fullscreen = True
    # initialize and run the scene
    gameplay_scene = GameplayScene()
    engine.run(gameplay_scene)

Our main.py looks very pro now! Run the game to make sure it works. You should see an empty, black screen. Press Alt+F4 to close it.

Load assets just once, from one place, and make them visible from everywhere

Proper assets management is very important. In Part 2 of the tutorial we have created Sprite objects inside Scene’s __init__. It might work OK in a small game, but in the long run it’s not a good idea because some scenes can be destroyed and created again. If we load assets inside scene’s __init__ - we would re-load the same assets files from disk each time scene is reset (e.g. when player starts a new game).

Scene’s __init__ should only create Nodes needed to initialize the scene. Sprites and other assets-related objects are immutable, so should be created only once, when the game starts. That’s what our AssetsController class is for. Let’s edit the assets_controller.py file:

controllers/assets_controller.py
import os
from kaa.sprites import Sprite, split_spritesheet
from kaa.geometry import Vector

class AssetsController:

    def __init__(self):
        # Load images:
        self.background_img = Sprite(os.path.join('assets', 'gfx', 'background.png'))
        self.title_screen_background_img = Sprite(os.path.join('assets', 'gfx', 'title-screen.png'))
        self.player_img = Sprite(os.path.join('assets', 'gfx', 'player.png'))
        self.machine_gun_img = Sprite(os.path.join('assets', 'gfx', 'machine-gun.png'))
        self.force_gun_img = Sprite(os.path.join('assets', 'gfx', 'force-gun.png'))
        self.grenade_launcher_img = Sprite(os.path.join('assets', 'gfx', 'grenade-launcher.png'))
        self.machine_gun_bullet_img = Sprite(os.path.join('assets', 'gfx', 'machine-gun-bullet.png'))
        self.force_gun_bullet_img = Sprite(os.path.join('assets', 'gfx', 'force-gun-bullet.png'))
        self.grenade_launcher_bullet_img = Sprite(os.path.join('assets', 'gfx', 'grenade-launcher-bullet.png'))
        self.enemy_stagger_img = Sprite(os.path.join('assets', 'gfx', 'enemy-stagger.png'))
        # few variants of bloodstains, put them in the same list so we can pick them randomly later
        self.bloodstain_imgs = [Sprite(os.path.join('assets', 'gfx', f'bloodstain{i}.png')) for i in range(1, 5)]

        # Load spritesheets
        self.enemy_spritesheet = Sprite(os.path.join('assets', 'gfx', 'enemy.png'))
        self.blood_splatter_spritesheet = Sprite(os.path.join('assets', 'gfx', 'blood-splatter.png'))
        self.explosion_spritesheet = Sprite(os.path.join('assets', 'gfx', 'explosion.png'))
        # enemy-death.png has a few death animations, so make this a list
        self.enemy_death_spritesheet = Sprite(os.path.join('assets','gfx','enemy-death.png'))

        # use the spritesheets to create framesets
        self.enemy_frames = split_spritesheet(self.enemy_spritesheet, frame_dimensions=Vector(33, 74))
        self.blood_splatter_frames = split_spritesheet(self.blood_splatter_spritesheet, frame_dimensions=Vector(50, 50))
        self.explosion_frames = split_spritesheet(self.explosion_spritesheet, frame_dimensions=Vector(100, 100), frames_count=75)

        self.enemy_death_frames = [
            split_spritesheet(self.enemy_death_spritesheet.crop(Vector(0, i*74), Vector(103*9, 74)),
                              frame_dimensions=Vector(103, 74)) for i in range(0, 5)
        ]

The code is using features we’ve learned in previous chapter: creating a new Sprite, using crop method and using split_spritesheet to prepare individual animation frames which we’ll use later.

Feel free to review the contents of the assets/gfx folder to verify we’re loading the files correctly.

As stated above, we want the assets controller to initialize just once and then be globally visible. Let’s modify the main.py in a following way:

main.py
import registry
from controllers.assets_controller import AssetsController

with Engine(virtual_resolution=Vector(settings.VIEWPORT_WIDTH, settings.VIEWPORT_HEIGHT)) as engine:
    # initialize global controllers and keep them in the registry
    registry.global_controllers.assets_controller = AssetsController()
    # ..... rest of the code .....

It’s good to keep scenes in a global registry too

It’s practical to store scene instances in the global registry as well. That will make them accessible from anywhere in the code. Let’s modify that part of main.py where GameplayScene is created:

main.py
with Engine(virtual_resolution=Vector(settings.VIEWPORT_WIDTH, settings.VIEWPORT_HEIGHT)) as engine:
    # ..... previous code .....
    # initialize scenes and keep them in the registry
    registry.scenes.gameplay_scene = GameplayScene()
    engine.run(registry.scenes.gameplay_scene)

Write classes for your in-game objects and inherit from kaa.Node

It would look much better if we could add a <Player> object to a scene, not just some generic <Node>, right? Let’s do this.

Let’s write a Player class that extends kaa Node. <Player> instance will represent a character controlled by the player.

objects/player.py
from kaa.nodes import Node
import registry


class Player(Node):

    def __init__(self, position, hp=100):
        # node's properties
        super().__init__(z_index=10, sprite=registry.global_controllers.assets_controller.player_img, position=position)
        # custom properties
        self.hp = hp
        self.current_weapon = None

By extending Node we can introduce our custom properties, such as player’s hit points. Also, notice how we imported and used our registry.py to access the sprite stored in the assets controller.

Let’s create classes for weapons the same way. They won’t have any custom properties for now. We’ll have a base class, called WeaponBase extending Node, and all our wepons will then extend the WeaponBase.

objects/weapons/base.py
from kaa.nodes import Node


class WeaponBase(Node):

    def __init__(self, *args, **kwargs):
        super().__init__(z_index=20, *args, **kwargs)
objects/weapons/machine_gun.py
import registry
from objects.weapons.base import WeaponBase


class MachineGun(WeaponBase):

    def __init__(self):
        # node's properties
        super().__init__(sprite=registry.global_controllers.assets_controller.machine_gun_img)
objects/weapons/force_gun.py
import registry
from objects.weapons.base import WeaponBase


class ForceGun(WeaponBase):

    def __init__(self):
        # node's properties
        super().__init__(sprite=registry.global_controllers.assets_controller.force_gun_img)
objects/weapons/grenade_launcher.py
import registry
from objects.weapons.base import WeaponBase


class GrenadeLauncher(WeaponBase):

    def __init__(self):
        # node's properties
        super().__init__(sprite=registry.global_controllers.assets_controller.grenade_launcher_img)

Implement higher-tier logic in controller classes

Let’s now write a controller class to manage the Player. Generally we want the controller classes to be used for higher-tier logic such as interactions between in-game objects and other classes (controllers or other in-game objects), managing collections, handling input, and so on…

Another important thing we want controllers to do is to add initial objects to the scene. Let’s start with exactly that:

controllers/player_controller.py
import settings
from objects.player import Player
from kaa.geometry import Vector

class PlayerController:

    def __init__(self, scene):
        self.scene = scene
        self.player = Player(position=Vector(settings.VIEWPORT_WIDTH/2, settings.VIEWPORT_HEIGHT/2))
        self.scene.root.add_child(self.player)

Note

As your code base will grow and you’ll add more objects and controllers you will sometimes face a dillema where to put your code: in the object class, in the controller class or maybe even directly in the scene class? We can’t give you precise answers here, just use common sense and general good programming practices for keeping your code clean.

Let’s add the player controller to the scene:

scenes/gameplay.py
from controllers.player_controller import PlayerController


class GameplayScene(Scene):

    def __init__(self):
        super().__init__()
        self.player_controller = PlayerController(self)

Finally, let’s run the game! We should see the player in the middle of the screen, holding the machine gun.

Lastly, let’s add some nicer background (black background is not fun).

scenes/gameplay.py
import registry
import settings
from kaa.nodes import Node
from kaa.geometry import Vector
# ... other imports...

class GameplayScene(Scene):

    def __init__(self):
        super().__init__()
        self.root.add_child(Node(sprite=registry.global_controllers.assets_controller.background_img,
                                 position=Vector(settings.VIEWPORT_WIDTH/2, settings.VIEWPORT_HEIGHT/2),
                                 z_index=0))
        # .... rest of the function ....

Run the game and enjoy the sights.

Let’s move on to the Part 4 of the tutorial where we’ll learn how to handle input from mouse and keyboard.