gbadev
Game Boy Advance homebrew development forum
Member
avatar
Joined:
Posts: 9

Recently, I've been reading the SFML Game Development book.
It covers a lot of topic about gamedev from scratch, including state stack based Scene class, GUI, command pattern, etc.

As a practice, I'm going to port Creepy Castle to the GBA, using Butano engine.

As it's an unauthorized port, I won't release it other than some video recordings...

Nothing much, only the GUI stuff for now;

The button implementation is a copycat of this menu implementation, but I'm not dynamically allocating it.

// buttons as member variables of the scene
ui::Container<4>           _uiContainer;
ui::SaveSlotButton         _btnSaveSlot[SAVE_SLOT_COUNT];
ui::IconTextButton<1, 4>   _btnBack;
ui::IconTextButton<2, 8>   _btnDeletion;

// initialize the buttons
_btnSaveSlot[0].setSaveFile(&_fakeTestSave1);
_btnSaveSlot[0].setPosition(SAVE_SLOT_BTN_START_POS);
_btnSaveSlot[0].setCallback([this]() {
    reqStackPop();
    reqStackPush(SceneId::GAME);
    getContext().saveFile = &_fakeTestSave1;
});
...

// add the buttons to the container
_uiContainer.addElement(&_btnSaveSlot[0]);
_uiContainer.addElement(&_btnSaveSlot[1]);
_uiContainer.addElement(&_btnBack);
_uiContainer.addElement(&_btnDeletion);

I'll work on loading 16x16 meta-tile based tilemap on this week.

Moderator
Joined:
Posts: 5

Looks great!

Have you tried talking to the author of the original game about releasing a small demo?
It could help as advertising, I guess...

Member
avatar
Joined:
Posts: 9

GValiente wrote:

Have you tried talking to the author of the original game about releasing a small demo?
It could help as advertising, I guess...

I haven't yet, but if this goes far enough, I'll consider it.
For now, the plan is releasing a small fan-made boss fight video on April Fools' Day.

Member
Joined:
Posts: 18

This looks so good! One of my favorite things about GBA is the landscape orientation of the display area. It translates beautifully.


-e9

Member
avatar
Joined:
Posts: 9

So, a week passed.
I've been implementing 16x16 meta-tilemap loading this week. (and still WIP)

I have this map hand-crafted on Tiled map editor.
Tiled tilemap

Upon build, it's parsed with pytmx to generate hard-coded C++ constexpr object like this.

// Auto-generated by `tool/mtilemap_gen.py` on 2023-02-16 21:29:36
// DO NOT EDIT this file;  It will be overwritten on next build!
#include "mtile/MTilemap.h"
...
inline constexpr MTilemap<95, 95, 16, 18, 8, 11, 1, 7> mtilemap_sc1_room0 (
    bn::regular_bg_tiles_items::mtileset_sc1_room0, 
    bn::bg_palette_items::pal_default_bg,
    { MobSpawnPoint{ 13, 7, entity::mob::MobKind::SQUEAKER }, ... },
    { ItemSpawnPoint{ 21, 7, entity::item::ItemKind::YELLOW_KEY }, ... },
    { DoorSpawnPoint{ 29, 6, entity::DoorKind::WHITE_KEY, core::HDirection::NONE }, ... },
    ...
    // 16x16 meta-tile id for `dynamic_regular_bg`.
    { ... , 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, ... },
);
...

And I can read these SpawnPoints and Meta-tile IDs to generate the things in actual world.

void World::setMTilemap(const mtile::MTilemapBase& mTilemap) {
    // init `dynamic_regular_bg` tiles
    _bg.setMTilemap(mTilemap);
    // init sprites
    span<const MobSpawnPoint> mobSpawnPoints = mTilemap.getMobSpawnPoints();
    BN_ASSERT(spawnPoints.size() <= entities.max_size(), "Not enough space for mobs (",
              spawnPoints.size(), " <= ", entities.max_size(), ")");
    for (const auto& spawn : mobSpawnPoints) {
        _mobs.emplace_back(spawn);
        _mobs.back().setCamera(camera);
        _mobs.back().allocateGraphics();
    }
    ...
}

And after dealing with meta-tile to gba-tile conversion, this pops out. yay

gba results gif

Scrolling is done based off the Butano's dynamic_regular_bg and camera.
It's done by checking the changed position of the camera each frame, and reload the meta-tiles around it if necessary.

scrolling BG

I've done this before, but it's much more clean this time, thanks to the Butano's update.

...and also thanks to the small camera window space.
I didn't have to load half-meta-tile like before, which complicates things a bit.

Moderator
Joined:
Posts: 5

Great update, I hope you can release a public demo or something.

Member
avatar
Joined:
Posts: 9

Video showcase

Wow, it's been 2 weeks from the last post? Time flies.
This time, I implemented EventQueue and the player movement.

hero_movement
See this as .mp4 video (audio included)

Nice, it's much better with the player animation and audio.

I'll explain how this works under the hood.

Explanation

First of all, there's a central EventQueue which stores and propagates every event happened in the game.
And there's EventProducer & EventListener, who pushes/listens an event to/from EventQueue.

Say, if an object wants to send an event to the EventQueue,
its class should derive from EventProducer, and call sendEvent<EArgs>({...}, delayTicks).

Similarly, if an object wants to receive events from the EventQueue,
its class should derive from EventListener, and override handleEvent(const EventProducer& sender, const arg::EventArgs&).

Code (Usage)

Here's the example code that triggers player fall off a cliff.
WorldCollision derives both from EventListener & EventProducer .
when it receives HERO_MOVE_END event, and if this is a falling point, it sends COLL_HERO_FALL_REQ back.

void WorldCollision::handleEvent(const EventProducer& sender, const event::arg::EventArgs& args) { ...
    switch (args.eventType) { ...
        case event::EventType::HERO_MOVE_END: {
            const auto& hero = static_cast<const entity::hero::Hero&>(sender);
            const auto heroPos = hero.getMCellPos();

            for (const auto& fallPoint : _mTilemap->getFallPoints())
                if (heroPos == bn::point{ fallPoint.mCellX, fallPoint.mCellY })
                    sendEvent<event::arg::EventArgs>({event::EventType::COLL_HERO_FALL_REQ});
            break;
        } ...
    }
}

Code above uses static_cast, so I should be extremely careful about who the actual sender is, to avoid invalid cast.
Who's the actual sender? It is prefixed on EventType (e.g. EventType::HERO_*, EventType::COLL_*).
I know this is kinda dangerous, maybe I should move to bn::any & any_cast instead?

Then, our player class (it's Hero) will receive this COLL_HERO_FALL_REQ event, and do what they want.
I've also implemented event filtering, so only the Hero will receive COLL_HERO_FALL_REQ event.

You can also use custom event arguments which derives from event::arg::EventArgs
And you can delay the event for certain frames, thanks for EventQueue using priority_queue internally.

void Hero::handleHeroMoveReq(const event::arg::HeroMove& moveArgs) { ...
    // send `HERO_MOVE_BEGIN` event right now, with `arg::HeroMove` event arguments.
    sendEvent<event::arg::HeroMove>({event::EventType::HERO_MOVE_BEGIN, moveArgs.direction, moveArgs.isAtStairs});
    ...
    // send `HERO_MOVE_END` event after `constants::HERO_WALK_FRAMES` ticks.
    sendEvent<event::arg::HeroMove>({event::EventType::HERO_MOVE_END, ...}, constants::HERO_WALK_FRAMES);
    ...
}

Code (EventQueue)

Here's the gist of EventQueue, with some checks omitted.

class EventProducer { ...
protected:
    template <typename EArgs> requires arg::EArgsConcept<EArgs> // size & derive from `arg::EventArgs` check
    void sendEvent(const EArgs& args, int delayTicks = 0) {
        _eventQueue.pushEvent(*this, args, delayTicks);
    } ...
};
class EventQueue { ...
public:
    template <typename EArgs> requires arg::EArgsConcept<EArgs> // size & derive from `arg::EventArgs` check
    void pushEvent(const EventProducer& sender, const EArgs& args, int delayTicks) {
        BN_ASSERT(_priorityQueue.size() < constants::MAX_EVENTS_IN_QUEUE);
        arg::EventArgs& newArgs = _pool.create<EArgs>(args);
        _priorityQueue.emplace(_sessionTime + core::PlayTime(delayTicks), _insertOrderCounter++, &sender, &newArgs);
    }
    void EventQueue::propagateEvents() {
        // `EventQueueElem`s are sorted first with `time`, and then `insertOrder`.  So, I can do this.
        while (!_priorityQueue.empty() && _sessionTime >= _priorityQueue.top().time) {
            const auto& eventElem = _priorityQueue.top();
            for (EventListener* listener : _listeners)
                if (listener->isSubscribing(eventElem.args->eventType))
                    listener->handleEvent(*eventElem.sender, *eventElem.args);
            _pool.destroy(*eventElem.args);
            _priorityQueue.pop();
        }
    } ...
private:
    bn::generic_pool<constants::MAX_EVENT_ARG_SIZE, constants::MAX_EVENTS_IN_QUEUE> _pool;
    std::priority_queue<EventQueueElem, bn::vector<EventQueueElem, constants::MAX_EVENTS_IN_QUEUE>,
                        std::greater<EventQueueElem>> _priorityQueue;
...
};
class EventListener { ...
public:
    virtual void handleEvent(const EventProducer& sender, const arg::EventArgs&) = 0;

    bool EventListener::isSubscribing(EventType type) const { ...
        return _subscribeFilter[(int)type];
    }
    void EventListener::subscribe(EventType type) { ...
        _subscribeFilter[(int)type] = true;
    }
    void EventListener::unsubscribe(EventType type) { ...
        _subscribeFilter[(int)type] = false;
    }
private:
    bn::bitset<EVENT_TYPE_BITSET_SIZE> _subscribeFilter;
    ...
};

Note

Game Programming Patterns and Game Engine Architecture are really helpful to roll out this EventQueue, you should check them out too.

I wanted to write this post concise, but the snippet is a little too much, unfortunately.
Hope I can write it shorter next time.

Member
avatar
Joined:
Posts: 9

Gosh, 3 weeks already.
I'll just put an image and a video showcase this time.

I can now put these "hearts" on the map like below.
They will become chests with some random foods in it.
Item placement

Item usage
Download this as .mp4 video (audio included)

I can pick up some food, eat them when I'm low on HP.
Also, I can open the doors with the keys.

I also added the title screen music, although it's not finished yet.
(it's too long, and I'm bad at trascribing music)

That's all for now.

Member
avatar
Joined:
Posts: 9

Finally, something that can be (barely) called a video game:


Download this as .mp4 video (audio included)

  • Basic battle implemented.
    • Of course, there's tons of things to add further, like Duel (mini-game battle) and Special Attack.
  • Battle theme plays when fighting with the HP>2 mobs, and stops playing when they are out of sight.
  • Particle effects added when mobs & hero dies.
    • Plus, camera shakes when mobs die.
  • Game over implemented.

There's still tons of things missing, but I'm happy to see it is finally somewhat playable.

Administrator
avatar
Joined:
Posts: 49

This is looking superb! I didn't know much about this game but the blend of mechanics (turn based combat + sidescroller) stands out to me as something pretty unique & refreshing, I can see why you wanted to port it!
Keep up the great work! ^^