Jecs - Optimizing declarative scene graphs with ECS

MXQqzqa

License: Apache 2.0 Wally

Jecs

Just a stupidly fast Entity Component System

  • Entity Relationships as first class citizens
  • Iterate 800,000 entities at 60 frames per second. (More if you inline via query:archetypes)
  • Type-safe Luau API
  • Zero-dependency package
  • Optimized for column-major operations
  • Cache friendly archetype/SoA storage
  • Rigorously unit tested for stability

For more information, check the documentation!


The debugger in the picture is a 3rd party library called jabby, get it under the assets page

Quick Example

local jecs = require("@jecs")
local world = jecs.World.new()

-- Define Position and Velocity components
local Position = world:component() :: jecs.Id<Vector3>
local Velocity = world:component() :: jecs.Id<Vector3>

-- Define the Move system
local function move(dt)
    for e, pos, vel in world:query(Position, Velocity) do
        pos += vel * dt

        world:set(e, Position, pos)  -- Update the position in the world
    end
end

-- Create an entity with Position and Velocity components
local entity = world:entity()
world:set(entity, Position, Vector3.new(1))
world:set(entity, Velocity, {x = 1, y = 2})

-- Call the Move system
move(1/60)

More examples can be found here. I specifically recommend looking at the examples folder that show usage of the API for different scenarios. The demo is a good overview of how a cohesive setup could look like.

Relations

Jecs provides first class entity relationships which are useful for scene hierarchies, graphs, physics joints and more in a safe, fast and ergonomic way working with the ECS rather than fighting against it.

Relations can be both stateless or have associated data, like spring or joint strengths.

Relations provide three categories of benefits:

  • Ergonomics: query for entities with specific relation types
  • Correctness: enforce logic invariants (such as “each child must have a parent” or “each child can only have at most one parent”). In addition, relations are cleaned up on deletion.
  • Performance: iterating children is cache friendly and does not require random access.
local world = jecs.World.new()
local pair = jecs.pair
local Name = world:component() :: jecs.Id<string>
local Position = world:component() :: jecs.Id<Vector3>
-- Regular entities are tags (zero-sized-types)
local Star = world:entity() 
local Planet = world:entity()
local Moon = world:entity()

local sun = world:entity()
world:add(sun, Star)
world:set(sun, Position, Vector3.one)
world:set(sun, Name, "Sun")

	local earth = world:entity()
	world:set(earth, Name, "Earth")
	world:add(earth, pair(ChildOf, sun))
	world:add(earth, Planet)
	world:set(earth, Position, Vector3.one * 3)

		local moon = world:entity()
		world:set(moon, Name, "Moon")
		world:add(moon, pair(ChildOf, earth))
		world:add(moon, Moon)
		world:set(moon, Position, Vector3.one * 0.1)

See the guide or examples for more information.

What is an ECS?

Entity Component System (ECS) is a design approach that promotes code reusability by separating data from behavior. An ECS typically have the following characteristics:

  • It has entities, which are unique identifiers
  • It has components, which are plain datatypes without behavior
  • Entities can contain zero or more components
  • Entities can change components dynamically
  • It has systems, which are functions matched with entities that have a certain set of components.

Who is using an ECS?

A number of notable projects that you may know utilize it in some way include:

Is ECS Fast?

A common question is: How does cache locality and hardware optimization matter in a language like Luau where all userdata and lua tables are pointers? You might assume that hardware optimizations, such as laying out data contiguously, would be irrelevant. However, benchmarks show that memory access patterns are still incredibly important. I’ve benchmarked several ECS implementations against jecs, and despite degraded locality conditions due to high memory usage, jecs consistently performs well. This is because it fetches archetypes into CPU caches which minimizes random access, whereas other implementations struggle during naive iterations because their data in non-contiguous structures.

For a more isolated example, you can test the difference between linear and random access orders in a synthetic benchmark. Under normal conditions, linear access can be nearly an order of magnitude faster than random access. The difference becomes even more pronounced under degraded locality conditions. In my benchmark, I used tables as objects since the TValue on the Lua stack points to heap objects, which in turn point to heap hashmaps. These layers of indirection extrapolate the cost of randomized access.

image

The key takeaway is while Luau doesn’t guarantee contiguous memory allocation for tables, cache locality can still improve when tables are created in bulk which may also lead Luau’s memory allocator to place them closer together. If pointers to these tables are stored contiguously in a parent table, the CPU can load their addresses efficiently, minimizing cache misses. Even though Luau objects are reference types, modern CPUs benefit from accessing nearby memory, and prefetching can help load data more efficiently. Iterating over tables in a predictable order, especially in systems like ECS, can therefore optimize memory access and reduce latency.

Another advantage of ECS is that it allows you to process entities with specific combinations of components in bulk, reducing the need for branching logic. This is a significant improvement, as it minimizes unnecessary checks. It can also help with memory fragmentation by keeping entity data smaller and more compact which is really beneficial for the garbage collector.

However when discussing performance optimization, it’s crucial to approach any proposed solution with a healthy degree of skepticism. While ECS can be an efficient way to store and update a scene graph — allowing better CPU utilization — it won’t automatically fix all performance issues as it is not a silver bullet.

Profiling is key — ensure you profile your game to identify actual bottlenecks before making sweeping changes.

Things that ECS implementations generally excel at are querying and iterating through entities in a linear, cache-friendly manner, or dynamically adding or removing components at runtime. However, they are generally less efficient for operations requiring complex data structures, like binary trees or spatial indexing, where specialized solutions are more appropriate. Knowing the tradeoffs of an implementation and leveraging its design ensures you get the most performance out of an ECS.

FAQ

Sander Mertens has already done a terrific job of compiling answers to frequently asked questions: https://github.com/SanderMertens

More resources to read: https://github.com/SanderMertens/ecs-faq?tab=readme-ov-file#Resources

Where to get help with jecs?

You can find a thread on jecs in the Roblox OSS discord server, or open an issue on the github repository.

31 Likes

Can we have a full tutorial on this?

Check out the get started guide!

1 Like

This is a really great ECS module. But I do wish for it to have a lot of documentation about using this module as its finest

Absolutely, what kind of things would you like to see documented further?

1 Like

Is there any Roblox game which is released and playable right now that uses this (or just ECS in general)?

Jecs may be fast, but is it faster than making games normally? I’m also confused on how this workflow is more efficient or “better” than the more traditional way for any type of game.

It is generally beneficial for the CPU but the benefits may be less noticeable in some games.

If you want to consider using ECS, I suggest looking at it for its organizational benefits rarher than just performance.

2 Likes

All of it.

Joking, but if you have time to spare. Could you make a real game example like the demo you made, but it is complete for people to learn from and that would be pretty great since most of the roblox developers nowadays are using OOP and Event Driven pattern. So this module might give them a chance to learn what is ECS and how Data Driven pattern works. If used correctly, they can make a whole new world inside Roblox.

Sure I might work on a tower defense game. No promises though. Generally I don’t believe there is a correct way of writing games.

2 Likes

only ten replies? feel like this should’ve been posted as soon it was a topic on OSS
using an ECS is a gem after suffering so much due to the limits OO. instantly understood that it is what i’ve been looking for so long after understanding most of the concepts.

enjoyed watching the unlisted GDC Overwatch presentation mentioned in the post a month before. Explains the benefits, singleton usage and even networking with ECS.

on my side, using it in my main project, finally away from the hard limits of OO. entities don’t have to be 10k lines classes with each handle logic for different type “accessory”, custom r6 inverse kinematics handled with queries on frame, entity_admin allows for playback demos to be possible…

thank you Markus and all other contributors for JECS. Matter seemed a bit too bloated at the time and… unnecessary abstract i guess? for the ECR mentioned in the benchmark - missed it, never heard of it.
and this. this just hits the spot.

I made this post only after I had decided that it was completely ready for production usage. I have now used jecs in my own games and seen many testimonies from other games in development that there has not been any jecs-related errors.

1 Like