ECSx: A New Approach to Game Development in Elixir

A set of gaming headphones on a desk in front of an LED-lit desktop monitor.

Why Not Elixir for Gamedev?

If you’ve been around the Elixir ecosystem for a while, you’ve probably noticed that game development is not nearly as common as in other, particularly object-oriented, languages. Looking into some of the threads on Elixir for gamedev, you’ll likely see a few common themes:

  • Mathematical operations are slower than in other languages
  • Real-time updates are slow due to message-passing immutable data structures
  • Lack of game-oriented libraries and frameworks

To address the first point, it’s true that pure math is not the BEAM’s strong point, and other languages will outperform it in that regard. However, we do have some tools at our disposal which can mitigate this disparity.

The first is Native-Implemented Functions (NIFs), which allow developers to run specific bits of code in a lower-level language, such as C or Rust. The second is the Nx project, which can perform numerical computation with specially optimized compilation methods, providing massive performance benefits over naïve calculation.

By leveraging these options where appropriate, we can achieve performance acceptable for many game applications.

As for the second point, Elixir does have some potential issues when storing game entities (such as a Player Character) as structs.

For example, if you have a storage process and a worker process, sending the struct back and forth between processes will create a new copy onto the process heap each time. If, instead, you do the work in the same process as the storage, you won’t be able to benefit from parallel processing over multiple CPUs.

A better solution is to minimize the amount of data sent from one process to another by architecting our application in a way that allows a character’s attributes to be read and written individually. There are several patterns we can use to accomplish this, one of them being Entity-Component-System (ECS).

This brings us to our final point, which is the lack of game-oriented libraries and frameworks for Elixir. To help remedy this issue, I’m happy to announce a new Elixir framework, over one year in the making, called ECSx. The goal of this project is to make it as easy as possible for developers of all experience levels to utilize ECS architecture for building real-time games and simulations in Elixir.

What is ECS?

ECS applications are made up of three parts: Entities, Components, and Systems.

Entities are the objects that make up the world. But instead of representing the objects as structs and working with those structs directly, ECS Entities are represented as a simple ID and nothing more.

To model the attributes that set Entities apart from one another, we create Components, which hold, at minimum, the Entity’s unique ID, but also can store a value. For example, if you’re running a two-dimensional simulation of cars on a highway:

  • Each car is an Entity, represented by an ID, such as 123
  • If the car with ID 123 is blue, we create a Color Component associating ID 123 with value "blue"
  • If the same car is moving west at 60mph, we might model this with a Direction Component with value "west", and a Speed Component with value 60
  • The car would also use Components such as XPosition and YPosition to locate it on the map

Then, once your Entities are modeled using Components, the game logic that operates on them is separated into Systems, each handling the updates for a different Component type. For example:

  • Any entity with a Speed and Direction Component must be moving, so we make a Movement System to read those components and then write updates to the X and Y Position
  • The Movement System reads all the Speed and Direction Components, calculates how far each car has moved since the last server tick, and updates the entities’ XPosition and/or YPosition Components accordingly.
  • If an entity does not have Speed and Direction Components, it is ignored by the Movement System entirely.
  • The Movement System doesn’t care about other Components, such as Color, so those values are never read or touched, avoiding unnecessary memory and computational work.

Implementing ECS in Elixir

Building an ECS application can be split into two architectural tasks—storing the Component data and running the System logic.

Since Component data will be read and updated many times per second, we need storage that is fast, even as the quantity of Components grows.

Fortunately, we have Erlang Term Storage (ETS), which can store large quantities of data while providing constant-time access for both reads and writes. The ECSx framework leverages this by automatically creating an ETS table for each Component type you define in your application, and providing a convenient API for access. To read a Color Component for Entity 123, simply call MyApp.Components.Color.get_one(123). To change the color to red, call MyApp.Components.Color.update(123, "red”). Of course, you can feel free to alias the module for a more elegant Color.get_one(123).

For running Systems, we have two concerns—running the code n times per second (where n is the tick rate of the server), and ensuring that multiple Systems don’t try to update a Component at the same time, causing a race condition which could lead to corrupted data.

Both of these concerns are handled particularly well by GenServer. In the ECSx framework, a GenServer process will run the logic for all Systems serially, triggered by consistent “tick” messages sent from a separate timer process.

ECSx Generators

One of the main goals of the ECSx framework is to make development as fast and simple as possible. To achieve this, generators are provided for creating all the files you’ll need for a complete backend server.

$ mix ecsx.setup will create the full ETS/GenServer implementation detailed in the previous section.

$ mix ecsx.gen.component [module name] [data type] will create a new Component type, ready to use with the ECSx.Component API.

For example, running $ mix ecsx.gen.component Color binary will allow you to immediately start adding colors in your app with MyApp.Components.Color.add(123, "blue”).

$ mix ecsx.gen.tag [module name] will create a new Component type with no value, such as IsPlayerCharacter or Targetable. A simplified API will be provided with functions such as add/1 and exists?/1.

$ mix ecsx.gen.system [module name] will create a new System module with a run/0 function which will automatically be called once per server tick. This is where you’ll write the logic that powers your application.

With these few simple commands, the whole data layer of your application will be set up in minutes, and you’ll be ready to start implementing game features into your System modules.

LiveView Makes Frontend Simple

Once you’ve got your application backend running, your next consideration will probably be to set up a frontend for players to observe and interact with the game world. While you can feel free to use any technology of your choice with your ECSx backend, the simplest and most versatile approach is to build a Phoenix LiveView.

Using Phoenix LiveViews, you can continue writing Elixir code, using familiar GenServer patterns, to create web frontends with HTML graphic interfaces such as SVG and Canvas. With the advent of LiveView Native, choosing to use LiveView won’t limit you to just web, either.

ECSx Components are fully accessible from the LiveView process using the standard API described earlier. However, writing to the tables from LiveViews could cause race conditions, so ECSx provides a module called ClientEvents, with another API for capturing player input or other events from clients, such as a new player connecting, button clicks, key presses, etc. These events are then handled serially by a System, where you can safely implement logic based on the event type and any included metadata.

ECSx LiveDashboard

Another tool in the ECSx framework is a custom LiveDashboard page for monitoring your game’s performance. If you’re using Phoenix in your application, LiveDashboard is already included, so you only need to add a few lines of config to use this page. The primary features as of this writing are reporting runtimes of individual Systems, how much time is used per tick overall, and inspecting the values and sizes of Component tables. This information is regularly updated in real time based on your LiveDashboard refresh rate.

Here are some examples of use cases where the ECSx LiveDashboard page might come in handy:

  • You’re not sure at what tick rate your server should run. Of course, higher is better, but you don’t want it to be so high that your Systems cannot keep up. If you check the dashboard and see that the overall load during gameplay is only 10%, you know that you could double the current tick rate without causing performance issues. Maybe later your game gets very popular and sees an increase in players online during the day. You check the dashboard and notice that your overall load is well above 50%, so now the tick rate might be too high for your current hardware; reducing the tick rate now could improve performance and ensure stability.
  • Perhaps your overall load measurement is high, but instead of reducing the tick rate, you would prefer to optimize System logic. Looking at the individual System runtimes, you see that one of your Systems takes longer to run than all the others combined. This would be the ideal place to start your optimization, either by finding a more efficient algorithm, running calculations in parallel, or even handling the calculations via NIF or Nx.
  • While checking the Components page of the dashboard, you notice one of the tables is much larger than expected. Clicking on that table brings up a list of all Components of that type. You notice most of these Component values indicate they belonged to projectile entities that are no longer in play. When you check the System logic for removing projectiles after collision, you see that all the projectiles’ Components are being removed except for one. This unintentional oversight, if gone unnoticed, would cause the table to continue growing infinitely. By checking the dashboard, you were able to quickly identify the problem before it was deployed to production.

Conclusion

Hopefully, at this point, you’re getting interested in trying out ECSx to bring your own game ideas to life. The fastest way to get started is to follow the official tutorial, which will walk you through the basics of both back-end and front-end development with a sample 2D game, which you can adapt and expand to fit whatever theme you’d like. The optional LiveDashboard page is available here, and if you’re interested in contributing to ECSx, the repo can be found here.

I’m very excited to see what kinds of games the Elixir community will create as we continue to grow this part of the ecosystem!

When you’re ready to find out the full extent of what Elixir can do for you—and your business—get in touch with DockYard.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box