ECSalt - Entity Component System for Erlang
ECSalt (pronounced Exalt) is an Entity-Component-System-like library for Erlang applications.
Installing
You can now find ECSalt on hex.pm! https://hex.pm/packages/ecsalt
Simply add it to your dependencies list to add it to your project:
{deps, [
ecsalt %, your other deps here..
]}.How to use
To demonstate how to use ECSalt, we'll go through adding a system that makes a monster take damage if they are on fire.
Creating the ECS World
First start a new ECSalt "world". Worlds are collections of entities, components, and systems. You may start any number of ECSalt worlds, up to the half of the maximum number of ETS tables in your Erlang runtime.
1> World = ecsalt:new().
{world,[],#Ref<0.3056694120.667287557.234440>,
#Ref<0.3056694120.667287557.234441>}Dynamically create entities with attached components
Suppose we have a fireplace and it has the burning state. The entity ID is an arbitrary reference, We represent the Fireplace with a unique reference:
2> Fireplace = make_ref().Then we put/3 it into the ECSalt world. Note that functions updating the world will always return a world() record.
3> ecsalt_component:put([{burning, true}], Fireplace, World).
{world,[],#Ref<0.3056694120.667287557.234440>,
#Ref<0.3056694120.667287557.234441>}Now let's imagine a goblin-cat snuggles a bit too close to the fireplace and starts smoldering:
4> GoblinCat = make_ref().
5> ecsalt_component:put([{burning, true}], GoblinCat, World).
{world,[],#Ref<0.3056694120.667287557.234440>,
#Ref<0.3056694120.667287557.234441>}The put/3 function takes a list of components, so we can add several components at once.
6> ecsalt_component:put([{hp, 35}, {color, green}, {brain_cells, 1}], GoblinCat, World).
{world,[],#Ref<0.3056694120.667287557.234440>,
#Ref<0.3056694120.667287557.234441>}Any component put/3 into the ECS will overwrite previous components for the same entity.
Matching on component lists
Now suppose want to check for all entities that are on fire and have some
health points (HP). We can use the match/2 function in the component module
to only return the functions that match all required components. For example,
our radiant goblin-cat matches here, but the fireplace does not because it
doesn't have HP:
7> ecsalt_component:match([hp, burning], World).
[{#Ref<0.3056694120.667156485.234478>,
[{hp,35},{color,green},{brain_cells,1},{burning,true}]}]Registering systems
We can also define systems that will act on collections of components. Systems
must be one of: fun with arity of 2, a mfa() tuple of the form {Module,
Fun, 2}. Suppose we have a system that checks if the cat is on fire and updates
their HP accordingly. We define a fun that reports the critter's status, and
wrap that in another fun that matches the required callback for an ECSalt
system.
8> Report = fun({ID, Components}) ->
HP = proplists:get_value(hp, Components),
case HP of
Value when Value =< 0 ->
io:format("Kitty is cooked!~n");
_ ->
io:format("The goblin-cat smolders cluelessly...~n")
end
end.
#Fun<erl_eval.41.39164016>
9> System =
fun(_Data, World) ->
Matches = ecsalt_component:match([hp, burning], World),
lists:foreach(Report, Matches)
end.
#Fun<erl_eval.42.130099583>The system should now be registered with ECSalt:
10> World1 = ecsalt_system:register(System, World)
{world,[{0,#Fun<erl_eval.41.130099583>}],
#Ref<0.3056694120.667287557.234440>,
#Ref<0.3056694120.667287557.234441>}Note that we have to update the World binding here. You should always treat World as an opaque object, but when adding/removing systems you must do so.
Activating systems
You can trigger the system whenever you like via proc/1 (short for process, a term borrowed from multi-user dungeons). We pass an empty list as extra data -- none of our systems use it.
12> ecsalt:proc([], World1).
The goblin-cat cluelessly smolders...
[{#Fun<erl_eval.41.130099583>,ok}]Using the foreach construction
Our goblin-cat is smoldering away, but nothing changes the state of the
critter. We want to reduce the HP of any burning creatures every time the
system triggers (i.e., procs). This time we can use the foreach/3 function to
simplify things a bit:
BurnSystem =
fun(_Data, World) ->
ecsalt_component:foreach([hp, burning],
fun(ID, _Components) ->
ecsalt_component:update(hp, fun(HP) -> HP - 10 end, ID, World),
io:format("Sizzle.. hiss.. crackle..~n")
end,
World)
end,
ecsalt_system:register(BurnSystem, World).Demo
If we run the proc again, representing a game, tick, we see the clueless
critter losing HP and, finally, becoming easy dinner:
8> ecsalt:proc([], World2).
Sizzle.. hiss.. crackle..
The goblin-cat cluelessly smolders...
[akao{#Fun<erl_eval.41.130099583>,ok},
{#Fun<erl_eval.41.130099583>,ok}]
9> ecsalt:proc([], World2).
Sizzle.. hiss.. crackle..
The goblin-cat cluelessly smolders...
[{#Fun<erl_eval.41.130099583>,ok},
{#Fun<erl_eval.41.130099583>,ok}]
10> ecsalt:proc([], World2).
Sizzle.. hiss.. crackle..
The goblin-cat cluelessly smolders...
[{#Fun<erl_eval.41.130099583>,ok},
{#Fun<erl_eval.41.130099583>,ok}]
11> ecsalt:proc([], World2).
Sizzle.. hiss.. crackle..
Kitty is cooked!
[{#Fun<erl_eval.41.130099583>,ok},
{#Fun<erl_eval.41.130099583>,ok}]