ECSpanse - an Entity Component System framework for Elixir

I know @ConnorRigby dabbles with game development using Elixir for the server part:

1 Like

New release v0.3.1

Fixes

  • fixes a bug where events could be scheduled after they were batched for the current frame, and before the current events are cleared, causing some events to be lost. Thanks to @andzdroid for identifying and documenting the issue.
  • fixes a bug where temporary timers would crash. Thanks to @holykol for finding and fixing the issue.

Features

  • imports Ecspanse.Query and Ecspanse.Command in all systems, so all the queries and commands are available without needing the respective module prefix.
  • imports Ecspanse in the setup module that use Ecspanse so the system scheduling functions are available without needing the module prefix.

If you are using the library, please update it to 0.3.1 to get rid of the 2 bugs.

I updated also the 2 demo projects to use the latest version.

1 Like

I tried to create 1000 entities, but the fps drops to 1 per second.

I also tried creating 10000 entities, this never got past initialization (or took more time than I was willing to wait).

Thank you for doing the test.

What was your use case? Did you try it with the I’ve seen things demo project and switch the player count to 1000?

Or did you build your own custom example?

  1. If you tried with the demo project, please remember that this is not optimized. As said, it was just a side project to build while developing the library. I think many things can be done differently there.
    Also, if you spawn 1000 players, that means probably around 5-6000 entities.

  2. If on the other hand, you built your custom project, could you share it?

That being said, I am fully aware that speed is not the greatest attribute of Ecspanse. I needed a library with extended ECS capabilities to build the Orbituary project with Elixir and Phoenix, and my plan is to improve the speed (where possible) as I develop that game.

No use case, only wanted to see how it runs with more entities.

I was on the ecspanse_demo project, I added this in the SpawnMarket system:

    Enum.each(0..999, fn _x ->
      %Ecspanse.Entity{} = Ecspanse.Command.spawn_entity!(Demo.Entities.Inventory.new_map())
    end)

Actually I just found out if I comment out the MaybeFindResources system it runs at a normal framerate.

Edit:
I also have another project with my own entities. It looks like if any system locks any of the components on the 1000 entities that are spawned, the framerate drops to 1. Even if the system runs on events that haven’t triggered.

Very interesting. I will try to reproduce as soon as I find some time, and get back.

1 Like

I tried your example, but until now, I could not reproduce exactly the case yet.

I used this

    Enum.each(0..3000, fn _x ->
      %Ecspanse.Entity{} = Ecspanse.Command.spawn_entity!(Demo.Entities.Inventory.new_map())
    end)

So, I ran your example with 3000 entities, not 1000, and still got 100+ frames.

Indeed the startup is extremely slow. But give it enough time and the frame rate changes from 1 to 100+.

I also updated the demo project to display the FPS in the LiveView. You can pull the main branch and use it.

I identified some potential small improvements for the next version, but nothing major.

Also as a heads-up, I’m thinking of removing the automatically generated events like Ecspanse.Event.ComponentUpdated. Their functionality can be easily replaced with triggering custom events, and they pollute the events queue, every frame.

Later Edit

After some optimizations for the tagging system, got the same 3000 entities running at ~500FPS.
This will require a lot more work on my side, but I hope to push a new version somewhere next week.

3 Likes

New release v0.4.0

  • breaking: removes the automatically generated events on every component create, update and destroy. While this was useful, it was also very noisy, generating many events every frame. The functionality can be easily replaced by emitting a manual event wherever needed, or by creating a short-lived component for the specific entity.

  • improvement: change the way tagged components are handled. This improved the frame rate for cases when many tagged components had to be queried.

Thanks again to @andzdroid for testing the library and reporting the performance issues.
While Ecspanse was not created with a focus on performance, but mostly on functionality, it is still good to improve also the performance whenever possible.

1 Like

@andzdroid, version v0.4.0 has now been released, so you can perform your tests with the new version if you want. I have also updated the ecspanse_demo project to use the latest version.

That being said, for your specific case where you want to spawn a significant amount of entities on startup, the Ecspanse.Command.spawn_entities!/1 is a much more efficient way to do this.

Per your example, it would be something like:

    specs =
      Enum.map(0..3000, fn _x ->
        Demo.Entities.Inventory.new_map()
      end)

    Ecspanse.Command.spawn_entities!(specs)

    IO.inspect("READY!!!")
1 Like

New release v0.5.0

Introduces ancestors related queries to look for parents of an entity, the parents of their parents, and so on:

  • added a new option to Ecspanse.Query.select/2: :for_ancestors_of
  • Ecspanse.Query.list_ancestors/1
  • Ecspanse.Query.list_tagged_components_for_ancestors/2

So now it is possible to look for top-level parents of deeply nested entities.

For example, let’s consider a hero entity, child of a clan entity, child of a mission entity, child of a dungeon entity. Now you can query the dungeon directly with the hero entity.

  Ecspanse.Query.select(
    {Ecspanse.Entity, Components.Dungeon}, 
     for_ancestors_of: [hero_entity]
  ) |> Ecspanse.Query.one()
3 Likes

New release v0.6.0

Introduces projections, a way to build state models across entities and components.

This is meant as a tool for external libraries (UI), and especially for Phoenix Liveview.
The Livewiew can create a Projection with the needed state, then the projection server can push the projection struct to the Liveview every time it changes.

I intend to use it as the main way to provide data to the Liveview for the Orbituary game.

2 Likes

New release v0.7.0

For state consistency reasons, Projections are now updated at the end of the frame, after all systems have run. In v0.6.0, the projection updates were executed in parallel with the systems. This ensured better performance, but possibly inconsistent state.

The new release introduces also a small breaking change. The Projection on_update/2 callback, became on_update/3 and takes both the new projection state as well as the previous projection as second and third arguments. This is useful for checking diff.

2 Likes

Great library, thanks! It got me to start on a long pending project. I’m trying to emulate tabletop like war game and it’s going pretty well. There are a few issues/questions I have run into…

The biggest issue I have is that the game engine raises an UndefinedFunctionError when t (I think) elixir recompiles and I have to restart the app. I can open a GitHub issue if you’d like.

Also since my game moves slowly, LiveView instance that join don’t see anything until a projection updates. I know I can get the data right after I create the projection, but would it make the coding cleaner if projections always treated the first run as an update. Since they never got called any state is technically different…

Also none of the tutorials mention a server running multiple concurrent games, but I guess you can just handle that in code, like create a session entity and make all of the units in that game children? I wonder if that kind of scoping could be built in idk if there’s a more efficient way than the way I’m doing it.

Sorry that was a lot. Thanks again for the cool library! I love using elixir for the backend work and I’m using LiveView and BabylonJS for the front end.

1 Like

First of all thanks for using the library and all the feedback. Please do share the status updates for your project. As said above, I’m very curious about what people are building with the library. You could share screenshots, or really anything.

Now, let me try to answer your questions.

The biggest issue I have is that the game engine raises an UndefinedFunctionError when t (I think) elixir recompiles and I have to restart the app. I can open a GitHub issue if you’d like.

Yes, please, if you can open an issue and document as much as possible. I’m not aware of the issue. But just as a potential cause, you may have to restart your server if you change the code in your components or systems. The loop runs in a GenServer and that server needs to be restarted to pick up the changes.

Also since my game moves slowly, LiveView instance that join don’t see anything until a projection updates. I know I can get the data right after I create the projection, but would it make the coding cleaner if projections always treated the first run as an update. Since they never got called any state is technically different…

Good idea! I will make this update.

Also none of the tutorials mention a server running multiple concurrent games, but I guess you can just handle that in code, like create a session entity and make all of the units in that game children? I wonder if that kind of scoping could be built in idk if there’s a more efficient way than the way I’m doing it.

I totally understand that. It was a tradeoff I had to do at some point during the library development process.
Allowing multiple Ecspanse servers to run in parallel wouldn’t have been that hard. However, all the external communications with the Ecspanse would have to provide at least the name of the server they refer to. My initial approach was to issue a token with encoded server data when starting the server.
But then you would have to provide that token for every query, event, or projection, basically for all external interactions.
So I decided on a simpler API to the detriment of this functionality.

That being said, I think your session entity parent is the way to go.

Thanks again, and looking forward to your game updates.

New release v0.7.2

  • c:Ecspanse.Projection.on_change/3 is called on Projection server initialization.

Thanks @stwf for the suggestion.

For whoever is interested, I’m invited to this Elixir Wizards podcast episode discussing ECS and Ecspanse: ECS / Game Development with Elixir vs. Python, JavaScript, React with Dorian Iacobescu & Daniel Luu | SmartLogic

6 Likes

New release v0.8.0

Refactors projections. There are breaking changes. Please see the changelog.

The main change is about introducing the Ecspanse.Projection struct

  @type t :: %Ecspanse.Projection{
          state: projection_state(),
          result: projection_result(),
          loading?: boolean(),
          ok?: boolean(),
          error?: boolean(),
          halted?: boolean()
        }

  defstruct state: :loading,
            result: nil,
            loading?: true,
            ok?: false,
            error?: false,
            halted?: false

This is somehow inspired by the Phoenix Liveview AsyncResult struct.

The struct is wrapping the implemented projections structs and manages their state.

During last week I found myself embedding state fields in the projections structs, so I can then decide, in the client, if the projection is ready to be used or not.

This can now be achieved with code like:

<div :if={@enemy_projection.loading?}><.spinner /></div>
<div :if={@enemy_projection.ok?}>
  <.enemy enemy={@enemy_projection.result} />
</div>

OK. Finally got something I’m ready to show.

My game is an attempt at a Kriegsspiel. A 19th century wargame that emphasized realistic issues that came up in battle. Mine at least will try to take into account proper size scales, lines of supply and communication, how long orders take to arrive, and be executed, etc

I’ve got the hex map editor working and have basic movement and some commands working. Next up will be line of sight for identifying enemy units, and Scouts bringing back delayed observations of enemy units. That sort of thing. Then battle!

So it’s been fun and a good learning experience if nothing else! I’ll be happy to open up the code if there someone other than me interested in this sort of thing…


Steve

14 Likes

This is REALLY amazing!

So, is the state of the hex map map “persisted” in the backend with Ecspanse?

I don’t even know where to start. I have so many questions :slight_smile:

I suppose the map itself is BabylonJS. The UI around Liveview? And then from Babylon to the server, you communicate something like:

BabylonJS → LiveviewJSHooks > Liveview > Ecspanse Events > Ecspanse Systems logic?

Or do you use some different architecture?

I am looking forward to seeing your next updates. And if you have anything deployed and need some play test, I would be happy to give it a try.

Thanks!
So yes. The map editor part stores the maps in Postgres. But I export them to JSON files to load in the game. They are loaded into Ecspanse and stored globally so multiple games can use them and not require separate copies.

And yes the map is BablylonJS and LiveView for the rest. I’m trying to keep the BabylonJs stuff as simple as possible for the game. It just renders the maps, knows how to draw units, and reports on what items get clicked on.

LiveView handles the UI state, and sends orders into Ecspanse, and when I get notifications from the Projections I just push the unit data into BabylonJS through a LiveView hook.

I will keep you in the loop for sure. Just couple of more features and I will work on getting it deployed… same old story lol. Thanks for the library and support!

4 Likes