Provides common behavior and callbacks for components.
Components are modular pieces of state and behavior that can be attached to entities.
Defining a component
Defining a component is as simple as creating a module that uses Genesis.Component.
Then, you define which properties the component should have by using the prop macro.
Options
:name- the component name used for registration (optional):events- list of events this component should handle
Examples
defmodule MyApp.Components.Health do
use Genesis.Component, events: [:damage]
prop :current, :integer, default: 100
prop :maximum, :integer, default: 100
end
defmodule MyApp.Components.MegaHealth do
use Genesis.Component, name: "custom_health", events: [:damage, :regenerate]
prop :current, :integer, default: 200
prop :maximum, :integer, default: 200
prop :regen_rate, :integer, default: 5
endHandling Events
Components can respond to events dispatched to their entity. When handling events you must
return either {:cont, event} to continue processing or {:halt, event} to stop propagation.
Here's a naive example of how one could handle a damage event to update an entity's health:
def handle_event(:damage, event) do
%{args: %{amount: amount}} = event
# Get the current health component
health = get(event.entity)
# Reduces the current health by the damage amount
update(event.entity, :current, & &1 - amount)
{:cont, event}
endYou can also do interesting things like dispatching more events or transforming
the arguments before it gets processes by other components. For instance, imagine
you have a FireShield component that halves the damage taken from fire attacks:
def handle_event(:damage, event) do
%{args: %{type: type, amount: amount}} = event
final_damage = if type == :fire, do: amount / 2, else: amount
{:cont, put_in(event, [:args, :amount], final_damage)}
endLifecycle Hooks
Components can also react to lifecycle events using the on_hook/3 callback:
defmodule MyApp.Components.Logger do
use Genesis.Component
require Logger
def on_hook(:attached, entity, component) do
Logger.info("Component attached to entity: #{entity.hash}")
end
def on_hook(:removed, entity, component) do
Logger.info("Component removed from entity: #{entity.hash}")
end
def on_hook(:updated, entity, component) do
Logger.info("Component updated on entity: #{entity.hash}")
end
end
Summary
Callbacks
Attaches a component to an entity. If the entity belongs to a world, the attachment is performed within the world's context.
Casts the given properties into a map of permitted values. This function normalizes input that can be used to create a component.
Retrieves a component from an entity.
Handles events dispatched to this component via its parent entity.
Creates a new component by casting the given properties.
The given properties are passed to the cast/1 function.
Called when a component is :attached, :removed or :updated on an entity.
Receives the hook name, the entity, and the component struct that triggered the hook.
Removes a component from an entity. If the entity belongs to a world, the removal is performed using the world's context.
Updates a component attached to an entity by merging the given properties. If the entity belongs to a world, the update is performed within the world's context.
Updates a specific property of a component attached to the entity. If the entity belongs to a world, the update is performed within the world's context.
Types
@type component() :: struct()
@type entity() :: Genesis.Entity.t()
@type event() :: Genesis.Event.t()
@type hook() :: :attached | :removed | :updated
Callbacks
@callback attach(entity(), properties()) :: :ok | :noop | :error
Attaches a component to an entity. If the entity belongs to a world, the attachment is performed within the world's context.
Returns :ok if the component was successfully attached, :noop if a component with the same properties
is already attached, or :error if the same component with different properties is already attached.
Examples
Health.attach(entity)
#=> :ok
Position.attach(entity, x: 10, y: 20)
#=> :ok
Position.attach(entity, x: 10, y: 20)
#=> :noop
Position.attach(entity, x: 15, y: 25)
#=> :error
@callback cast(properties()) :: map()
Casts the given properties into a map of permitted values. This function normalizes input that can be used to create a component.
Retrieves a component from an entity.
Returns the component struct if present or the default value.
Examples
Health.get(entity)
#=> %Health{...}
Position.get(entity)
#=> nil
Position.get(entity, Position.new())
#=> %Position{...}
Handles events dispatched to this component via its parent entity.
Given that the same event is dispatched to all components within an entity, this
function should return a tuple with :cont or :halt to either keep processing
the event or stop propagating the event to the remaining components in the pipeline.
@callback new(properties()) :: component()
Creates a new component by casting the given properties.
The given properties are passed to the cast/1 function.
Examples
# Using default values
health = Health.new()
# Using a map
health = Health.new(%{current: 80, maximum: 100})
# Using a keyword list
health = Health.new(current: 80, maximum: 100)
Called when a component is :attached, :removed or :updated on an entity.
Receives the hook name, the entity, and the component struct that triggered the hook.
@callback remove(entity()) :: :ok | :noop
Removes a component from an entity. If the entity belongs to a world, the removal is performed using the world's context.
Returns :noop if the component is not present.
Examples
{:ok, entity} = Genesis.Context.create(context)
Health.attach(entity, current: 80, maximum: 100)
Health.remove(entity)
#=> :ok
Position.remove(entity)
#=> :noop
@callback update(entity(), properties()) :: :ok | :noop | :error
Updates a component attached to an entity by merging the given properties. If the entity belongs to a world, the update is performed within the world's context.
Will return :ok on success or :noop if the component is not present.
Examples
{:ok, entity} = Genesis.Context.create(context)
Health.attach(entity, current: 80, maximum: 100)
Health.update(entity, current: 50)
#=> :ok
Position.update(entity, x: 15, y: 25)
#=> :noop
Updates a specific property of a component attached to the entity. If the entity belongs to a world, the update is performed within the world's context.
Will return :noop if the component is not present or :error if the property does not exist.
Examples
{:ok, entity} = Genesis.Context.create(context)
Health.attach(entity, current: 80, maximum: 100)
Health.update(entity, :current, & &1 + 10)
#=> :ok
Position.update(entity, :x, & &1 + 5)
#=> :noop
# Trying to update a non-existing property
Health.update(entity, :invalid, & &1 + 10)
#=> :error