Video showcase
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.
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.