-module(ecsalt_component).
-moduledoc "Put, remove, and query named data on entities.".
-include("ecsalt.hrl").
-export([put/3, remove/3, update/4, match/2, fold/4, foreach/3]).
-doc """
Associate a component defined as the tuple {key,value} with an entity. If the
component already exists, it will be replaced with the new value. Returns the
opaque world() type.
""".
-spec put([{term(), term()}], id(), world()) -> world().
put(Components, EntityID, World) ->
F =
fun({Name, Data}) ->
put_one(Name, Data, EntityID, World)
end,
lists:foreach(F, Components),
World.
-doc """
Remove a component from a given entity. This function will always succeed even
if the component does not exist.
""".
-spec remove([term()], id(), world()) -> world().
remove(Components, EntityID, World) ->
F =
fun(Component) ->
remove_one(Component, EntityID, World)
end,
ok = lists:foreach(F, Components),
World.
-doc """
Update a component's value by applying a function to it. The component
must already exist on the entity.
""".
-spec update(term(), fun((term()) -> term()), id(), world()) -> world().
update(Name, Fun, EntityID, World) ->
#world{entities = E} = World,
[{EntityID, ComponentList}] = ets:lookup(E, EntityID),
{Name, OldData} = lists:keyfind(Name, 1, ComponentList),
put_one(Name, Fun(OldData), EntityID, World),
World.
-doc """
Return all entities matching a given set of components. Returns empty list if
there are no matches.
""".
-spec match([term()], world()) -> [entity()].
match(ComponentList, World) ->
% Multi-match. Try to match several components and return the common
% elements. Use sets v2 introduced in OTP 24
Sets = [
sets:from_list(match_one(X, World), [{version, 2}])
|| X <- ComponentList
],
sets:to_list(sets:intersection(Sets)).
-doc """
Fold a function over all entities matching a given set of components,
accumulating a result.
""".
-spec fold([term()], fun((id(), [component()], Acc) -> Acc), Acc, world()) -> Acc.
fold(ComponentList, Fun, Acc0, World) ->
Entities = match(ComponentList, World),
lists:foldl(fun({ID, Components}, Acc) -> Fun(ID, Components, Acc) end, Acc0, Entities).
-doc """
For each entity with the specified Component, apply fun(EntityID, Values).
""".
-spec foreach([term()], fun((id(), [component()]) -> any()), world()) -> ok.
foreach(ComponentList, Fun, World) ->
Entities = match(ComponentList, World),
lists:foreach(fun({ID, Components}) -> Fun(ID, Components) end, Entities).
% Internal API
put_one(Name, Data, EntityID, World) ->
#world{entities = E, components = C} = World,
Components =
case ets:lookup(E, EntityID) of
[] ->
% No components
[{Name, Data}];
[{EntityID, ComponentList}] ->
lists:keystore(Name, 1, ComponentList, {Name, Data})
end,
true = ets:insert(E, {EntityID, Components}),
true = ets:insert(C, {Name, EntityID}),
ok.
remove_one(Name, EntityID, World) ->
#world{entities = E, components = C} = World,
% Remove the data from the entity
case ets:lookup_element(E, EntityID, 2) of
[] ->
ok;
ComponentList ->
% Delete the key-value identified by ComponentName
ComponentList1 = lists:keydelete(
Name, 1, ComponentList
),
% Update the entity table
ets:insert(E, {EntityID, ComponentList1})
end,
% Remove the data from the component bag
true = ets:delete_object(C, {Name, EntityID}),
ok.
match_one(ComponentName, World) ->
% From the component bag table, get all matches
#world{entities = E, components = C} = World,
Matches = ets:lookup(C, ComponentName),
% Use the entity IDs from the lookup in the component table to
% generate a list of IDs for which to return data to the caller
IDs = [ets:lookup(E, EntityID) || {_, EntityID} <- Matches],
lists:usort(lists:flatten(IDs)).