View Source Ecspanse.Projection behaviour (ECSpanse v0.7.3)
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)
struct!(__MODULE__, pos_x: pos.x, pos_y: pos.y, resources_gold: gold.amount, resources_gems: gems.amount)
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.
Optional callback that allows the projection to run only when certain conditions are met. This is useful for expensive projections that are not always needed.
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
%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)
Callbacks
@callback on_change( attrs :: map(), new_projection :: struct(), previous_projection :: struct() ) :: any()
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
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 the projection struct.
Examples
@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)
struct!(__MODULE__, pos_x: pos.x, pos_y: pos.y, resources_gold: gold.amount, resources_gems: gems.amount)
end
Optional callback that allows the projection to run only when certain conditions are met. This is useful for expensive projections that are not always needed.
It takes the attrs
map argument passed to Ecspanse.Projection.start!/1
and
the current projection struct as arguments. It returns a boolean.
Examples
@impl true
@doc "Run the projection only if the hero is alive"
def run?(%{entity_id: entity_id} = _attrs, _current_projection) do
with {:ok, entity} = fetch_entity(entity_id),
{:ok, hero_comp} = Demo.Components.Hero.fetch(entity) do
hero.state == :alive
else
_ -> false
end
end