Erlang, Entity System, gen_event. Oh my!
Last night, I was reading about Erlang’s gen_event and thought to myself: ‘Hmm, this is interesting. It looks like a good candidate for an entity system relying on message passing’. So I sat down, read more about it, and coded up some components, and I was pretty happy about the results.
This is a really big subject that I would suggest google it for more info. Basically, this is a get away from traditional OOP approach in game development, which is quite messy when you get lots of different types of entity. The Entity System breaks down game object’s (or entity) data and behaviors into components and/or subsystems and the entity is just a container for those components. That way you can mix and max different components to have all kind of interesting entities.
Note: I’m an Erlang beginner, so I’m sure I will be wrong, please correct me!
gen_event is a behavior in Erlang, where you will have a process that acts like an event manager. This event manager can have multiple event handlers (which is callback modules) attached to it, and can notify any event to all its handlers. The original example is the error_logger where, on error event, can have handlers to output to console, or to file, etc…
Why gen_event and Entity System?
One of the major specification for Entity System is how to handle communication between entity and its components, or between the components themselves. There are different ways to do it, such as one component keeping reference to other components, or by message passing.
Erlang already provides efficient message passing. gen_event behavior allows us to attach a set of handlers to specific event manager. To the Entity System, we could think of each entity as an event manager process, and each component as an event handler. Each component will handle a specific state of the entity (such as HP, position). Events from the world will let entity notify its components to modify the state of the entity.
Again, I may be wrong, but let’s experiment!
Because of the way Erlang works, I cannot have each module as an entity. I need name for each. So instead I created a module to act as an entity manager that will spawns entity (which is an event manager, identified by its Pid). There are some abstract functions to handle gen_event functionality.
Nothing special here, just the basic of gen_event.
Let’s test it out by creating a component for health, called health_component:
-module (health_component). -behaviour(gen_event). -export([init/1, handle_event/2, terminate/2]).
I specified the behavior of this component as gen_event, and export the required functions.
The first function is init/1, which is executed when gen_event:add_handler(Entity, health_component, Args) is called. The Args is passed through. This function must return a tuple {ok, State}.
So in here, the only state I need for this health component is the HP of the entity. You can specify the amount of HP the entity has by passing it as Args.
The next required function is handle_event/2, which is executed when gen_event:notify(Entity, Event) is called. Event can be anything, and it will be passed to component’s handle_event/2 as the first argument.
handle_event({hit, Damage}, HP) when Damage =< HP -> NewHP = HP - Damage, io:format("Entity got hit with ~p damage and has ~p HP left.~n", [Damage, NewHP]), {ok, NewHP};
The interesting bit in here is that the State of the component is handled automatically, which is not mentioned clearly on Erlang documentation (or I might have skipped it). That way you don’t have to keep how many HP the entity currently has. Again, this is probably obvious for Erlang programmers, but bear with me.
Anyway, I expect an event in format {hit, Damage} when the entity got hit and it still has enough HP. Like I said before, the HP in the arguments reflects the current HP state, and you don’t have to pass it manually.
To make thing more interesting, I have some pattern matching for some more events for this component:
handle_event({hit, _Damage}, 0) -> io:format("Entity is dead already -.-.~n"), {ok, 0}; handle_event({hit, Damage}, _HP) -> io:format("Entity took ~p damage and died.~n", [Damage]), {ok, 0};
And lastly, since all gen_event handlers for a specific event manager will receive the same event, so we need a last clause to ignore all other events.
% The failsafe callback for other events that this component does not care % handle_event(_, State) -> {ok, State}.
The terminate/2 is called when the component is removed from the entity. We don’t do anything special here.
terminate(_Args, _State) -> ok.
I created another component to see if I could expand it. This component handles 2D position for the entity:
A really cool thing with Erlang is hot swapping. With the newly created component, I would just have to compile it, then continue to add the component to the existing entity (without stopping it) and it would just work. Awesome!
Just a quick note, if you don’t have a catch-all handle_event clause in your component, it will crash when other event is sent to it.
I am playing with this more to see how this would work with a full Entity System.