Packages

An Entity Component System (ECS) for Elixir focused on ease of use and ergonomics.

Current section

Files

Jump to
genesis lib genesis world.ex
Raw

lib/genesis/world.ex

defmodule Genesis.World do
use GenServer
alias __MODULE__
alias Genesis.Object
alias Genesis.Aspect
alias Genesis.Utils.ETS
require Logger
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, World)
GenServer.start_link(World, %{}, name: name)
end
def list_aspects() do
GenServer.call(World, :list_aspects)
end
def register_aspect(module) do
# We want the server to block registration calls to ensure
# that the table is created before other process tries to access it.
GenServer.call(World, {:register_aspect, module})
end
def send(%Object{} = object, {op, aspect}) when op in [:added, :removed] do
# When sending an event to an object about the creation or removal of an aspect,
# we also block to ensure that the respective ETS tables get updated for further consumption.
GenServer.call(World, {op, object, aspect})
end
def send(%Object{} = object, message) do
GenServer.cast(World, {:event, object, message})
end
@impl true
def init(args) do
ETS.init(World)
state = Map.merge(args, %{aspects: []})
{:ok, state, {:continue, :setup}}
end
@impl true
def terminate(reason, _state) do
ETS.drop(World)
aspects = list_aspects()
Enum.each(aspects, &ETS.drop/1)
Logger.info("Terminating World: #{inspect(reason)}")
end
@impl true
def handle_continue(:setup, state) do
# TODO: Load entities, etc.
{:noreply, state}
end
@impl true
def handle_call(:list_aspects, _from, state) do
{:reply, Enum.reverse(state.aspects), state}
end
@impl true
def handle_call({:register_aspect, module}, _from, state) do
if not is_aspect?(module), do: raise("Invalid aspect #{inspect(module)}")
{:reply, :ok, Map.update!(state, :aspects, &[module.init() | &1])}
end
@impl true
def handle_call({:added, object, aspect}, _from, state) do
ETS.update(World, object.id, [aspect], &[aspect | &1])
{:reply, :ok, state}
end
@impl true
def handle_call({:removed, object, aspect}, _from, state) do
equals? = fn %{__struct__: s1}, %{__struct__: s2} -> s1 == s2 end
ETS.update!(World, object.id, fn aspects ->
Enum.reject(aspects, &equals?.(&1, aspect))
end)
{:reply, :ok, state}
end
@impl true
def handle_cast({:event, object, {event, _args}}, state) do
Logger.info("Sending event #{inspect(event)} to object #{object.id}")
{:noreply, state}
end
defp is_aspect?(module) do
attributes = module.__info__(:attributes)
Aspect in Access.get(attributes, :behaviour, [])
end
end