3 min read

7DRL25 Postmortem Series: ECS is a dynamic scripting language

This will be a series of shorter posts on topics we found interesting in hindsight after this year's 7DRL. It might be all over the place.

Mika here! I love using ECS for roguelikes, they create an easy win for systemic, emergent gameplay. Last year, I've done the 7DRL in Rust, using Bevy's wonderful ECS to handle everything and it was a joy. Notably, I was doing everything in Rust, losing time fighting simple type issues and compile times. I didn't have a scripting langauge. As the game grew, I found that I needed tools to sort out which system was touching what! Basically, I've created a diverse enough ecosystem that I was starting to lose track.

To fight that, I've already created bevyrly, a tool to query the queries! It's a vscode plug-in, and is limited by bad syntax and a very brutish inspection of the code it's filtering. Also, works only for bevy. There's definite room for improvement.

This year, we went with C# and Lua, learning from last year. When doing the calculus on what goes where, we decided to put the windowing and inputs into C# and everything else, including the ECS into Lua. This enabled us to quickly be done with C# and not touch it at all when making the game itself. Due to a few bad habits, we didn't save a lot of time, but that's going to be a different post-mortem piece in the series...

At any rate, the ECS in Lua idea was going great, until we got to day 4 and we started to truly work completely separately, and without much inter-communication. Stuff needed to be done. What I noticed then was that two people, used to each other and their peculiarities, were deciding, arbitrarily, on the spot, between either designing some piece of code to be ECS-centric (I'll turn this into a component and have it be reached by these systems) or, for lack of a better word, script-centric (creating auxiliary tables in Lua, mapping the relations through those, and patching their usage through functions that would then be called within some running system). It seemed like we had a mess on our hands, but both approaches seemed okay on their own. It was having them living together that was problematic, but also the spark for this whole topic: I couldn't suddenly notice the benefits of neither ECS or Lua as much as before – they were melding into each other's usability profiles.

From Rust's or C#'s perspective, an ECS framework gives us entities, which are runtime-defined variables of an open type. Components are mixin types that carry associated field values. Systems are type-based mapping mechanisms that basically pipeline pieces of work over several types one into another. It's as if your scripting language was a pure functional language that encoded everything in union types and direct maps and reduces over those. Importantly, it basically adds a layer of a weak type system that translates into the native one on top. The boundary conditions for this translation require strict regulations, giving us the nice limitations that ECS is known for, but it's important to realize that this is secondary and easy to lose if you don't pay attention. Just the same way it's easy to not write optimized Lua if you don't pay attention.

Using Lua and ECS together felt more confusing because they weren't mixed enough, I reason. Next time, I want to try having the ECS in Lua be the way to go, so that we default to it and use the laxness of Lua only to push things that are hard to do in this regime. Having this knowledge (which seems not really useful at the moment), however, also outfits us for some trickery going forward: I see it opening up the field of ECS ghosting, picking which operations to run in which regime (and even language) while all the while not changing how we write the code. These things being equal should mean that we can substitute them with no loss. It may even hide the key to why I get lost in ECS code as it grows! I'm very much looking forward to where this takes us!