View Source Ecspanse.Projection behaviour (ECSpanse v0.10.0)
The Ecspanse.Projection
behaviour is used to build state projections.
The projections are defined by invoking use Ecspanse.Projection
in their module definition.
Projections are used to build models and query the state of application across multiple entities and their components.
They are designed to be created and used by external clients (such as UI libraries, for example Phoenix LiveView),
The Projections are GenServers and the client that creates them is responsible for
storing their pid
and using it to communicate with them.
The module invoking use Ecspanse.Projection
must implement the mandatory
Ecspanse.Projection.project/1
callback. This is responsible for
querying the state and building the projection struct.
Info
On server initialization, the
on_change/3
callback is called with the initial calculated projection as the new_projection, and the default projection struct as the previous_projection. This is executed even if the calculated projection is the same as the default one. As theon_change/3
callback is generally used to send the projection to the client, this ensures that the client receives the initial projection.
Note
The
project/2
callback runs every frame, after executing all systems. Many projections with complex queries may have a negative impact on performance.
Options
:fields
- a list with all the event struct keys and their initial values (if any) For example:[:pos_x, :pos_y, resources_gold: 0, resources_gems: 0]
Examples
The Projection
defmodule Demo.Projections.Hero do
use Ecspanse.Projection, fields: [:pos_x, :pos_y, :resources_gold, :resources_gems]
@impl true
def project(%{entity_id: entity_id} = _attrs) do
{:ok, entity} = fetch_entity(entity_id)
{:ok, pos} = Demo.Components.Position.fetch(entity)
{:ok, gold} = Demo.Components.Gold.fetch(entity)
{:ok, gems} = Demo.Components.Gems.fetch(entity)
result = struct!(__MODULE__, pos_x: pos.x, pos_y: pos.y, resources_gold: gold.amount, resources_gems: gems.amount)
{:ok, result}
end
@impl true
def on_change(%{client_pid: pid} = _attrs, new_projection, _previous_projection) do
# when the projection changes, send it to the client
send(pid, {:projection_updated, new_projection})
end
end
The Client
#...
projection_pid = Demo.Projections.Hero.start!(%{entity_id: entity.id, client_pid: self()})
projection = Demo.Projections.Hero.get!(projection_pid)
# ...
def handle_info({:projection_updated, projection}, state) do
# received every time the projection changes
# ...
end
# ...
Demo.Projections.Hero.stop(projection_pid)
Summary
Implemented Callbacks
Gets the Projection struct by providing the server pid
.
Starts a new projection server and returns its pid
.
Stops the projection server by its pid
.
Callbacks
Optional callback that is executed every time the projection changes.
The project/1
callback is responsible for querying the state and building the Projection struct.
Implemented Callbacks
Gets the Projection struct by providing the server pid
.
Implemented Callback
This callback is implemented by the library and can be used as such.
Examples
%Ecspanse.Projection{state: :ok, result: %Demo.Projection.Hero{}} =
Demo.Projections.Hero.get!(projection_pid)
Starts a new projection server and returns its pid
.
It takes a single attrs map
argument.
Info
The
attrs
map is passed to theEcspanse.Projection.project/1
andEcspanse.Projection.on_change/3
callbacks.
The caller is responsible for storing the returned pid
.
Implemented Callback
This callback is implemented by the library and can be used as such.
Examples
projection_pid = Demo.Projections.Hero.start!(%{entity_id: entity.id, client_pid: self()})
@callback stop(projection_pid :: pid()) :: :ok
Stops the projection server by its pid
.
Implemented Callback
This callback is implemented by the library and can be used as such.
Examples
Demo.Projections.Hero.stop(projection_pid)
Types
@type projection_result() :: projection_result_success() | any() | nil
@type projection_result_success() :: struct()
@type projection_state() :: :loading | :ok | :error | :halt
@type t() :: %Ecspanse.Projection{ error?: boolean(), halted?: boolean(), loading?: boolean(), ok?: boolean(), result: projection_result(), state: projection_state() }
Callbacks
Optional callback that is executed every time the projection changes.
It takes the attrs
map argument passed to Ecspanse.Projection.start!/1
,
the new projection and the previous projection structs as arguments. The return value is ignored.
Examples
@impl true
def on_change(%{client_pid: pid} = _attrs, new_projection, _previous_projection) do
send(pid, {:projection_updated, new_projection})
end
@callback project(attrs :: map()) :: :loading | {:loading, any()} | {:ok, projection_result_success()} | :error | {:error, any()} | :halt
The project/1
callback is responsible for querying the state and building the Projection struct.
It takes the attrs
map argument passed to Ecspanse.Projection.start!/1
.
It must return one of the following:
:loading
- the projection is in the:loading
state, theresult
isnil
{:loading, result :: any()}
- the projection is in the:loading
state, theresult
is the given value{:ok, success_projection :: struct()}
- the projection is in the:ok
state, theresult
is the implemented projection struct:error
- the projection is in the:error
state, theresult
isnil
{:error, result :: any()}
- the projection is in the:error
state, theresult
is the given value:halt
- the projection is in the:halt
state, theresult
is the existing projection result
Info
Returning
:halt
is very useful for expensive projections that need to run just in certain conditions. For example, a projection that calculates the distance between two entities should only run when both entities exist. If one of the entities is removed, the projection can be set to:halt
and should not be recalculated until the entity is added again.
Examples
@impl true
def project(%{entity_id: entity_id} = _attrs) do
# ...
cond do
enemy_entity_missing?(entity_id) ->
:loading
enemy_location_component_missing?(entity_id) ->
:halt
true ->
{:ok, struct!(__MODULE__, pos_x: comp_pos.x, pos_y: comp_pos.y}}
end
end
# the client
<div :if={@enemy_projection.loading?}>Spinner</div>
<div :if={@enemy_projection.ok?}>Show Enemy</div>