Mass: specialized processors vs fewer larger processors?

I’m relatively new to ECS, and I’m currently working on a project where I’m trying to move a significant part of the simulation and gameplay into Mass.

I’m running into an architectural question: to keep responsibilities separated, I’m ending up with multiple specialized processors. For example: Movement → Behaviors → Collision → Responses → Destruction/VFX.

The concern is that some of these processors need to iterate over essentially the same entities/chunks again.

For those of you who have used Mass extensively, especially on larger projects, what approach has worked better in practice?

Do you prefer smaller, specialized processors even when that means multiple passes over the same entities, or do you tend to combine related work into larger processors to reduce the number of iterations?

I understand that sequential iteration over contiguous data can be relatively cheap, and that cache behavior, data access patterns, dependencies, and parallelization may matter more than simply counting iterations. But I’d be interested in hearing where people with real Mass experience have found the practical balance between modularity and performance.

Thanks in advance.

I would start with a few specialized processors, not one giant one, and only merge them when you can see the cost.

Mass already splits work by fragment and by phase, and processors in the same phase can run in parallel. The cost you are worried about is walking the same chunks twice, which shows up as cache-missing time rather than as “too many classes”. A single processor that does Movement, Behaviors, Collision, Responses, and Destruction in one pass usually ends up worse, because those stages need different fragments and you want the phases to overlap.

A split that has worked for me:

- Movement writes the next position and velocity.

- Behavior reads velocity and writes a target or state tag.

- Collision reads the new position.

- Destruction and VFX only run when a fragment flag is set.

Do not put destruction in the same iteration as movement. Gate it on a fragment such as HasDestructible or a response tag so it only touches chunks that changed. Spawn VFX on the response phase, not inside the movement loop.

If two processors still walk the same chunks every frame, check whether they can sit in the same phase. Same-phase processors run in parallel and must not depend on each other’s writes. If the second one needs the first one’s output, it belongs in a later phase. That second pass is correct.

I would look at the Mass debugger timing overlay before merging anything. If a processor is a small slice of the frame, splitting is not the bottleneck. If one phase is the bottleneck, merge only the processors that share fragments and do not need to overlap.

One thing to avoid: a responses processor that both decides the response and applies destruction, while movement also applies damage. That double-applies and forces both to iterate everything. Let movement only write a hit fragment, and let a later processor resolve it.

I am not sure a single processor is ever the right default on a project this size. Specialized processors with clear phase boundaries a

re easier to debug.

1 Like

Thanks for the reply! To add some context to my question, I’m developing a data-driven projectile system using Mass.

In my case a projectile can have different combinations of features: ballistic movement, homing, collision, lifetime, exhaust/VFX, impact responses, destruction, etc. The projectile definition determines which fragments/features the entity gets.

As I’ve been separating responsibilities, I’ve naturally ended up with several processors: movement/behaviors, collision, responses, VFX, destruction, etc.

Some of these form a pipeline, so the same projectile may legitimately go through several processors in one frame:

Movement → Collision → Response → Destruction/VFX

With potentially thousands of projectiles, my concern is the tradeoff between keeping these systems specialized and cache-friendly versus repeatedly traversing the same chunks/entities several times per frame.

I’m assuming Mass queries/archetypes should keep many of these passes restricted to only relevant entities, but some stages inherently operate on much of the same population.

For this kind of workload, would you generally keep those stages as separate processors, or combine tightly related stages when they operate on essentially the same fragments/entities?

My advice is to use smaller, specialized processors. This has a few key reasons for smaller processors.

  • Smaller processors can be made to require fewer fragments which can lead to less contention and an increased ability to parallelize.
  • Larger processors tend to do multiple things. Those multiple things could be their own processors.
  • Smaller processors are easier to understand exactly what they do.
  • Smaller processors lead to less branching which can become expensive.

The more you can make your Mass code parallelizable and thread-safe. The faster it will run. You can also look at having separate entities for the different functions of your units rather than one monolithic entity. I would recommend that you watch the Unlearning OOP talk given at Unreal Fest Chicago which shares out some practical tips for using Mass from a dev who has worked on several game projects in an ECS.

-James

2 Likes

Thanks, @James.Keeling! I’ll definitely check out the talk.

I’m still relatively new to Mass and ECS in general and building this projectile framework has been a great learning experience. One of my biggest challenges so far has been representation.

Profiling with Unreal Insights showed that EndVisualChanges() averaged approximately 10.7 ms in one capture, suggesting a potentially significant cost in the representation pipeline. I tried disabling UMassUpdateISMProcessor to help distinguish the cost of updating ISM instances from rendering them, but doing so caused the representations to disappear.

My current hypothesis (which I haven’t fully verified): UMassUpdateISMProcessor appears to execute on the game thread. I suspect this may be related to how Mass Representation manages and updates shared ISM data, but I haven’t confirmed whether this is an inherent threading limitation or simply a consequence of my current implementation.

I’d be interested to know if I’m interpreting this correctly or if there are alternative approaches within Mass Representation that I may have overlooked.

I’ve since switched to Niagara’s Mesh Renderer, feeding projectile positions through Niagara Data Channels while keeping the simulation in Mass. I can now render 20,000 moving meshes with barley any cost in FPS in my initial tests. I haven’t performed a controlled, apples-to-apples comparison yet, but my first impression is that Niagara performs significantly better for this use case.

Thanks again!

The UMassUpdateISMProcessor runs on game thread due to our implementation of it using the shared data. I still would not expect to see 10.7ms for using it, but that could change based on number of entities being updated.

We have some teams looking at running Niagara from Mass via NDCs as well.

This is an interesting topic and I would be happy to look at the Insight capture. Can you create a new thread about this? You can also @ me in the thread. I don’t want to keep sending notifications to the original author, and it helps for folks who may be searching for similar answers in the future.

-James

1 Like

Will do.