Packages

Multiplayer game server framework

Current section

Files

Jump to
overworld src ow_ecs2.erl
Raw

src/ow_ecs2.erl

-module(ow_ecs2).
%% API
-export([start/0, stop/1]).
-export([
new_entity/2,
rm_entity/2,
entity/2,
entities/1,
add_component/4,
add_components/3,
del_component/3,
del_components/3,
try_component/3,
match_component/2,
match_components/2,
foreach_component/3,
add_system/3,
add_system/2,
del_system/2,
proc/1,
proc/2,
to_map/1,
get/3,
get/2,
take/3,
take/2
]).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Types and Records
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-record(world, {
systems = [] :: [{term(), system()}],
entities :: ets:tid(),
components :: ets:tid()
}).
-opaque world() :: #world{}.
-export_type([world/0]).
-type component() :: {term(), term()}.
-type entity() :: {term(), [component()]}.
-export_type([entity/0]).
-type system() :: {mfa() | fun()}.
-type id() :: integer().
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% API
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec start() -> world().
start() ->
#world{
systems = [],
entities = ets:new(entities, [set, public]),
components = ets:new(components, [bag, public])
}.
-spec stop(world()) -> ok.
stop(#world{entities = ETable, components = CTable}) ->
ets:delete(ETable),
ets:delete(CTable),
ok.
% This can potentially create an unbounded number of atoms! Careful!
-spec to_map(entity()) -> map().
to_map({EntityID, Components}) ->
% Create the component map
EMap = maps:from_list(Components),
% Add the ID
EMap#{id => EntityID}.
-spec get(term(), [component()]) -> term().
get(Component, ComponentList) ->
get(Component, ComponentList, false).
-spec get(term(), [component()], term()) -> term().
get(Component, ComponentList, Default) ->
case lists:keyfind(Component, 1, ComponentList) of
{_Component, Data} ->
Data;
false ->
Default
end.
-spec take(term(), [component()]) -> term() | false.
take(Component, ComponentList) ->
take(Component, ComponentList, false).
-spec take(term(), [component()], term()) -> {term(), [term()]} | term().
take(Component, ComponentList, Default) ->
case lists:keytake(Component, 1, ComponentList) of
{value, {_Component, Data}, Rest} ->
{Data, Rest};
false ->
Default
end.
-spec new_entity(id(), world()) -> ok.
new_entity(EntityID, World) ->
#world{entities = E} = World,
case ets:lookup(E, EntityID) of
[] ->
% ok, add 'em
ets:insert(E, {EntityID, []});
_Entity ->
ok
end.
-spec rm_entity(id(), world()) -> ok.
rm_entity(EntityID, World) ->
#world{entities = E, components = C} = World,
case ets:lookup(E, EntityID) of
[] ->
% ok, nothing to do
ok;
[{EntityID, Components}] ->
% Remove the entity from the entity table
ets:delete(E, EntityID),
% Delete all instances of it from the component table as well
[
ets:delete_object(C, {N, EntityID})
|| {N, _} <- Components
],
ok
end.
-spec entity(id(), world()) -> {id(), [term()]} | false.
entity(EntityID, World) ->
#world{entities = E} = World,
case ets:lookup(E, EntityID) of
[] ->
false;
[Entity] ->
Entity
end.
-spec entities(world()) -> [{id(), [term()]}].
entities(World) ->
#world{entities = E} = World,
ets:match_object(E, {'$0', '$1'}).
-spec try_component(term(), id(), world()) -> {ok, [term()]} | false.
try_component(ComponentName, EntityID, World) ->
#world{entities = E, components = C} = World,
case ets:match_object(C, {ComponentName, EntityID}) of
[] ->
false;
_Match ->
% It exists in the component table, so return the Entity data
% back to the caller
[{EntityID, Data}] = ets:lookup(E, EntityID),
{ok, Data}
end.
-spec match_component(term(), world()) -> [entity()].
match_component(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)).
-spec match_components([term()], world()) -> [entity()].
match_components(List, 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_component(X, World), [{version, 2}])
|| X <- List
],
sets:to_list(sets:intersection(Sets)).
-spec foreach_component(fun(), term(), world()) -> ok.
foreach_component(Fun, Component, World) ->
Entities = match_component(Component, World),
F =
fun({ID, EntityComponents}) ->
Values = get(Component, EntityComponents),
Fun(ID, Values)
end,
lists:foreach(F, Entities).
-spec add_component(term(), term(), id(), world()) -> ok.
add_component(Name, Data, EntityID, World) ->
#world{entities = E, components = C} = World,
% On the entity table, we want to get the entity by key and insert a new
% version with the data
Components =
case ets:lookup(E, EntityID) of
[] ->
% No components
[{Name, Data}];
% BEHAVIOUR CHANGE
[{EntityID, ComponentList}] ->
case lists:keytake(Name, 1, ComponentList) of
{value, _Tuple, ComponentList2} ->
% Throw away the old data and add the new data
[{Name, Data} | ComponentList2];
false ->
[{Name, Data} | ComponentList]
end
end,
% Insert the new entity and component list
ets:insert(E, {EntityID, Components}),
% Insert the entity EntityID into the component table
ets:insert(C, {Name, EntityID}),
ok.
-spec add_components([{term(), term()}], id(), world()) -> ok.
add_components(Components, EntityID, World) ->
F =
fun({Component, Data}) ->
add_component(Component, Data, EntityID, World)
end,
lists:foreach(F, Components),
ok.
-spec del_component(term(), id(), world()) -> ok.
del_component(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
ets:delete_object(C, {Name, EntityID}),
ok.
-spec del_components([term()], id(), world()) -> ok.
del_components(Components, EntityID, World) ->
F =
fun(Component) ->
del_component(Component, EntityID, World)
end,
lists:foreach(F, Components),
ok.
-spec add_system(system(), world()) -> {ok, world()}.
add_system(Callback, World) ->
add_system(Callback, 100, World).
-spec add_system(system(), integer(), world()) -> {ok, world()}.
add_system(Callback, Priority, World) ->
#world{systems = S} = World,
S0 =
case lists:keytake(Callback, 2, S) of
false ->
S;
{value, _Tuple, SRest} ->
% Replace the current value instead
SRest
end,
S1 = lists:keysort(1, [{Priority, Callback} | S0]),
{ok, World#world{systems = S1}}.
-spec del_system(system(), world()) -> {ok, world()}.
del_system(Callback, World) ->
#world{systems = S} = World,
S1 = lists:keydelete(Callback, 2, S),
{ok, World#world{systems = S1}}.
-spec proc(world()) -> any().
proc(World) ->
proc([], World).
-spec proc(any(), world()) -> [any()].
proc(Data, World) ->
#world{systems = Systems} = World,
Fun = fun({_Prio, Sys}, Acc) ->
Result =
case Sys of
{M, F, 1} ->
erlang:apply(M, F, [World]);
{M, F, 2} ->
erlang:apply(M, F, [Data, World]);
Fun2 ->
Fun2(Data, World)
end,
[{Sys, Result} | Acc]
end,
lists:foldl(Fun, [], Systems).