Text engine stuff today.
Now I can easily add NPCs & dialogue points to my Tiled tilemap.
hud::TextBox
(edit: previously hud::DialogueBox
)DialoguePoint
,NpcSpawnPoint
to spawn Npc
Download this as .mp4 video (audio included)
This TextBox
prints the characters in a single sprite, and adds additional sprite when it is full.
See this message in the gbadev discord butano channel for more info.
exelotl wrote:
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! ^^
Thanks!
There's also this mini-game-ish combat I haven't added yet, which is one of the main reason I wanted to port this game.
The other being the OST; Every single song in this game is a decent chiptune, they are still in my chiptune playlist.
Finally, something that can be (barely) called a video game:
Download this as .mp4 video (audio included)
There's still tons of things missing, but I'm happy to see it is finally somewhat playable.
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.
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.
Wow, it's been 2 weeks from the last post? Time flies.
This time, I implemented EventQueue
and the player 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.
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&)
.
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 tobn::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);
...
}
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;
...
};
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.
It seems that it's not possible to embed videos in posts now.
It would be nice if video embedding is supported. (youtube videos, direct link to .mp4
files, etc.)
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.
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 SpawnPoint
s 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
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.
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.
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.
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.