Introduction
Embedded game development, part 3
embedded systems

Embedded game development, part 3

So far, so good. Take a look:

0:00
/0:11

That's right, object pool management (infinite ammo), physics, animation, and visual effects!

I have officially cut 1.0.0 in the Pulse2D engine and am now putting all my focus on the pilot game: the space-shooter asterisk.

Take a closer look at the thruster on the ship, a persistent sprite animation. Next, look at the visual effects on the asteroid when the laser collides with it. Very cool, right?

The DSL has changed and is no longer an all-caps, "script-like" set of macros. It has been split into two layers: a Core API (runtime) and the Internal DSL for hardware hooks, utilities, and setup.

Here's an example:

#include PULSE2D_HEADER
#include PULSE2D_GRAPHICS
#include "../include/explosion-anim.h"
#include "../include/stars-bg.h"

PULSE2D_START_PULSE();

PULSE_DEFINE_SCENE(Space_Shooter, 20, 6);
PULSE_INIT_GAME(my_game, Space_Shooter);

PULSE_DEFINE int score = 0;
PULSE_DEFINE int cooldown = 0;
PULSE_DEFINE bool enemy_hit = false;

PULSE_ON_GAMESCENE_START(Space_Shooter)
{
    my_game.set_sprite_flash("bg_stars", stars_bg, 320, 240);
    my_game.set_sprite("ship_sprite", "ship.bin", 48, 48);
    my_game.set_sprite("enemy_sprite", "enemy.bin", 48, 48);
    my_game.set_sprite("bullet_sprite", "bullet.bin", 12, 8);

    my_game.add_parallax_layer("bg_stars", 320.0f, 15.0f);

    my_game.register_vfx("explosion", explosion_frames, 64, 64, 8, 12.0f);

    my_game.init_pool("bullets",
        {
            .width = { 0.15f, 0.08f }
    });

    my_game.set_controlled_body("ship_object",
        {
            .position = { -4.0f, 0.0f },
              .width = { 0.5f,  0.5f }
    });
    my_game.set_dynamic_body("enemy_object",
        {
            .position = { 3.0f, 0.0f },
            .mass = 1.0f,
            .width = { 0.6f, 0.6f }
    });
}

PULSE_ON_GAMESCENE(Space_Shooter)
{
    my_game.tick();

    PULSE_POLL_SEESAW_GAMEPAD();

    my_game.render_backgrounds();
    my_game.set_arcade_directional_control("ship_object", 3.5f);

    pulse2d_body& ship = my_game.get_body("ship_object");

    if (cooldown > 0)
        cooldown--;

    if (SEESAW_BUTTON_INPUT(SEESAW_A) && cooldown == 0) {
        my_game.spawn("bullets",
            100,
            ship.position.x + 0.6f,
            ship.position.y,
            8.0f,
            0.0f);
        cooldown = 10;
    }

    my_game.render_pool("bullets", [&](pulse2d_body* bullet) {
        pulse2d_body& enemy = my_game.get_body("enemy_object");

        if (bullet->position.x > 6.0f) {
            my_game.despawn("bullets", bullet);
        } else {
            my_game.draw_body(bullet, "bullet_sprite");
        }

        my_game.on_collision(bullet, &enemy, [&] {
            if (!enemy_hit) {
                enemy_hit = true;
                auto coords = get_body_coordinates(&enemy);
                my_game.play_vfx("explosion", coords.x, coords.y);
                score += 100;
            }
            my_game.despawn("bullets", bullet);
        });
    });

    if (SEESAW_BUTTON_INPUT(SEESAW_START)) {
        PULSE_SET_SCENE(my_game, Space_Shooter);
    }

    my_game.draw("ship_object", "ship_sprite");

    if (!enemy_hit) {
        my_game.draw("enemy_object", "enemy_sprite");
    }

    my_game.tick_vfx();
    my_game.render();
}

PULSE_ON_GAMESTART()
{
    Serial.begin(115200);
    pulse_register_etl_error_handler();

    my_game.init(0.0f, 0.0f, 10);

    PULSE_ENABLE_SEESAW_GAMEPAD();

    PULSE_SET_SCENE(my_game, Space_Shooter);
}

PULSE_ON_GAMELOOP()
{
    PULSE_TICK_GAMESCENE();
}

Some of this should look familiar from my last post. You'll notice a new my_game identifier being used throughout, which starts from this line:

PULSE_INIT_GAME(my_game, Space_Shooter);


This initiates the new magical runtime for the game. It holds the engine, physics world, active scene, and the total list of scenes in the game. It compiles to:

using game_name_t = Runtime<Space_Shooter>; 
static game_name_t my_game {};

The runtime type has full control of actions within a scene and its physics world. This simplified resource management, API design, and, really, it feels much better to write, too: you now have Intellisense available as you build your game.

The physics is now set up with my_game.init(0.0f, 0.0f, 10) , which in the runtime emplaces the deferred hardware engine and physics:

    PULSE2D_INLINE void init(float gravity_1, float gravity_2, int solver)
    {
        engine.emplace();
        world.emplace(
            pulse2d::graphics::math::Vec2{ gravity_1, gravity_2 }, solver);
        engine->init();
    }

Scene dispatch is still handled via the internal DSL. The magic that makes scenes work seamlessly relies on function pointers and the game hooks, such as PULSE_ON_GAMESCENE_START(Space_Shooter).

PULSE_SET_SCENE(my_game, Space_Shooter) compiles to:

my_game.world->clear(); 
my_game.engine->storage().reset(); 
my_game.current_scene.emplace<Space_Shooter>(); 
pulse2d_scene_enter_Space_Shooter(); 
active_scene_fn = pulse2d_scene_fn_Space_Shooter;

This has turned out to be an API I'm pretty proud of. The complete API reference documentation can be found here.

My next post in this series will likely be my last. I'm currently designing game levels, enemies, and menus. I'm done with the engine itself. The next post will be examples from the completed pilot game! 🎮



Honestly, this project has made me think about the compiler Credence I made a lot. I'm not quite done with Credence. Since I've been so close to bare-metal, optimizing for space and code sections. I want to go back to credence and add compiler optimization options, such as -O1 -O2, and -O3.

I've had to learn about Cache Locality and the L1 Cache while working on Pulse2D and the Cortex-M7. I may even add a union language feature to really learn alignment and memory packing.

Jahan Addison
View Comments
Previous Post

Embedded game development, part 2