Skip to main content

src/ecsalt_component.erl

-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)).