P-Type MiniGame

This project isn’t meant to be a proper game. It’s not going to be polished, balanced or even fun to play. Adding polish might actually detract from its purpose: to be a simple demonstration of techniques that work well on the Pokitto. It is under the same MIT license as FemtoIDE, so feel free to rip whatever code you need from it.


Elevator Pitch

This is going to be an R-Type-style sidescroller. The player controls one of four ships, the background moves from right to left and different enemies attack the player until the end of the level is reached.


End Result

The full source code is available in FemtoIDE v0.2.0.

5 Likes

Day 1

The StateMachine

Started off with a new C++ Tiles+Sprites project. There will be a start screen, the game itself, and a game over screen. Each screen will be a game state and they will be managed by a simple state machine.

One way to do this would be to create a switch and do something different for each state. The problem with this is that it mixes everything together in one place. Instead, I’d prefer to have one class for each state:

class State {
public:
  virtual ~State() = default;
  virtual void update() = 0;
};

class StateIntro : public State {
public:
  StateIntro(){ /* setup intro state */ }
  void update() override { /* do intro stuff */ }
};

State *currentState = new StateIntro();

That creates a nice separation between states. It has a new problem, though: Using the heap is prone to fragmentation, and this is especially likely when you only have 32kb of RAM.
So let’s put the states and all of their data in static memory.
Only problem is, we’d quickly run out of RAM if we just did this:

StateIntro stateIntro;
StateGame stateGame;
StateGameOver stateGameOver;
State *currentState = &stateIntro;

Since a state machine only has one active state at a time, let’s make them all occupy the same place in RAM by putting them in a union. Unions can be risky. C++17 to the rescue: we can use std::variant. Lets put that detail into its own StateMachine class:

template<class... States>
class StateMachine {
    std::variant<States...> state;
    void (*_update)(StateMachine *) = [](StateMachine *sm){
        std::get<0>(sm->state).update();
    };

public:
    void update(){ _update(this); }

    template<class StateType>
    void setState(){
        _update = [](StateMachine *sm){
            sm->state.template emplace<StateType>();
            sm->_update = [](StateMachine *sm){
                std::get<StateType>(sm->state).update();
            };
            sm->_update(sm);
        };
    }

    template<class StateType>
    StateType &getState() {
        return std::get<StateType>(state);
    }
};

template <typename Type>
class State {
public:
    static Type& get();
};

This might look ugly, but using it is quite simple:

StateMachine<
  StateIntro,
  StateGame,
  StateGameOver
> stateMachine; // create a state machine with 3 states

stateMachine.setState<StateGame>(); // activate the game state

stateMachine.update(); // update whichever state is active

StateGame &game = StateGame::get(); // get the state

If you try to access a state that isn’t active it will crash… and that’s a good thing.
If it were just a plain union, you’d be reading incorrect data and the bug would be much harder to detect and debug.

2 Likes

Day 2

Entities. Entities Everywhere.

All games have two kinds of stuff: Static stuff and stuff that moves around.
StuffThatMoves is a rubbish class name, so I’ll call them Entity.
All the objects in the game will then extend the Entity base class, giving me a single place to control the things they have in common.

What do they have in common?

  • They all have a Sprite, so might as well extend that.
  • World-space coordinates.
  • Collision detection using an Axis-Aligned Bounding Box.
  • They have to know how to put themselves on the screen.

And here is what it looks like in code:

struct AABB {
    uint8_t left;
    uint8_t right;
    uint8_t top;
    uint8_t bottom;

    void init(Sprite& sprite){
        left = sprite.getFrameX();
        right = left + sprite.getFrameWidth();
        top = sprite.getFrameY();
        bottom = top + sprite.getFrameHeight();
    }

    void shrink(uint32_t amount){
        left += amount;
        right -= amount;
        top += amount;
        bottom -= amount;
    }
};

class Entity : public Sprite {
public:
    AABB boundingBox;
    int16_t x = 0;
    int16_t y = 0;
    uint8_t recolor = 0;
    bool mirror = false;
    bool flip = false;

    virtual void update(int32_t cameraX, int32_t cameraY) {
        draw(cameraX + x, cameraY + y, flip, mirror, recolor);
    }

    bool collidesWith(Entity &other){
        if(x + boundingBox.left > other.x + other.boundingBox.right)
            return false;
        if(x + boundingBox.right < other.x + other.boundingBox.left)
            return false;
        if(y + boundingBox.top > other.y + other.boundingBox.bottom)
            return false;
        if(y + boundingBox.bottom < other.y + other.boundingBox.top)
            return false;
        return true;
    }
};

Now if I want to put something on the screen, I can just make an Entity and set it up:

class StateX {
  Entity stuff;
public:
  StateX(){
    stuff.play(stuffSprite, StuffSprite::RunAround);
  }
  void update(){
    stuff.update(0, 0);
  }
};
2 Likes

Day 3

The Intro Screen

The most important screen in a game is the intro screen.
This game will feature four ships, so I’ll use this screen to select the ship you want to use.
I’ll have 4 floating head entities on screen, one representing each ship, and will highlight the currently selected one by recoloring all the non-selected options:

    selection %= 4;
    for(int i = 0; i<sizeof(head) / sizeof(head[0]); ++i){
        head[i].recolor = (selection == i) ? 0 : 30 * 8;
        head[i].update(0, 0);
    }

(in retrospect, I should’ve used a headCount variable, but meh)

This works well enough, but it really needs some kind of fancy background.
The background in TAS mode is made of Tiles, so let’s use a Tilemap:

    uint8_t map[14*11] = {};

    for(int i = 0; i < 256; ++i)
        tilemap.setColorTile(i, i);

    tilemap.set(14, 11, map);

By using setColorTile we can have tiles that are of a solid color.
Note that map isn’t const. We can write to the map every frame.
It’s like having a framebuffer with really large pixels!

void StateIntro::updateMap(){
    constexpr float pi = 3.14159265f;

    time++;
    for(int y = 0; y < 11; ++y){
        float sy = cos((y + time * 0.25f) * (pi / 11.0f)) * 10.0f;
        for(int x = 0; x < 14; ++x){
            map[y * 14 + x] = sy * cos(x * (pi / 14.0f)) + time * 0.5f;
        }
    }

    tilemap.draw(0, 0);
}

Since the framebuffer resolution is so low, there is no need to optimize this code by removing the floats and cos.

And here’s the result:

2 Likes

Day 4

Oh, right, there needs to be a Game.

In-game we’ll have:

  • A HUD
  • The Player
  • Some enemies
  • Lots of bullets flying around

So each one is a class of its own.
Again, we don’t want to use the heap. Instead of using something like
Player *player = new Player(), all of the entities are stored directly in the state, like this:

    Enemy enemy[enemyCount];
    Bullet playerBullet[bulletCount];
    Bullet enemyBullet[bulletCount];
    Player player;

That means that a pool of enemies and bullets are created in the beginning.
When the player shoots, a “dead” bullet will be picked from the playerBullet array and it will be considered “live” until its life (ticksToLive) runs out.

Enemies are recycled in a similar way.
I wanted to be able to place them on the map using Tiled.
By using a custom tile property, I can define what type of enemy should be spawned in a certain position:
image

With that I can freely place enemy spawn points on the map:

I don’t want the spawn points to be visible, however, so I put them on their own hidden layer.
Now, when the screen scrolls to the right and a new column of tiles becomes visible, I need to check if there are any spawn points on it and create the corresponding Enemy:

void StateGame::spawnEnemies(){
    int spawnColumn = (-cameraX + PROJ_LCDWIDTH) / POK_TILE_W;
    if(spawnColumn == prevSpawnColumn){
        return;
    }
    prevSpawnColumn = spawnColumn;

    for(int y = 0; y < Level1[1]; ++y){
        auto tile = Level1Enum(spawnColumn, y);
        if(tile&Karen)
            spawnEnemy(spawnColumn, y, EnemySprite::Karen);
        if(tile&Bob)
            spawnEnemy(spawnColumn, y, EnemySprite::Bob);
    }
}

As for the HUD, I use another Tilemap that only gets drawn when the player’s HP changes:

void StateGame::updateHUD(){
    if(!uidirty)
        return;
    uidirty = false;
    PD::setTASRowMask(0b1111'11000000'00000000);

    uimap.draw(0, 0);
    PD::setCursor(3 * POK_TILE_W + 5, 9 * POK_TILE_H - 3);
    PD::fontSize = 2;
    PD::color = 49;
    PD::bgcolor = PD::invisiblecolor;
    PD::print(player.getHP());
    PD::update();

    PD::setTASRowMask(0b0000'00111111'11111111);
}

By using a mask, the HUD’s tilemap won’t try to draw over the game. Once it’s done, it inverts mask so that the game won’t draw over the HUD.

2 Likes

Day 5

More Gameplay

Sprites need to be drawn in the correct order. For this, I need to make a list of all the entities and sort it so that the sprites that should be on top are at the end of the list.

I could use std::sort. I should probably use std::sort. std::sort is the proper way to do this.

So let’s do something else instead:

Before drawing an entity, check if the next one should be behind it.
If their order is incorrect, swap them.
Done! Kinda. Things might be a little messed up at some point, but in the end everything is gonna be alright.

    Entity *next;
    for(int i = 0; i < totalEntityCount - 1; ++i){
        Entity *current = entities[i];
        next = entities[i+1];

        if(current->y > next->y){
            entities[i] = next;
            entities[i+1] = current;
            current = next;
            next = entities[i+1];
        }

        current->update(cameraX + shakeX, cameraY + shakeY);
    }

    next->update(cameraX + shakeX, cameraY + shakeY);

The player, bullets and enemies are on screen. I need them to collide with each other.

Getting the bounds of a sprite is a somewhat costly operation and it’s good to use a cache.
In this case, the bounds are stored in each Entity’s AABB when the entity is initialized:

    void spawn(int32_t x, int32_t y, EnemySprite::Animation anim){
        play(enemySprite, anim);
        boundingBox.init(*this); // load bounds from animation frame
        boundingBox.shrink(2); // shrink bounds by 2 pixels on each side

To check if one entity collides with another, entities have the collidesWith function:

    bool collidesWith(Entity &other){
        if(x + boundingBox.left > other.x + other.boundingBox.right)
            return false;
        if(x + boundingBox.right < other.x + other.boundingBox.left)
            return false;
        if(y + boundingBox.top > other.y + other.boundingBox.bottom)
            return false;
        if(y + boundingBox.bottom < other.y + other.boundingBox.top)
            return false;
        return true;
    }

With that it’s really simple for an Enemy to check if it is colliding with the Player:

    auto& player = StateGame::get().getPlayer();
    if( collidesWith(player) ){
        player.hit(HP);
        HP = 0;
        return;
    }
2 Likes

Day 6

Sound

As is mandated by tradition, adding sound and music has been left to the very last moment.

For music, I:

  • Created a new dream.beep file.
  • Clicked File -> Import URL
  • Pasted the URL to a neat tune by Spectrumnist.
  • Clicked File -> Export Song -> Export song to .raw
  • Enabled Copy to SD on the music folder
  • Added the following to main.cpp:
    PS::playMusicStream("music/dream.raw", 0);

Why that zero, you ask?

As for sound effects, I converted a WAV to C++:

To play it all I need is this:

#include "sfx/Boop.h"

Pokitto::Sound::playSFX(Boop, sizeof(Boop));

Done!

6 Likes