[January 2025] Full ECS Stack Review
Hello everyone. It is time for another one of these to kick off the new year.
As a reminder in case you aren’t familiar with this format, I periodically make big posts like this discussing my thoughts on the Unity ECS ecosystem. This feedback is mainly addressed to the engineers and other staff at Unity, though everyone is welcome to read it.
Please note that I will make a lot of criticisms in this post. I am criticizing the technology and the decisions made, not the people behind them. I do not fault any individual staff member for anything, as there are a lot of factors at play. My intent is simply to point out how things could be better, because I want to see Unity ECS keep pushing boundaries. On the contrary, complements are directed at staff. Good things were built by smart people after all.
This review ended up being later than I originally planned. Last time back in June, I did a thing where I provided a positive, a negative, and a question. None of the questions were directly answered in that post, so I am not going to do that format again. Instead, I’m going to try to review each category more broadly, looking at Unity’s offerings as a whole from the perspective of a heavy ECS user.
And just like before, I’ve gone ahead and updated my wishlist, if you want actionable items.
General Feeling
Back in February last year, I made the comment that Entities 1.X feels abandoned. In June, I changed my stance and commented that DOTS felt understaffed. In hindsight, I believe I was mostly right about both, but I also now know a bit more about the circumstances and can see things with a more level-headed perspective.
Back then, I was very frustrated with the state of Unity, and the direction it was going. RTF made Unity feel like a possible dead-end platform, and it seemed like every new ECS feature only made things worse for me. I had no idea what kind of timelines to expect, nor any signs that things would actually get better. Around Unite, a lot of that was rectified. RTF was completely undone, and Unite gave me much more insight into the road ahead, and many of the conversations afterwards have been good.
The other thing I think I’ve come to realize (though I could be wrong) is that the DOTS team is a lot smaller than it used to be. There was a post a few years back where it was stated the DOTS team had 60-80 people. I suspect that number is probably closer to 20 today, most of whom might be working on Game Object integration type projects and not on the packages themselves.
But in spite of that, there’s one thing you have gotten right. And that’s that you have made the packages today very stable. That’s allowed me to be much more productive over the last few months, not having to worry about fighting compatibility fires or being cautious of a new Entities 2.0 preview being dropped at any moment.
There were also a fair number of improvements that happened last year that I cared about. Many of them were buried inside Unity 6, so I didn’t realize they were there back in my June review. As for the future, I like the concepts proposed at Unite, but today’s tech is fundamentally incompatible with some of them, so forgive me if I’m a bit skeptical of there being a smooth transition.
Is Unity ECS where it should be right now? No. Is it something I can easily recommend to people? Not really. But right now, I’m at peace with the current state of things, and I plan to enjoy that peace while it lasts.
Anyways, it is time to dive into the features!
Collections, Jobs, and Mathematics
I’m grouping these together because they haven’t really changed since last time. For 99% of the time, I generally like them. I think they work well, and are stable. It is always just little issues that catch me off guard.
For example, why is there no proper inspector for half
? Why does NativeStream
keep count of write calls instead of bytes when reading back? Why is there no single-threaded scheduler for IJobParallelForDefer
?
But I think my biggest gripe right now is job scheduling overhead. Even just calling .Run()
on a job has multiple microseconds of overhead. And this overhead adds up a lot faster than you’d expect in NetCode prediction of culling loops with lots of shadow-casting lights. JobHandle.CombineDependencies()
is also really bad sometimes, which means I often have to consider the tradeoff of better worker thread utilization or better main thread latency.
But in general, using these packages and APIs are a very pleasant experience.
Burst
I’ve read some of the recent blob posts surrounding Burst. I was actually surprised to learn that every job and function pointer was compiled completely independently, without any function sharing. While yes, that’s pretty awful, it also means that Burst’s raw throughput is quite fast!
Right now, I’m waiting for the next Burst release to hopefully fix explicit interface method implementations. But besides that, Burst hasn’t really given me any issues lately. And I push it fairly hard.
I don’t know if this is a job thing or a Burst thing, but it would be really nice if there was a way for the main thread to be notified when an exception occurs in a job in the editor. I like to be able to turn off all my ECS systems when the first thing that goes wrong is detected. That way, I don’t get a mess of error messages and make a mess of memory allocations. Plus then I can inspect the ECS state in the editor and see what happened. But right now, this only works on the main thread, and not in jobs.
Entities
It is hard to identify positives about the entities package that have happened this year outside of the bugfixes. There have been a good number of them, but honestly, none of them stand out as revolutionary. This core is stagnant, similar to Collections, Jobs, and Mathematics. But unlike those other packages, this one isn’t as fully cooked.
Back in February last year, I called out that Entities had a bunch of API holes that were causing developers to use asmref or modify the package to be productive. Out of all the DOTS packages, Entities is by far the most targeted of these modifications. There’s just a lot of small issues. Missing shared component lookup APIs, missing RefRW
APIs for EntityManager
and for chunk components, missing dynamic buffer change filter flexibility offered to components via unsafe APIs, and those are just some of the gaps. I could list off just as many bugs and unnecessary performance overhead issues that overall make Entities feel not as good to develop with as it could be.
I wish I could be discussing how to eliminate the chunk occupancy concept so that people can better leverage entities of wildly different sizes. I wish I could be discussing how to eliminate all the restrictions to baking so that anything is possible while still preserving incremental correctness. I wish I could be hypothesizing how to batch multiple structural changes together via ECB, or define a new way to do reactive systems. But these are big items, and you don’t seem to even have the resources to improve the little things.
The most frustrating part is that I don’t know what to do about this. It would be extremely tedious for me to make a repro case for every single issue. Most of the time, I just see the issue while reading through the code while investigating if something is viable. It doesn’t help that every other category in this post has had good representation from staff here in Discussions, but there hasn’t really been anyone answering questions for Entities. And the very slow pace at which bugs are getting stomped out relative to when they are reported is not helping, especially when you do not accept pull requests.
How can I help make Entities be fully cooked? Do I need to spam you with bug reports? Do I need to fork the package and do everything myself? Do I need to start emailing and PM-ing specific developers?
And why are API gaps and bugs not getting fixed promptly in in the first place? I would have expected that with the progression towards Entities 2.0 and “Entities For All”, that you would be really trying to iron out a lot of these issues, so that there would be less pitfalls and gotchas before the tech reaches the greater masses. Is there too much red tape? Is Entities 2.0 on fire? Did someone refactor the entire package in the 2.0 branch and made semi-automatic backporting with git no longer viable? Is the package even being touched at all? Or is everyone in C++ land?
I just don’t understand what Unity is doing here. Do you?
Scenes and Serialization
I hear a lot of people complain about this area with DOTS. However, I’ve personally had very few issues this last year. With that said, I pretty much exclusively use subscenes, which is kinda crazy because I used to avoid those entirely. I’ve started using UnityObjectRef
for a lot more things. And one thing that bit me is that objects get duplicated when loaded in subscenes vs loaded in scenes via Game Objects. But I was able to work around that using project asset GUIDs.
I’m excited to see where the content pipeline upgrades are going, as the ideas sounded very good.
I especially appreciate that EntityRemapUtility
is fully public API. I was able to make really good use of that for my Unika scripting module. You actually broke the API after 1.0, but I’m not complaining, because it was easy to fix and made the API more consistent. I wish you were more willing to do this for “advanced” APIs.
I do think that there’s some potential scaling issues with EntityPrefabReference
or lots of subscenes. And I also think the content pipeline is not documented well enough. I’ve avoided dealing with those issues.
But if there is one thing I have a gripe with, it is the entity differ during incremental baking. It drops chunk components, and changes values without easy detection, which breaks a lot of reactive systems that need to update to the new values. It would be awesome if there was a way to force any entity that was rebaked to completely overwrite specific components and enabled states with whatever the baked result was.
Overall, this and the runtime entity inspector tooling are a big part of why I don’t build my own ECS solution. There’s a lot of work that has gone into all of this. I think many people underestimate the amount of pitfalls that are being circumvented within this tech. There are a lot of things that are surprisingly painless.
Transforms
I don’t really have anything to say about transforms since last time, since you haven’t touched it at all. Hierarchies are still inefficient and nondeterministic. You could fix that without breaking API. I don’t think you can improve chunk occupancy any more though without breaking API.
A really nice addition would be to allow bakers to specify a manual override mode that still allows child entities to be parented to them.
I’m extremely curious what you have planned for the new unified transform system. The more I think about it, the more challenging it seems. If you make a transform just a pointer into the engine, you’d have to make it an ICleanupComponentData
or else instantiation wouldn’t work. And then you’d still have to make touching transforms thread-safe. Plus, there wouldn’t be any cache coherency relative to chunks. But if you make the transform live in ECS, then the C++ needs to somehow access it, which could mean either the C++ calls directly into Entities utility methods, or ECS functionality gets duplicated in the C++. Plus, I’m curious how you will go about non-uniform scaling and world-space constraints.
Graphics
I’ve learned that the ECS graphics team barely exists. It is impressive that Entities Graphics is still somewhat supported and represented here!
I don’t really expect new ECS graphics features any time soon, since there is a plan to switch everything to GPU RD. I’m not convinced that’s a good idea, as the one thing Entities Graphics does very well is processing entities each frame with minimal data movement (minus material properties, which I fixed). GPU RD sounds like a recipe for lots of random accesses to make culling work. I’ll wait to see the final implementation though before I decide if I switch to it or keep my heavily-modified Entities Graphics stack alive.
I would like to see more things be friendly to BatchRendererGroup
. I don’t care if you decide to not make Entities Graphics support these things. I’ll do that for my framework. But it would be really cool if you could make projected decal shaders support DOTS instancing alongside the classical instancing. Lights would also be great to add an unmanaged path for. And I’d love to see all-around better batching APIs for high shadow-casting light counts.
Another request would be to make VFX Graph support a SampleBuffer
that uses ByteAddressBuffer
instead of StructureBuffer
. This would allow sparse uploading of ECS data for VFX Graph without having to write compute shaders for each type.
I’m really looking forward to URP/HDRP unification! I’ve been avoiding doing a lot of custom graphics stuff just because I don’t want to implement things twice.
Personally, I’m really happy with my modified Entities Graphics stack. I can achieve scale and complexity with it that is beyond just about everything else out there. BatchRendererGroup
is an absolutely amazing low-level API.
And one last thing, I know this was done for GPU RD, but the OnFinishedCulling
callback in BRG is really awesome in Unity 6! This was a big item on my wishlist that I hadn’t realized was in Unity 6 back in June. That was a huge win!
Physics
I know the physics team is hard at work defining a new unified API for Game Objects and Entities.
In general, Unity Physics is very efficient at cache-coherent algorithms. However, it is also wasteful in a lot of ways. The whole design of Unity Physics is to combine as much as possible into a single baked collider at bake time. But then, it generates event data for every single primitive in every collider pair (usually with full contacts) for any collider that needs triggers. This is overkill, and it shows up in the profiler. Perhaps it may be worth examining what real games need, and trying to make Unity Physics only do just that dynamically.
Besides the collision tolerance changes and early signs of TGS, not much has changed since my last review in June. But you all are still very active and supportive here for everyone else. That’s awesome to see!
Controllers (Character, Camera, ect)
Controllers are an area where Unity has done quite well. A lot of people I’ve talked to really like the Character Controller package. They like it because the out-of-the-box samples often “just work” and “feel good”. However, those who have had to extend it have often relayed frustration with that process, as callbacks involving a readonly
IAspect
require a few syntactical gymnastics.
I’ve also found the character controller to be a little on the heavy side. You can definitely reduce its archetype memory footprint without any loss of functionality by compressing all your options into bitfields. And I don’t think all the per-frame values need to be stored either. The character controller also makes a lot of spatial queries. This is understandable, but it is also expensive. I believe there could be ways to filter things better, and perhaps you could keep a local cache of nearby colliders around the character to reduce the amount of BVH traversal, since a lot of the queries are small areas around the character. None of this is really an issue for player-controlled characters, but it does sting in the profiler with high NPC counts or RTS units.
I’m looking forward to seeing what an IAspect
-free implementation looks like. Hopefully with the new implementation, the steps for composing a character controller from scratch will be documented a little better. It is kinda hard though, because many developers are easily overwhelmed by everything involved.
I’d also like to see better integration with Cinemachine. Which speaking of, I really like the direction Cinemachine has gone with 3.0. It makes customizing it much more accessible. However, I also don’t like how incompatible it is with the ECS world. I believe this should be a high priority given the direction Unity seems to be going. Sure, I can hack it, but something so essential shouldn’t need hacking.
Overall, Unity’s controllers are very good at doing out-of-the-box things in a way most people can understand them, while also being very flexible. There’s always room for improvement, but I generally hear a lot of positives in this area.
Are vehicle controllers still in development?
Audio
Historically in these reviews, audio has been at the top of my list of issues. There have been two issues. The first was subscene importing not working correctly with AudioClip
API. That has been fixed. However, the second issue was that something with audio was causing domain reloads to get stuck after code changes, causing Unity to deadlock. I’ve learned that the cursed combination is IAudioOutput
and AudioClip.GetData()
both being used during runtime. Remove either one, and everything works smoothly. I don’t really know why this happens, but after figuring this out, I tried a completely wild idea of updating a DSP Graph inside a MonoBehaviour
’s OnAudioFilterRead()
. I thought for sure there would be issues regarding DSP jobs not having the correct initialized resources and the AudioKernel
allocator not being safe. Instead, everything worked, and no more softlock issues.
Excluding IAudioOutput
, I think DSP Graph is less buggy than companion Game Object audio. I did a game jam recently with some friends, and they accidentally used a Unity audio source rather than my custom one. Everyone was confused why the audio kept playing in the editor.
Right now, I am completely unblocked regarding audio, which is a first in a very long time! Whoever on the audio team made DSP Graph compatible with OnAudioFilterRead()
all those years ago, thank you!
And one last thing, thank you for the Span
APIs for AudioClip
! That was another wishlist item of mine.
UI
So UI doesn’t really have any representation in DOTS right now. But Unity is pushing UIToolkit very hard right now. However, it is completely unusable for me.
I do not know why, but even just 40 bytes of GC per-frame will cause frame spikes in a build in my space shooter test project. It is worth noting there are very few managed objects in that game, but very large amounts of entities. I’ve tried enabling and disabling incremental GC, but the result is always the same. It is as-if the GC was walking through all the unmanaged allocations. The last time I encountered this was when upgrading from 2022.3 to Unity 6 preview where StringBuilder started allocating GC when appending numerical values.
UIToolkit only supports managed strings for text, which means modifying strings results in modifying text. Maybe CoreCLR will fix this, but right now I really don’t get why it receives so much praise and attention.
I’d much rather see Unity expose more low-level APIs and utilities that allows us to build our own UI solutions with Burst.
NetCode
In past reviews, my experience with NetCode was subpar, so a lot of my feedback and opinions were anecdotes from others. Not this time!
NetCode does one thing incredibly well. Tooling.
When you develop with NetCode and enter play mode, you get the full client and server experience. And you have complete control over that experience with the network simulator, which is a fantastic tool for ensuring the feel of your game works in a networked environment. On top of that, there’s now multiplayer play mode that makes it easy to diagnose other types of synchronization issues.
The other thing that is very well-done is basic replication. It is magically simple.
However, there’s also a lot of things I found problematic in my deep-dive. I really do not like the serialization system. It is very inflexible, and requires that you define your components as primitively as possible with NetCode in mind. It is not expressive at all. Then there’s prediction smoothing which has an awful API. Chunk components are the wrong tool for configurations and external data, as they act more like caches than any kind of persistent tracked state. And then prespawned ghosts in subscenes use the Disabled component, but don’t respect LinkedEntityGroup
rules at all.
However, the real thing that threw me off is how prediction timing management works. Prediction ticks on the client have the concept of partial ticks, which is how the client moves ahead from the last fixed-rate tick to the time dictated by the dynamic framerate of their hardware. However, physics and the character controller don’t use that at all. Instead, they choose to interpolate between the last two fixed steps, resulting in physics always being a full tick “behind” relative to everything else. This is super confusing! I think you need to either commit fully to partial ticks, or interpolation. Doing a little of both is a recipe for a lot of jittery frustration.
Take the first-person character controller, and turn down the sharpness, which effectively gives the controller a more slippery inertia. Now try it under average to not-so-great networking conditions. It feels terrible, right? How do you fix that? Normally, I’d try to account for prediction errors at the velocity and acceleration level, but doing this isn’t possible without some extreme hackery. I don’t want to fight NetCode to do this.
Going back to the positives though, the improvements I’ve seen over the last year have been well-placed. The new configuration settings and driver controls and APIs solved a lot of problems I know a lot of people were having. And the support your team has provided here in Discussions has been absolutely top-notch!
Final Thoughts
It seems like 2024 was a massive damage control and re-stabilization year for Unity. If that was the goal, then I think it was successful. 2025 is a new year, and new opportunity to separate from the nightmares of the past. I look forward to seeing if this year will allow the brilliance of Unity’s engineers shine, or if history will repeat itself as is the default tendency.
As always, anyone (but especially staff at Unity) is welcome to reply to this topic/thread or reach out to me publicly or privately by any other channel. While my feedback in this review was relatively high-level this time around, I usually prefer to have much more technical and detailed conversations. Don’t worry about getting too specific.
And finally, thanks for reading this massive wall of text!