ECSpanse - an Entity Component System framework for Elixir

What is the purpose for only allowing on Server per otp app? I’m trying to figure out the best way to have a room based game system, where you can join a virtual room to play with others. I’d like to be able to start one server per “room” (Room “ABCD” and Room “ZYXW”) like so:

DynamicSupervisor.start_child(
  MyApp.DynamicSupervisor,
  {MyApp.EcsServer, name: {:via, Registry, {MyApp.Registry, "ABCD"}}}
)

DynamicSupervisor.start_child(
  MyApp.DynamicSupervisor,
  {MyApp.EcsServer, name: {:via, Registry, {MyApp.Registry, "ZYXW"}}}
)

But trying to start a second server gives an error. Am I thinking about this wrong? Is there a better way to accomplish what I’m going for?

2 Likes

@trentjones21 Your question makes a lot of sense. There were other users before asking about the same thing but in a different context. For example the last question in this post: ECSpanse - an Entity Component System framework for Elixir - #34 by iacobson

As I replied also there, it was a deliberate decision I took. I totally agree that spawning a new server for each room or session etc., feels more “Elixiry”.

However, the library needs to interact with a frontend framework (eg. LiveView, LiveBook) that is not aware of the internal context of the Ecspanse server. That would mean, for every query run outside of the ECS context, you would need to pass a map or a token that would identify the Ecspanse server and the corresponding ETS tables where the data is stored. So the query would need what room to run the query for.

For example, in a LiveView Component, instead of:
Ecspanse.Query.fetch_component(entity, Demo.Components.Energy)
you would have:
Ecspanse.Query.fetch_component(some_token, entity, Demo.Components.Energy)

This gets quite annoying, especially when you do not need this feature. So the tradeoff was to hardcode the Server and all ETS names and allow a single ETS “World” per instance.

The other reason is that sharing data between those servers is not trivial.

The alternative

That being said, the current alternative is to create room entities and assign the players as Children to the rooms.

The you can easily query all the players in a room. For example:

If only player entities are children of the room
player_entities =  Ecspanse.Query.list_children(room_entity)
If the room has also other type of children
player_entities = Ecspanse.Query.select({Ecspanse.Entity}, 
  with: [MyGame.Comp.Player],
  for_children_of: [room_entity])
|> Ecspanse.Query.stream()
|> Enum.map( fn {player_entity} -> player_entity end )

Hope this answers your question, even if not ideal for your use case.

2 Likes

How do you modify the state of the Ecspanse server through a liveview? I see in the examples for enabling a system in the setup callback, you can add the option of “run_in_state”. I’m not sure of how to modify the state of the server so that the system only runs when in, let’s say, the “loading” state.

For example:

 @impl Ecspanse
  def setup(data) do
    data
    |> Ecspanse.add_system_set({__MODULE__, :onload_sync_systems}, [run_in_state: :loading])
    |> Ecspanse.add_system_set({__MODULE__, :play_sync_systems}, [run_in_state: :play])
  end

  def onload_sync_systems(data) do
    data
    |> Ecspanse.add_frame_start_system(Systems.SpawnShip)
  end

  def play_sync_systems(data) do
    data
    |> Ecspanse.add_system(Systems.MoveShip)
  end

How would I change the state to “:loading” or “:play”?

1 Like

The generic way to modify anything (components or resources) from outside Ecspanse (eg. Liveview) is with events.

Some example in the tutorial: Tutorial — ECSpanse v0.8.1

In your specific case, you can create an event (eg UpdateStateEvent) and a system that subscribes to this event (eg UpdateStateSystem).

The global state we speak about is just an Ecspanse predefined Resource.

So in the system you can retrieve the State Resource and update it like so:

{:ok, state_resource} = Ecspanse.Query.fetch_resource(Ecspanse.Resource.State)
Ecspanse.Command.update_resource!(state_resource, value: :loading)

Additional info useful for anybody using the library

While the above solution would work fine now, I am planning a complete re-write of the State resource that will introduce some breaking changes.
Now there is just this Ecspanse.Resource.State global state, and while useful, it is quite inflexible.
I plan to:

  • replace it with a generic State template Resource, so we can then create more states.
  • add special commands to create and update the state resources.
  • on_enter_state and on_exit_state systems which will be automatically called on state change.
  • a way to initialize state and resources in the setup function

Please consider this is in a very early concept phase, so it will take some time until release.

1 Like

Great thank you! I’ll give that try

1 Like

New release v0.9.0

The main goal of this release is to introduce a flexible way to create global state machines, replacing the existing Resource.State which is very inflexible.

Breaking

  • removes Ecspanse.Resource.State in favor of Ecspanse.State functionality. Please check the changelog for a quick guide on how to replace the old state and use the new functionalities.

Features

  • allows inserting resources at startup with Ecspanse.insert_resource/2
  • allows state init at startup with Ecspanse.init_state/2
  • introduces Ecspanse.State state functionalities. See the breaking changes for more details.
  • new library built-in Ecspanse.Event.StateTransition event
  • new library built-in Ecspanse.Component.Name component

Improvements

  • Ecspanse.Query.entity_exists?/1 to check if an entity still exists
  • Ecspanse.Command.add_and_fetch_component!/2 wrapper to return a component after creation
  • Ecspanse.Command.update_and_fetch_component!/2 wrapper to return a component after update
3 Likes

I’m really looking forward to making something with this library. The documentation is so good.

1 Like

Hi! Just want to thank you for the library.

I’m learning Elixir/Phoenix and decided to build a sports simuation game using ECSpanse. It’s been fun so far!

1 Like

Thanks so much for trying Ecspanse. And have fun learning Elixir!

New release v0.10.0

Introduces the introduces Ecspanse.Snapshot module to enable custom save and load solutions.

Check the new guide for some implementation ideas. And of course, let me know what you think

4 Likes

Hmm, maybe something isn’t clicking…

I have a Game Entity where I use a GameID Component to store the game_id.

How do I use Ecspanse.Query.select to select a game for a particular game_id?

Indeed, queries will not look into the component’s internal state, which is also not their goal.

Depending on your game setup and what you try to achieve, there are multiple ways to achieve this. For example:

  1. If you really need the game ID to be stored in a component

like %Game{id: "game_id_1"}

This is not an ideal case if you have multiple parallel games and you need to query one of them. Still, you can achieve it by querying all Game components with their Entitites, then find the one you are looking for. For example:

{entity, game_component} = 
Ecspanse.Query.select({Ecspanse.Entity, Game})
|> Ecspanse.Query.stream()
|> Enum.to_list()
|> Enum.filter(fn {entity, game_component} -> game_component.id == "game_id_1" end)

Now you have your game entity, and game_component, if you need it.

  1. If you do not need to store your game ID in a component

You can optimize your code by spawning your game entity with a custom ID. You can still pass the Game component to it if you want.

Ecspanse.Command.spawn_entity!(
      {
        Ecspanse.Entity,
        id: "game_id_1",
        components: [{Game, id: "game_id_1"}]

# Then you can query the entity by its ID

{:ok, game_entity} = Ecspanse.Query.fetch_entity("game_id_1")

# Then if you need, you can fetch the game component for the game entity

{:ok, game_component} = Game.fetch(game_entity)

I hope this solves your current issue.

Doh! That makes more sense. ID should be on the Entity, not the Component.

Much appreciated. :smile:

1 Like