View Source Ecspanse behaviour (ECSpanse v0.10.0)
Ecspanse is an Entity Component System (ECS) framework for Elixir.
Note
Ecspanse is not a game engine, but a flexible foundation for managing state and building logic, offering features like:
- flexible queries with multiple filters
- dynamic bidirectional relationships
- versatile tagging capabilities
- system event subscriptions
- asynchronous system execution
The core structure of the Ecspanse library is:
Ecspanse
: The main module used to configure and interact with the library.Ecspanse.Server
: The server orchestrates the execution of systems and the storage of components, resources, and events.Ecspanse.Entity
: A simple struct with an ID, serving as a holder for components.Ecspanse.Component
: A struct that may hold state information or act as a simple label for an entity.Ecspanse.System
: Holds the application core logic. Systems run every frame, either synchronously or asynchronously.Ecspanse.Resource
: Global state storage, similar to components but not tied to a specific entity. Resources can only be created, updated, and deleted by synchronously executed systems.Ecspanse.Query
: A tool for retrieving entities, components, or resources.Ecspanse.Command
: A mechanism for changing components and resources state. They can only be triggered from a system.Ecspanse.Event
: A mechanism for triggering events, which can be listened to by systems. It is the way to communicate externally with the systems.
Usage
To use Ecspanse, a module needs to be created, invoking use Ecspanse
. This implements the Ecspanse
behaviour, so the setup/1
callback must be defined. All the systems and their execution order are defined in the setup/1
callback.
Examples
defmodule TestServer1 do
use Ecspanse, fps_limit: 60, version: 1
def setup(data) do
data
|> add_startup_system(Demo.Systems.SpawnHero)
|> add_frame_start_system(Demo.Systems.PurchaseItem)
|> add_system(Demo.Systems.MoveHero)
|> add_frame_end_system(Ecspanse.System.Timer)
|> add_shutdown_system(Demo.Systems.Cleanup)
end
end
Info
The
Ecspanse
system scheduling functions are imported by default for the setup module thatuse Ecspanse
Configuration
The following configuration options are available:
:fps_limit
(optional) - integer or :unlimited - the maximum number of frames per second. Defaults to :unlimited.:version
(optional) - non negative integer - the version of the ECS game logic. Can be used to determine backwards compatibility when saving and loading state(seeEcspanse.Snapshot
for details). Defaults to 0.
Special Resources
Some special resources, such as FPS
, are created by default by the framework.
Summary
Callbacks
The setup/1
callback is called on Ecspanse startup and is the place to define the running systems.
It takes an Ecspanse.Data
struct as an argument and returns an updated struct.
Functions
Schedules a frame end system to be executed each frame during the game loop.
Schedules a frame start system to be executed each frame during the game loop.
Schedules a shutdown system.
Schedules a startup system.
Schedules an async system to be executed each frame during the game loop.
Convenient way to group together related systems.
Queues an event to be processed in the next frame.
Retrieves the Ecspanse Server process PID. If the data process is not found, it returns an error.
Initializes a state at startup.
Inserts a new global resource at startup.
See Ecspanse.Resource
and Ecspanse.Command.insert_resource!/1
for more info.
Callbacks
@callback setup(Ecspanse.Data.t()) :: Ecspanse.Data.t()
The setup/1
callback is called on Ecspanse startup and is the place to define the running systems.
It takes an Ecspanse.Data
struct as an argument and returns an updated struct.
Examples
defmodule MyProject do
use Ecspanse
@impl Ecspanse
def setup(%Ecspanse.Data{} = data) do
data
|> Ecspanse.Server.add_system(Demo.Systems.MoveHero)
end
end
Functions
@spec add_frame_end_system( Ecspanse.Data.t(), system_module :: module(), opts :: keyword() ) :: Ecspanse.Data.t()
Schedules a frame end system to be executed each frame during the game loop.
A frame end system is executed synchronously at the end of each frame. Sync systems are executed in the order they were added.
Options
- See the
add_system/3
function for more information about the options.
Examples
Ecspanse.add_frame_end_system(ecspanse_data, Ecspanse.Systems.Timer)
@spec add_frame_start_system( Ecspanse.Data.t(), system_module :: module(), opts :: keyword() ) :: Ecspanse.Data.t()
Schedules a frame start system to be executed each frame during the game loop.
A frame start system is executed synchronously at the beginning of each frame. Sync systems are executed in the order they were added.
Options
- See the
add_system/3
function for more information about the options.
Examples
Ecspanse.add_frame_start_system(ecspanse_data, Demo.Systems.PurchaseItem)
@spec add_shutdown_system(Ecspanse.Data.t(), system_module :: module()) :: Ecspanse.Data.t()
Schedules a shutdown system.
A shutdown system runs only once when the Ecspanse.Server terminates. Shutdown systems do not take any options. This is useful for cleaning up or saving the game state.
Examples
Ecspanse.add_shutdown_system(ecspanse_data, Demo.Systems.Cleanup)
@spec add_startup_system(Ecspanse.Data.t(), system_module :: module()) :: Ecspanse.Data.t()
Schedules a startup system.
A startup system runs only once during the Ecspanse startup process. Startup systems do not take any options.
Examples
Ecspanse.add_startup_system(ecspanse_data, Demo.Systems.SpawnHero)
@spec add_system(Ecspanse.Data.t(), system_module :: module(), opts :: keyword()) :: Ecspanse.Data.t()
Schedules an async system to be executed each frame during the game loop.
Options
:run_in_state
- a tuple of the state module usingEcspanse.State
and the atom state in which the system should run.:run_not_in_state
- same asrun_in_state
, but the system will run when the state is not the one provided.:run_if
- a list of tuples containing the module and function that define a condition for running the system. Eg.[{MyModule, :my_function}]
. The function must return a boolean.:run_after
- only for async systems - a system or list of systems that must run before this system.
Order of execution
Systems are executed each frame during the game loop. Sync systems run in the order they were added to the data's operations list.
Async systems are grouped in batches depending on the components they are locking.
See the Ecspanse.System
module for more information about component locking.
The order in which async systems run can pe specified using the run_after
option.
This option takes a system or list of systems that must be run before the current system.
When using the run_after: SystemModule1
or run_after: [SystemModule1, SystemModule2]
option, the following rules apply:
- The system(s) specified in
run_after
must be already scheduled. This prevents circular dependencies. - There is a deliberate choice to allow only the
run_after
ordering option. While arun_before
option would simplify some relations, it can also introduce circular dependencies.
Example of circular dependency:
- System A
- System B, run_before: System A
- System C, run_after: System A, run_before: System B
Info
The 'run_after' option does not depend on the "before" system being executed or not (eg. when the "before" system subscribes to an event that is not triggered this frame). The system will be executed anyway, even if the "before" system is not executed this frame. The
:run_after
option is evaluated only once, at server start-up, when the async systems are grouped together into batches. Then the scheduler tries to execute every system in every batch.
Conditionally running systems
The systems can be programmed to run only if some specific conditions are met. The conditions can be:
- state related:
run_in_state
andrun_not_in_state
- custom conditions:
run_if
a function returnstrue
run_in_state
supports a single state. If a combination of states is needed to run a system, the run_if
option can be used.
Ecspanse.add_system(
ecspanse_data,
Demo.Systems.MoveHero,
run_if: [{Demo.States.Game, :in_market_place}]
)
def in_market_place do
Demo.States.Game.get_state!() == :paused and
Demo.States.Location.get_state!() == :market
end
It is important to note that the run conditions are evaluated only once per frame, at the beginning of the frame. So any change in the running conditions will be picked up in the next frame.
If the system is part of a system set, and both the system and the system set have run conditions, the conditions are cumulative. All the conditions must be met for the system to run.
Examples
Ecspanse.add_system(
ecspanse_data,
Demo.Systems.MoveHero,
run_in_state: {Demo.States.Game, [:play]},
run_after: [Demo.Systems.RestoreEnergy]
)
@spec add_system_set( Ecspanse.Data.t(), {module(), function :: atom()}, opts :: keyword() ) :: Ecspanse.Data.t()
Convenient way to group together related systems.
New systems can be added to the set using the add_system_*
functions.
System sets can also be nested.
Options
The set options that applied on top of each system options in the set.
- See the
add_system/3
function for more information about the options.
Examples
defmodule Demo do
use Ecspanse
@impl Ecspanse
def setup(data) do
data
|> Ecspanse.add_system_set({Demo.HeroSystemSet, :setup}, [run_in_state: {Demo.States.Game, :play}])
end
defmodule HeroSystemSet do
def setup(data) do
data
|> Ecspanse.add_system(Demo.Systems.MoveHero, [run_after: Demo.Systems.RestoreEnergy])
|> Ecspanse.add_system_set({Demo.ItemsSystemSet, :setup})
end
end
defmodule ItemsSystemSet do
def setup(data) do
data
|> Ecspanse.add_system(Demo.Systems.PickupItem)
end
end
end
@spec event( Ecspanse.Event.event_spec(), opts :: keyword() ) :: :ok
Queues an event to be processed in the next frame.
Options
:batch_key
- A key for grouping multiple similar events in different batches within the same frame. The event scheduler groups the events into batches by unique{EventModule, batch_key}
combinations. In most cases, the key may be an entity ID that either triggers or is impacted by the event. Defaults todefault
, meaning that similar events will be placed in separate batches.
Examples
Ecspanse.event({Demo.Events.MoveHero, direction: :up}, batch_key: hero_entity.id)
@spec fetch_pid() :: {:ok, pid()} | {:error, :not_found}
Retrieves the Ecspanse Server process PID. If the data process is not found, it returns an error.
Examples
Ecspanse.fetch_pid()
{:ok, %{name: data_name, pid: data_pid}}
@spec init_state(Ecspanse.Data.t(), Ecspanse.State.state_spec()) :: Ecspanse.Data.t()
Initializes a state at startup.
Note
States can be initialized once, only at startup.
@spec insert_resource(Ecspanse.Data.t(), Ecspanse.Resource.resource_spec()) :: Ecspanse.Data.t()
Inserts a new global resource at startup.
See Ecspanse.Resource
and Ecspanse.Command.insert_resource!/1
for more info.