Packages

Multiplayer game server framework

Current section

Files

Jump to
overworld src ow_ecs.erl
Raw

src/ow_ecs.erl

-module(ow_ecs).
-behaviour(gen_server).
-define(SERVER(World),
{via, gproc, {n, l, {?MODULE, World}}}
).
%% API
-export([start/1, start_link/1, 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
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Types and Records
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-record(world, {
name :: term(),
systems = [] :: [{term(), system()}],
entities :: ets:tid(),
components :: ets:tid()
}).
-type 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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
start(World) ->
gen_server:start(?SERVER(World), ?MODULE, [World], []).
start_link(World) ->
gen_server:start_link(?SERVER(World), ?MODULE, [World], []).
stop(World) ->
gen_server:stop(?SERVER(World)).
% 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 try_component(term(), id(), world()) -> {ok, [term()]} | false.
try_component(ComponentName, EntityID, World) ->
% @doc If EntityID has the specified ComponentName, return all components
gen_server:call(
?SERVER(World), {try_component, ComponentName, EntityID}
).
-spec match_component(term(), world()) -> [entity()].
match_component(ComponentName, World) ->
gen_server:call(?SERVER(World), {match_component, ComponentName}).
-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 new_entity(id(), world()) -> ok.
new_entity(EntityID, World) ->
gen_server:call(?SERVER(World), {new_entity, EntityID}).
-spec rm_entity(id(), world()) -> ok.
rm_entity(EntityID, World) ->
gen_server:call(?SERVER(World), {rm_entity, EntityID}).
-spec entity(id(), world()) -> {id(), [term()]} | false.
entity(EntityID, World) ->
gen_server:call(?SERVER(World), {entity, EntityID}).
-spec entities(world()) -> [{id(), [term()]}].
entities(World) ->
gen_server:call(?SERVER(World), entities).
-spec add_component(term(), term(), id(), world()) -> ok.
add_component(Name, Data, EntityID, World) ->
gen_server:call(?SERVER(World), {add_component, Name, Data, EntityID}).
% TODO: ok -> true?
-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).
-spec del_component(term(), id(), world()) -> ok.
del_component(Name, EntityID, World) ->
gen_server:call(?SERVER(World), {del_component, Name, EntityID}).
% TODO: ok -> true?
-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).
-spec add_system(system(), world()) -> ok.
add_system(System, World) ->
add_system(System, 100, World).
-spec add_system(system(), integer(), world()) -> ok.
add_system(System, Priority, World) ->
gen_server:call(?SERVER(World), {add_system, System, Priority}).
-spec del_system(system(), world()) -> ok.
del_system(System, World) ->
gen_server:call(?SERVER(World), {del_system, System}).
-spec proc(world()) -> any().
proc(World) ->
proc(World, []).
-spec proc(world(), any()) -> [any()].
proc(World, Data) ->
Systems = gen_server:call(?SERVER(World), systems),
Fun = fun({_Prio, Sys}, Acc) ->
Result =
case Sys of
{M, F, 1} ->
erlang:apply(M, F, [World]);
{M, F, 2} ->
erlang:apply(M, F, [World, Data]);
Fun2 ->
Fun2(World, Data)
end,
[{Sys, Result} | Acc]
end,
lists:foldl(Fun, [], Systems).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% gen_server callbacks
%%%%%%%/%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
init([WorldName]) ->
World = #world{
name = WorldName,
systems = [],
entities = ets:new(entities, [set, public]),
components = ets:new(components, [bag, public])
},
{ok, World}.
handle_call(
{add_component, ComponentName, ComponentData, EntityID}, _From, State
) ->
#world{entities = E, components = C} = State,
% 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
[{ComponentName, ComponentData}];
[{EntityID, ComponentList}] ->
% Check if the component already exists
case lists:keytake(ComponentName, 1, ComponentList) of
{value, _Tuple, ComponentList2} ->
% Throw away the old data and add the new data
[{ComponentName, ComponentData} | ComponentList2];
false ->
[{ComponentName, ComponentData} | 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, {ComponentName, EntityID}),
{reply, ok, State};
handle_call({del_component, ComponentName, EntityID}, _From, State) ->
#world{entities = E, components = C} = State,
% Remove the data from the entity
case ets:member(E, EntityID) of
true ->
case ets:lookup_element(E, EntityID, 2) of
[] ->
ok;
ComponentList ->
% Delete the key-value identified by ComponentName
ComponentList1 = lists:keydelete(
ComponentName, 1, ComponentList
),
% Update the entity table
ets:insert(E, {EntityID, ComponentList1})
end;
false ->
ok
end,
% Remove the data from the component bag
ets:delete_object(C, {ComponentName, EntityID}),
{reply, ok, State};
handle_call({try_component, ComponentName, EntityID}, _From, State) ->
#world{entities = E, components = C} = State,
Resp =
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,
{reply, Resp, State};
handle_call({match_component, ComponentName}, _From, State) ->
% From the component bag table, get all matches
#world{entities = E, components = C} = State,
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],
Resp = lists:flatten(IDs),
{reply, Resp, State};
handle_call({new_entity, EntityID}, _From, State) ->
#world{entities = E} = State,
Resp =
case ets:lookup(E, EntityID) of
[] ->
% ok, add 'em
ets:insert(E, {EntityID, []});
_Entity ->
ok
end,
{reply, Resp, State};
handle_call({rm_entity, EntityID}, _From, State) ->
#world{entities = E, components = C} = State,
Resp =
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,
{reply, Resp, State};
handle_call({entity, EntityID}, _From, State) ->
#world{entities = E} = State,
Resp =
case ets:lookup(E, EntityID) of
[] ->
false;
[Entity] ->
Entity
end,
{reply, Resp, State};
handle_call(entities, _From, State) ->
#world{entities = E} = State,
ets:match_object(E, {'$0', '$1'});
handle_call(systems, _From, State) ->
#world{systems = S} = State,
{reply, S, State};
handle_call({add_system, Callback, Prio}, _From, State) ->
#world{systems = S} = State,
S0 =
case lists:keytake(Callback, 2, S) of
false ->
S;
{value, _Tuple, SRest} ->
% Replace the current value instead
SRest
end,
S1 = lists:keysort(1, [{Prio, Callback} | S0]),
Reply = {ok, Prio},
{reply, Reply, State#world{systems = S1}};
handle_call({del_system, Callback}, _From, State = #world{systems = S}) ->
S1 = lists:keydelete(Callback, 2, S),
{reply, ok, State#world{systems = S1}}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% internal functions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%