previous article, we discussed the basics of the Entity-Component-System architecture, but ran into a bit of a pickle in the end. If systems are run every frame, how do we design systems that only do stuff occasionally? How do we have something happen only in response to a certain stimuli (i.e. a player walks up and presses E on a sandwich)? This is where we must talk about Events.
Events are not exclusive to ECS, but they are (in my opinion) crucial to efficient ECS design. We can describe much more specific interactions between entities, and designate certain functions to only run when it would make sense. Some use cases for Events:
All of this functionality that would be difficult or inefficient to capture within an update loop can now be written as a response to specific Events. They are usually raised on a specific entity, and handlers can usually specify a subset of components that that entity must have to run for it. You would only want to run the "try to eat food" event on a Food
entity!
We'll write some events and event handlers to describe trying to eat food. Suppose we have the following components:
class Food {
String flavor = "savory";
}
/// If a `Food` entity has this, then it's rotten and you can't eat it
class Rotten {}
class Stomach {
bool full = false;
}
We will also define the following events with the given attributes:
class InteractEvent {
/// The entity that is interacting with us.
Entity source;
/// If set to true, we can exit the handler early, as another
/// handler has already used this event.
///
/// i.e. If you clicked on a button in the pause menu,
/// you wouldn't want your character to fire their weapon!
bool handled = false;
}
Now we can write an event handler for InteractEvent
that will run when an entity with the Food
component is interacted with. In this, we will check that the interacting entity has space in their Stomach
, and they will eat the food if they do.
void TryEatFood(Entity foodEntity, Food foodComp, ref InteractEvent args) {
// Some other event handler on this entity already used this event
if(args.handled) return;
// Try to get the Stomach component of the interacting entity
// and store it in the "stomach" variable.
// Exit the handler if the interacting entity has no stomach.
if(!TryGetComponent<Stomach>(args.source, out var stomach))
return;
// We'll try to eat, so handle the event
args.handled = true;
// Can't eat on a full stomach
if(stomach.full) {
ShowMessage("You're already full!");
return;
}
// Delete the food entity and fill the
// interacting entity's stomach.
ShowMessage($"Mmmm, tastes {foodComp.flavor}.");
Delete(foodEntity);
stomach.full = true;
}
Now, what if we wanted to give food the ability to spoil? We can add an additional line within our handler to check for this:
if(HasComponent<Rotten>(foodEntity)) {
ShowMessage("Eww! This food is rotten!");
return;
}
However, this is a bad pattern to follow. We would need to add more and more statements for every new component and system that could change how we eat food. What if the sandwich isn't rotten, but it has cheese and our character is lactose intolerant? What if the other people at the table haven't gotten their food yet, and our character wants to be polite?
If we want to make our system general and allow for new mechanics to easily interact with it in the future, we can raise another event to check that we can eat the food. We'll define that event like so:
class CanEatEvent {
Entity foodEntity;
Food foodComp;
// If nobody prevents us from eating, we will eat
// `foodEntity` and delete it.
bool canEat = true;
}
Now, we can redesign the handler to raise this CanEatEvent
:
void TryEatFood(Entity foodEntity, Food foodComp, ref InteractEvent args) {
// Some other event handler on this entity already used this event
if(args.handled)
return;
// Try to get the Stomach component of the interacting entity
// and store it in the "stomach" variable.
// Exit the handler if the interacting entity has no stomach.
if(!TryGetComponent<Stomach>(args.source, out var stomach))
return;
// We'll try to eat, so handle the event
args.handled = true;
// Can't eat on a full stomach
if(stomach.full) {
ShowMessage("You're already full!");
return;
}
// Raise the CanEatEvent on the interacting
// user to check if any event handlers will
// prevent it from eating
var eatCheck = new CanEatEvent(foodEntity, foodComp);
RaiseEvent(args.source, eatCheck);
// Can we eat?
if(!eatCheck.canEat)
return;
// Delete the food entity and fill the
// interacting entity's stomach.
ShowMessage($"Mmmm, tastes {foodComp.flavor}.");
Delete(foodEntity);
stomach.full = true;
}
Now, we can catch this event in a separate handler that specifically handles when the food is Rotten
:
void DontEatRottenFood(Entity eater, ref CanEatEvent args) {
// We already can't eat it anyways
if(!args.canEat)
return;
// Cancel the eat attempt if the food is rotten
if(HasComponent<Rotten>(foodEntity)) {
ShowMessage("Eww! This food is rotten!");
args.canEat = false;
return;
}
}
Now, the food-eating handler doesn't need to know anything about the Rotten
component, and we can use this CanEatEvent
for any number of scenarios in which we wouldn't want to eat something!
As you can see, Events can be used to describe and handle numerous situations that would be less efficient or clunky to check every frame. They can also be used to modify how we perform certain actions, which can be used to prevent us from eating a rotten sandwich, or make busting open a door faster if we have a crowbar equipped. If you want to see a more concrete example of ECS design, you should read my follow-up blog series about ECS programming in Space Station 14!