Using events with Libgdx Ashley
From components to events. How I evolved my game development architecture with LibGDX Ashley by building a small event-driven solution on top of this entity-component-system.
My 2025 challenge is to develop and release a tiled-based single-player game using Java and LibGDX. In addition to building the game’s backend, I also plan to create all game’s artwork using pixel art.
Why did I choose LibGDX?
It is not a pure OpenGL library; Instead, it offers additional features like sprite-batches, tiled-maps support, orthographic camera and so on.
My first commit was on March 13th and it has been a greate learning experience since then. I began implementing everything on my own, and when I got stuck, I turned to the LibGDX Discord community for guidance.
Within the first week, I was able to get a tiled map up and running, a player and a monster. I implemented a couple of conditional statements to handle the keyboard input events and successfully integrated the pathfinding algorithm (A-star). I will discuss the pathfinding aspect in a separate post as it requires a more in-depth explanation detailing how I constructed the graph. In addition to the technical aspects, I was able to create some tilesets and characters. The result looks great, doesn’t it?
Ashley, an Entity-Component-System
Following the first week of coding, I realized that scaling the game would become a challenge. When I say scaling, I mean adding new features, new layers, questing, spells and more. Because the system I was implementing was not flexible enough! Every time I added a new feature I broke something, either the rendering or the rules processing.
I explored solutions to this issue and discovered Ashley.
In short, Ashley is an entity-component-system (ECS). In the context of game development, it allows game entities to be extended through the addition of components. Let me start by explain the basic architecture. Ashley consists of three main components: EntitySystem, Entity and Component.
A EntitySystem processes Entities and is tied to the Engine’s lifecycle. An Entity represents a game object, while a component is a collection of attributes that extend an Entity’s functionality, such as HealthComponent, SpawnComponent or MovementComponent.
I decided to refactored the entire project resulting in the removal of 2500 lines of code and introduced a few new classes such as Actors (Player, Monster and NPC), MapSystem (for rendering the Map), RenderingSystem (for rendering the sprites), InputSystem (for handling the keyboard input events), MovementSystem (for handling the actors movement) and so on.
Using Ashley, I established a robust structure for my game my game. Now, I have clear understanding of Actors, EntitySystems and Components. I created EntitySystems that run in every engine update loop, ones that only process certain types of Components such has HealthComponents and others that process all entities like MovementSystem.
This week, I began implementing the Spawn feature. In short, when a monster dies, it should respawn after a delayed defined by spawnSpeed. I created an initial implementation using SpawnComponent and deltaTimes to track the time until spawn, but soon realized that this approach wouldn’t scale. I mean, Ashley is not the best fit for certain features.
Spawn Feature
Ashley EntitySystems run every engine update, so if there are 60 frames per second, I think the game update will run 60 times (not sure, but I suspect). Consider a scenario with 1000 monsters and solely for the spawn feature, I would need to check all 1000 monsters 60 times per second.
I think that whenever a monster dies, there should be an event of Death and followed by a spawn event. In this scenario, events make more sense to me than processing components.
As a result, I conducted some research and soon realized that Ashley lacks a built-in event-driven solution. Therefore, I decided to create a Multicast Event Dispatcher and a Event Listeners. This may seem complex but it only required implementing a couple of classes with a few lines of code.
public class MulticastEventDispatcher extends EntitySystem {
private static final MulticastEventDispatcher instance = new MulticastEventDispatcher();
List<EventListener> listenerList = new ArrayList<>();
Queue<Event> eventQueue = new Queue<>();
Queue<Event> delayedQueue = new Queue<>();
private MulticastEventDispatcher() {
}
public static MulticastEventDispatcher getInstance() {
return instance;
}
public void sendEvent(Event event) {
this.eventQueue.addLast(event);
}
public void delayEvent(Event event) {
this.delayedQueue.addLast(event);
}
public void addListener(EventListener listener) {
listenerList.add(listener);
}
private void dispatchDelayQueue() {
long currentTime = TimeUtils.nanoTime();
Queue<Event> keepDelayedQueue = new Queue<>();
while (delayedQueue.notEmpty()) {
Event event = delayedQueue.removeFirst();
if (currentTime - event.createdAt() >= event.getDelayTime()) {
for (EventListener eventListener : listenerList) {
eventListener.consume(event);
}
} else {
keepDelayedQueue.addLast(event);
}
}
delayedQueue.clear();
keepDelayedQueue.forEach(delayedQueue::addLast);
}
private void dispatch() {
while (eventQueue.notEmpty()) {
Event event = eventQueue.removeFirst();
for (EventListener eventListener : listenerList) {
eventListener.consume(event);
}
}
}
@Override
public void update(float deltaTime) {
dispatch();
dispatchDelayQueue();
}
}
The MulticastEventDispatcher class extends the EntitySystem which runs the dispatcher with every engine update (as mentioned before). It features two queues, a normal queue and a delayed queue. On each execution, it checks the size of both queues and sends events to all registered listeners.
The key difference between the two queues lies in the delay queue’s behaviour, where it checks whether an event should be sent by comparing the current time with the event’s designated dispatch time.
On the Listener side, it checks whether the received event is of the correct class. If so, it processes the event. For example, the SpawnListener exclusively processes SpawnEvents.
public class SpawnEventListener implements EventListener {
@Override
public void consume(Event event) {
long currentTime = TimeUtils.nanoTime();
if (!(event instanceof SpawnEvent)) return;
This event-driven solution has room for improvement by incorporating polling instead of pushing events. Or by continuing to push events and only sending them to the relevant listeners by mapping listeners to specific event classes.
Please note that the current event-driven solution is built on top of Ashley. The dispatcher extends the EntitySystem.
It has been a fun journey for me, I am relearning OpenGL and finding immense joy in this project. Looking ahead, my plan for the next few days or weeks is: to implement the questing and/or the spelling systems. I am undecided on which one to tackle first but I believe both will benefit from this event-driven approach.