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 that use 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(see Ecspanse.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

Link to this function

add_frame_end_system(data, system_module, opts \\ [])

View Source
@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)
Link to this function

add_frame_start_system(data, system_module, opts \\ [])

View Source
@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)
Link to this function

add_shutdown_system(data, system_module)

View Source
@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)
Link to this function

add_startup_system(data, system_module)

View Source
@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)
Link to this function

add_system(data, system_module, opts \\ [])

View Source
@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 using Ecspanse.State and the atom state in which the system should run.
  • :run_not_in_state - same as run_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 a run_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 and run_not_in_state
  • custom conditions: run_if a function returns true

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]
  )
Link to this function

add_system_set(data, arg, opts \\ [])

View Source
@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
Link to this function

event(event_spec, opts \\ [])

View Source
@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 to default, 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}}
Link to this function

init_state(data, state_spec)

View Source

Initializes a state at startup.

Note

States can be initialized once, only at startup.

Link to this function

insert_resource(data, resource_spec)

View Source

Inserts a new global resource at startup. See Ecspanse.Resource and Ecspanse.Command.insert_resource!/1 for more info.