View Source Ecspanse.System (ECSpanse v0.9.0)

The system implements the logic and behaviors of the application by manipulating the state of the components. The systems are defined by invoking use Ecspanse.System in their module definition.

The system modules must implement the Ecspanse.System.WithoutEventSubscriptions.run/1 or Ecspanse.System.WithEventSubscriptions.run/2 callbacks, depending if the system subscribes to certain events or not. The return value of the run function is ignored.

The Ecspanse systems run either synchronously or asynchronously, as scheduled in the Ecspanse.setup/1 callback.

Systems are the sole mechanism through which the state of components can be altered. Running commands outside of a system is not allowed.

Resources can be created, updated, and deleted only by systems that are executed synchronously.

There are some special systems that are created automatically by the framework:

Info

The Ecspanse.Query and Ecspanse.Command functions are imported by default for all modules that use Ecspanse.System

Options

  • :lock_components - a list of component modules that will be locked for the duration of the system execution.
  • :event_subscriptions - a list of event modules that the system subscribes to.

Component locking

Component locking is required only for async systems to avoid race conditions.

For async systems, any components that are to be modified, created, or deleted, must be locked in the lock_components option. Otherwise, the operation will fail. Wherever it makes sense, it is recommended to lock also components that are queried but not modified, as they could be modified by other systems.

Not all async systems run concurrently. The systems are grouped in batches, based on the components they lock.

Event subscriptions

The event subscriptions enables a system to execute solely in response to certain specified events.

The Ecspanse.System.WithEventSubscriptions.run/2 callback is triggered for every occurrence of an event type to which the system has subscribed. These callbacks execute concurrently to enhance performance. However, they are grouped based on their batch keys (see Ecspanse.event/2 options) as a safeguard against potential race conditions.

Examples

  defmodule Demo.Systems.Move do
    @moduledoc "An async system locking components, that subscribes to an event"
    use Ecspanse.System,
      lock_components: [Demo.Components.Position],
      event_subscriptions: [Demo.Events.Move]

    def run(%Demo.Events.Move{entity_id: entity_id, direction: direction}, frame) do
      # move logic
    end
  end

  defmodule Demo.Systems.SpawnEnemy do
    @moduledoc "A sync system that does not need to lock components, and it is not subscribed to any events"
    use Ecspanse.System

    def run(frame) do
      # spawn logic
    end
  end

Summary

Functions

Utility function. Gives the current process Ecspanse.System abilities to execute commands.

Allows running async code inside a system.

Types

@type system_queue() ::
  :startup_systems
  | :frame_start_systems
  | :batch_systems
  | :frame_end_systems
  | :shutdown_systems
@type t() :: %Ecspanse.System{
  execution: :sync | :async,
  module: module(),
  queue: system_queue(),
  run_after: [system_module :: module()],
  run_conditions: [{module(), atom()}]
}

Functions

@spec debug() :: :ok

Utility function. Gives the current process Ecspanse.System abilities to execute commands.

This is a powerful tool for testing and debugging, as the promoted process can change the components and resources state without having to be scheduled like a regular system.

See Ecspanse.TestServer for more details.

This function is intended for use only in testing and development environments.

Link to this function

execute_async(enumerable, fun, opts \\ [])

View Source
@spec execute_async(Enumerable.t(), (term() -> term()), keyword()) :: :ok

Allows running async code inside a system.

Because commands can run only from inside a system, running commands in a Task, for example, is not possible. The execute_async/3 is a wrapper around Elixir.Task.async_stream/3 and is built exactly for this purpose. It allows running commands in parallel.

The result of the processing is ignored. So the function is suitable for cases when the result is not important. For example, updating components for a list of entities.

Info

This function is already imported for all modules that use Ecspanse.System

Options

  • :concurrent - the number of concurrent tasks to run. Defaults to the number of schedulers online. See Elixir.Task.async_stream/5 options for more details.

use with care

While the locked components ensure that no other system is modifying the same components at the same time, the execute_async/3 does not offer any such guarantees inside the same system.

For example, the same component can be modified concurrently, leading to race conditions and inconsistent state.

Examples

    Ecspanse.System.execute_async(
      enemy_entities,
      fn enemy_entity ->
        # update the enemy components
      end,
      concurrent: length(enemy_entities) + 1
    )