Apr 9 2023

Events, Queues, and new APIs

Introducing Events, the Event API, and new Component Storage API


Introduction

This Devlog is going to cover a good bit. It’s going to cover how I solved the problem of “attacks” and also cover a bit on how I started to clean up some of my code. The engine started to get a bit “all over the place” and although I am using ECS as my high level architecture, the general day-to-day use felt a bit hectic. As I solved the “Attack” problem with Events, at the same time I began to create APIs for large sections of my code. This inolved some compile time polymorphism (using Templates) and sectioning off of certain code modules into managers. This will be elaborated upon further in the article. This is a fairly code-heavy piece.

Problem Description

As I was working on attacks, I ran into the issue of how do I initiate and follow through with attacks? One way would be the brute-force use of boolean flags and setting hit-box detection and animation components based on flags. I was not a fan of this. How would I extend to other usable components? What about when I come to health potions? What about when I start to introduce magic staffs and Bow and Arrows? Do I just devolve to boolean hell? How will I incorporate AI into this and allow them to trigger Attacks/Heals/etc. This stumped me for a while but luckily I was able to come up with a pretty nice solution that I really enjoy.

Introducing Events, EventQueues, and EventPools

When coming up with the Attack, I quickly decided that any melee attack should really conform to the following Finite State Machine (FSM):

I didn’t like the idea of creating explicit FSM’s and incorporating them into the code, so I needed a way to abstract this into more usable situations. And this is where Events, EventQueues, and EventPools are introduced. Here are the following APIs that they conform to (bit of a code dump, apologies):

struct Event 
{
    bool is_complete;

    void trigger(EntityManager& entity_manager);
    void process(EntityManager& entity_manager);
    void flush(EntityManager& entity_manager);
};
template<typename EventType>
struct EventQueue
{
	std::queue<EventType> events;

	void enqueue(EventType& event) {...}

	EventType& peek() {...}

	EventType& dequeue() {...}

	size_t size() {...}

	void process(EntityManager& entity_manager, EventPool<EventType>& pool)
	{
		while (!events.empty())
		{
			EventType& event = dequeue();
			event.trigger(entity_manager);
			pool.insert(event);
		}
	}
};
template<typename EventType>
struct EventPool
{
	std::set<EventType> pool;

	void insert(EventType& event);

	void remove(EventType e);

	void process(EntityManager& entity_manager)
	{
		for (auto it = pool.begin(); it != pool.end();) 
		{
			EventType& event = *it;
			if (event.is_complete) 
			{
				event.flush(entity_manager);
				it = pool.erase(it);
			}
			else 
			{
				event.process(entity_manager);
				it++;
			}
		}
	}
};

and the following is the EventManager which manages all the Queues and Pools:

struct EventManager {
	EventQueue<AttackEvent> attack_queue;
	EventPool<AttackEvent> attack_pool;
	
    //...

	void process_events(EntityManager& entity_manager)
    {
        attack_queue.process(entity_manager);
        attack_pool.process(entity_manager);

        //...
    }

};

EventQueue essentially becomes a wrapper for the std::queue, and the EventPool becomes a wrapper for a std::set. The reason this is important is because now, the event can handle itself. For example, here is the following lifespan of an AttackEvent:

First, something like the CharacterController can generate an attack event:

if (input.clicked == GLFW_PRESS && input.prev_clicked == GLFW_RELEASE) {
	AttackEvent ae = AttackEvent(e);
	event_manager.attack_queue.enqueue(ae);
}

This will generate an attack event with the associated entity_index and enqueue into the correct Queue. Notice this is completely generator agnostic. The Character Controller, AI, proximity sensor, etc. could all generate AttackEvents, the only required information is the entity_index.

This gets enqueued into the AttackQueue, which gets processed at the end of my GameState integration step.

During the Queue process step, the Event is dequeued and the trigger(...) method gets called. This is how the event is initiated. In the case of the AttackEvent, it sets and modifies all relevant components, in this case, sets some flags and transitions the Animation state:

    void AttackEvent::trigger(EntityManager& entity_manager)
    {
        auto equipment = entity_manager.equipment_components[e];

        auto melee_weapon_ent = equipment.equipment[0].id;

        entity_manager.flags[melee_weapon_ent].set({
            tags::sprite,
            tags::skeleton_animation,
            tags::hit_box
        });

        entity_manager.skeletal_animation_components[melee_weapon_ent].current_state = "ATTACKING";
    }

Once here, the EventQueue will place this event in the EventPool where it gets processed every frame:

    void AttackEvent::process(EntityManager& entity_manager)
    {
        auto equipment = entity_manager.equipment_components[e];

        auto sword_ent = equipment.equipment[0].id;

        SkeletalAnimationComponent& skel = entity_manager.skeletal_animation_components[sword_ent];
        SkeletalAnimationState& state = skel.states[skel.current_state];

        if (state.is_complete)
        {    
            state.is_complete = false;
            is_complete = true;
        }  
    }

In this case, the AttackEvent‘s process(...) is really only checking if the animation is complete. If it is, then we need to flush(...) the event. The flush stage will consist of doing any clean-up such as e.g. unsetting the relevant flags, and making sure all Animation States are correct. This stage should essentially undo the trigger(...) stage:

    void AttackEvent::flush(EntityManager& entity_manager)
    {
        auto equipment = entity_manager.equipment_components[e];

        auto melee_weapon_ent = equipment.equipment[0].id;

        entity_manager.flags[melee_weapon_ent].unset({
            tags::sprite,
            tags::skeleton_animation,
            tags::hit_box
        });

        entity_manager.skeletal_animation_components[melee_weapon_ent].current_state = "IDLE";
    }

And that is the complete lifecycle of an AttackEvent. You can map these states directly to the aforementioned FSM as well.

All in all, this means the EventManager‘s can remain relatively agnostic to the event type and allow the event itself to do all of it’s own processing. For example, a Health Potion may replenish 25 health all at once, in which case its process(...) method will remain fairly simple, whereas a Health Regen potion may regen a set 2 Health Per Second and its process(...) method will be a bit more involved. This keeps all logic barricaded to the Event itself and in separate states, and allows the Systems (the ECS systems) to do their work without having to worry about various edge case boolean flags.

This seems to be a really nice solution that extends quite nicely. Like previously stated, I will soon be working on Health Potions, Proximity Sensors, AI, etc. and those will all fit really nice into this Event System. Part of the game will be infinite dungeon level generation and even something like GenerateNewDungeonFloorEvent will fit nicely into this.

Creating new APIs

While working on the Events, I started delving into compile time polymorphism using Templates. I’ve been avoiding using virtual functions because of the “behind-the-scenes” VMT generation and if I ever needed to maintain a vector of Abstract types, it would require pointers. I try really hard to to subscribe to Casey Muratori’s idea of “Performance Aware Programming” (not always, as you can see by my use of Hash Tables and Sets from the STL, something I have been trying to migrate away from), and that means avoiding the use of Virtual Methods. This has been fine until I started creating Events. I wanted to have a general idea of an event that conformed to the trigger(), process(), and flush() API without having to simply “remember”. I also have been shy about using templates just because of their sheer complexity. But I finally decided to dive right in and so far I have really enjoyed it. You can see my AttackEvent, but I also introduced this idea to my Components.

I have used explicit std::vector<std::optionals<>> throughout my EntityManager to maintain my components since I am using a Sparse ECS. Some entities will have a component, others won’t, and instead of checking .has_value() on them, used the flag system. This meant that a pattern emerged. Whenever I wanted to access a Component, I always used the value() call, I never had to check the has_value() call since that information was stored in the entity flag. This meant I could create a new API: the ComponentStorage.

template<typename ComponentType>
struct ComponentStorage
{
	std::vector<std::optional<ComponentType>> components;

	// Operator Overloading
	// If we are grabbing data at this point, we should have a value there
	ComponentType& operator[](int i)
	{
		assert(components[i].has_value());
		return components[i].value();
	}

	const ComponentType& operator[](int i) const
	{
		assert(components[i].has_value());
		return components[i].value();
	}

	void set(int i, ComponentType component)
	{
		assert(i >= 0 && i < components.size());
		components[i] = component;
	}

	void push_component(std::optional<ComponentType> component)
	{
		components.push_back(component);
	}

	std::optional<ComponentType>& get_component(entity_index i)
	{
		assert(i >= 0 && i < components.size());
		return components[i];
	}

    // This static method allows for a really cool feature mentioned below
	static ComponentStorage<ComponentType>& get_storage(EntityManager& em);
};

This has a couple of benefits. First, I can index using the [] operator since if I am ever using that operator, I know it will always have a value. And Second, I can do some really cool stuff with my entity manager:

typedef struct EntityManager {
	entity_index entity_count = 0;


	std::vector<flag>								flags;
	ComponentStorage<TransformComponent>			transform_components;
	ComponentStorage<SpriteComponent>				sprite_components;
	ComponentStorage<FrameAnimationComponent>		animation_components;
	ComponentStorage<SkeletalAnimationComponent>	skeletal_animation_components;
	ComponentStorage<HitBoxComponent>				hit_box_components;
	ComponentStorage<InputComponent>				input_components;
	ComponentStorage<CollisionComponent>			collision_components;
	ComponentStorage<PhysicsComponent>				physics_components;
	ComponentStorage<EquipmentComponent>			equipment_components;
	ComponentStorage<CollectableComponent>			collectable_components;

	template<typename ComponentType>
	ComponentStorage<ComponentType>& get_storage()
	{
		return ComponentStorage<ComponentType>::get_storage(*this);
	}

	template<typename... ComponentTypes>
	entity_index spawn_entity(flag flag, ComponentTypes... components)
	{
		entity_count++;
		entity_index index = entity_count - 1;

		flags.push_back(flag);
		transform_components.push_component(std::nullopt);
		sprite_components.push_component(std::nullopt);
		animation_components.push_component(std::nullopt);
		skeletal_animation_components.push_component(std::nullopt);
		hit_box_components.push_component(std::nullopt);
		input_components.push_component(std::nullopt);
		collision_components.push_component(std::nullopt);
		physics_components.push_component(std::nullopt);
		equipment_components.push_component(std::nullopt);
		collectable_components.push_component(std::nullopt);

		((get_storage<ComponentTypes>().components[index] = std::forward<ComponentTypes>(components)), ...);

		return index;
	}

	std::vector<entity_index> get_entity_indices_with_flag(flag reqd_tags);

} EntityManager;

The spawn_entity method uses variadic arguments of ComponentTypes and the very cool get_storage<...>() method to spawn a new entity. Since I am using Compile Time polymorphism, if my new ComponentStorage doesn’t conform to the get_storage<>() method, it will fail to compile.