It started about three years ago. I had this idea of building a multiplayer game with Elixir and Phoenix to play with supervision trees, GenServers and LiveView. Even the most famous game engines, were just some obscure names for me. It was a sci-fi-themed game with some battle royale flavor, where players would fight battle stations in a shrinking orbit. The graphics concept was more like an admin tool than a regular game.

It is worth mentioning I did not have any game development knowledge at that time.

Since then, the concept of the game changed a lot (more about it later) and so did my ideas about developing it.

From GenServer to ECS (Entity Component System)

The initial concept was a single GenServer in the backend that would push data to LiveView. As you may imagine, this got unmaintainable very quickly. Updating deeply nested structures, some of them as part of a list, was no fun. Also, in the mid-term, the solution would not be scalable, as the single GenServer would become the bottleneck.

The next attempt was one GenServer per player. Except for the fact that I still had to deal with deeply nested structs, it was very soon obvious that the game needs more structures than players. For example, the players would share an orbital node where they would meet (and fight), would find alien artifacts, etc.

This was about the time when I found out about the ECS concepts, and I become immediately interested. I read about it and decided I have to build my library based on some of the ECS concepts, but still using GenServers as entities. It took a while, but I created Faktor . And re-wrote my game backend to use it. And it worked. Somehow. But things got more and more complicated because of race conditions. As each entity was working in isolation, I had to implement conventions on which entities are allowed to call the others to maintain a consistent state.

I decided to delve deeper into how other ECS libraries function and their APIs. Bevy stood out to me as the framework that provided solutions to most of my questions and concerns. I went on to read the unofficial Bevy cheat book section on ECS and, about nine months ago, inspired by the bevy_ecs API, I decided to create an ECS library in Elixir.

Introducing ECSpanse

Before exploring Ecspanse, I want to clarify that I am not asserting that Ecspanse is the equivalent of Bevy written in Elixir. While Ecspanse draws inspiration from Bevy’s API and concepts, Bevy is a fully featured open-source game engine that is quickly gaining popularity. And I believe it deserves the recognition it’s getting.

To sum it up, if you have an idea for the next big indie game on Steam and you’d like to use ECS, I recommend giving Bevy a try. However, if you’re interested in exploring game development in Elixir, please continue reading.

With that said, I don’t want to dwell too much on the basic concepts of ECS. There are plenty of excellent articles and videos out there that can explain it better than I can. Given that this topic is becoming increasingly popular, there’s a good chance that you’re already familiar with the fundamentals.

Also, this article will not serve as a tutorial for Ecspanse. If you are interested in learning more about how Ecspanse works, I suggest checking out the Getting Started Guide and the official Step-by-Step Tutorial which provide a comprehensive overview of the basic concepts.

Instead, I will be highlighting some of the features of Ecspanse that I am particularly proud of. ECS has some general concepts, but each library has a unique way of implementing them. Ecspanse makes no exception. So, let’s begin!

🦄 Flexible Queries with Multiple Filters

One of the things I liked about Bevy is its powerful query system. In the system function definition, you can query components based on various criteria.

For Ecspanse, I went for a more Ecto-like approach. The Ecspanse.Query module offers the tools to query entities and components on multiple combinations of criteria. Let’s see an example for a Demo project:

Ecspanse.Query.select(
{Ecspanse.Entity, Demo.Components.Hull, opt: Demo.Components.Shield},
with: [Demo.Componets.Corvette],
or_with: [Demo.Components.Frigate, without: [Demo.Components.Railgun],
for: selected_enemy_entities
)
|> Ecspanse.Query.stream()
|> Enum.to_list()

The above select statement can be read as: “select the Entity struct, the Hull component, and optionally the Shield components (only entities that have all required components are returned), with either the component Corvette or the Frigate component where its entity does not have a Railgun component, for a list of selected enemy entities”.

The query is then piped to the stream/1 function that returns a stream of tuples, with the queried components as elements.

A result of the above query may be:

[
{%Ecspanse.Entity{id: 123}, %Demo.Components.Hull{value: 80}, nil},
{%Ecspanse.Entity{id: 456}, %Demo.Components.Hull{value: 99}, %Demo.Components.Shild{value: 20}}
]

The select function offers even more options. you can filter for the entity's children or descendants (we will discuss entities relationships a bit later). The Ecspanse.Query also offers many common convenience functions, easier to write than a full select statement.

🦄 Asynchronous Systems Execution

Depending on how the systems are scheduled in the Ecspanse.setup/1 callback, the systems would be executed either synchronously, in the order they were added, or concurrently.

Systems added with the Ecspanse.add_system/3 function are async. However, to avoid race conditions, you need to manually lock the components potentially modified by the systems.

The systems are then grouped depending on their looked components. And only systems that are not locking the same components are running concurrently in the same batch.

defmodule Demo.Systems.RegenerateShield do
use Ecspanse.System,
lock_components: [Demo.Components.Energy, Demo.Components.Shield]

@impl true
def run(frame) do
# system logic reducing the Energy and increasing the Shield
end
end

🦄 System Event Subscriptions

In many ECS libraries, most of the logic in the systems is traversing lists of components, performing different operations. With event subscriptions, Ecspanse loops through the list of the frame events and triggers the run/2 callback only for the events the system is subscribed to.

The run callbacks are executed concurrently, but again, in batches, to avoid race conditions. This time the batches are defined by the event batch key.

The event subscriptions make it then easy to pattern match the subscribed events struct in the run/2 callback.

defmodule Demo.Systems.SpaceJump do
use Ecspanse.System,
event_subscriptions: [Demo.Events.Jump]

@impl true
def run(%Demo.Events.Jump{player_id: player_entity_id}, frame) do
# the space jump logic
end
end

🦄 Dynamic Bidirectional Entities Relationships

There’s no tutorial I found about ECS to discuss an important topic: collections. And it seems there is no consensus on how to address them. Some libraries allow an entity to have multiple components of the same types, but most of them don’t, including Ecspanse. I even wrote this Reddit post a while ago addressing this topic for Bevy.

Let’s take an example. Our demo spaceship has 6PDCs (point defense cannon). Each PDC can fire independently and has its properties, like the ammunition amount.

The way to model this in ecspanse is through entity relations. The battleship entity can have 6 PDC entities as children. The PDC children can hold their components in different states. Both Ecspanse Queries and Commands offer dedicated functions to deal with entity relationships.

The Ecspanse entity relationships are bidirectional. In our example, that means that if we add a PDC as the battleship child, the ship will automatically be added as a parent of the PDC. The same is valid when removing relations.

This solves one of the annoyances of many ECS libraries when you need to remember to manually update those relations. In this direction, Ecspanse offers also a Ecspanse.Command.despawn_entity_and_descendants/1 function that allows terminating the full tree of children of an entity, when that entity is terminated.

🦄 Versatile Tagging Capabilities

Ecspanse offers a parallel query model, with component tags. This addresses also the collection issue, as in the section above, but this time for components.

The components can be tagged either at compile time or at runtime when creating them. After this point, the component’s tags cannot be edited or removed.

Let’s think for example of the ship inventory: air supplies, water, food, etc. If those are simple enough structs not to require their entities, they can be modeled as the ship’s components.

defmodule Demo.Components.Air do
use Ecspanse.Component,
state: [quantity: 10000],
tags: [:inventory]
end

defmodule Demo.Components.Water do
use Ecspanse.Component,
state: [quantity: 1000],
tags: [:inventory]
end

But by tagging them, now we can easily find all the ship’s inventory items.

inventory_item_components = 
Ecspanse.Query.list_tagged_components_for_entity(ship_entity, [:inventory])

What kind of games can I build with ecspanse?

As said before, Ecspanse is just a state management framework, not a game engine. It has no idea about rendering, physics, input, etc. So you will need some additional tools to create something meaningful.

However, I’m not thinking about any fancy 3D game. But mostly low-graphics <> mid/high-complexity kind of games. I can imagine some terminal-ui RPGs like Caves of Qud or Cogmind, rendered with Ratatouille or maybe with Scenic.

Unfortunately, there are not many straightforward ways to generate native applications with Elixir at this point.

The other option I see is Phoenix and LiveView for multiplayer browser-based types of games.

Can I see any game built with Ecspanse?

The Ecspanse Demo

The EcspanseDemo app is the one used for the official tutorial of the library. It is not really a game, but it is a working application using Ecspanse. It can be “played” in Elixir Livebook. You can find details on how to set it up in the GitHub Repo.

I’ve Seen Things — multiplayer browser game

It’s the game I coded while building the Ecspanse framework. You can find its code on GitHub.

I’ve seen things

The game is currently deployed on fly.io and can be played here.

It’s a rock-paper-scissors kind of game where players can use various defensive and offensive abilities of their spaceships to counter the enemies' actions. You can compete against other players, or against the 100 spawned bots. Spoiler alert: don’t take the game too seriously. It’s not balanced at all, and the bots are just dumb.

In terms of code, it has a lot of rough edges. So don’t consider it a set of best practices, but rather a display of the framework functionalities. The game was built along with the framework, so many parts were just experimental and so they remained.

What’s Next?

Going back to the beginning of this article, I mentioned a multiplayer game and an orbit 😄. This leads us to “Orbituary”!

Orbituary

This is the Elixir/LiveView game I am working on and it’s still in its very early development phase. The current backend is written using my previous library, Faktor.

Orbituary gameplay

The next logical move would be to re-write the Orbituary backend using the Ecspanse library. However, that’s not all. With the new framework, there are now numerous possibilities to expand the game’s scope. While the specifics of the game are still being refined, I hope to write a follow-up article about Orbituary and Ecspanse in the near future.

Until then, I’m eager to read your genuine feedback on Ecspanse, as well as showcase any projects you’ve created with it. And, why not, maybe seeing even some pull requests to enhance it further.

ECS Alternatives with Elixir

If you are just starting with ECS and you find Ecspanse too complex, you can try the ECSx library. It has a simpler API but lacks some of the Ecspanse flexibility. This article offers more details about it.

Ecspanse Links

--

--

No responses yet