ECS and you
Hello again,
with the first Early Access release for our patrons in the works, I figure it’s time we talked about some actual numbers. These aren’t as eye-catching as flashy screens but I promise this update will have at least one screen of our ongoing work (which may or may not be flashy).
So what is Unity ECS? As we talked about in previous blog posts, ECS stands for Entity Component System. In traditional object-oriented programming your code would look something like this: a Survivor object containing a health variable and a position variable, TakeDamage() and Heal() methods to modify health and a GoTo() method to make it go places.
With ECS, you instead have a Survivor entity which contains a Health component containing a health value, as well as a Translation component storing the position. Then you have a separate HealthSystem and MoveSystem which go through all entities with Health and Translation components respectively and apply their modifications.
“But Max,” you say, “that’s all programmer talk, but what does that mean for us, the players?” Well for one, it means tasks can be easily parallelized. If one System only needs to modify the Health component and the other only needs to modify the Translation component then it’s easy to schedule them in different threads and give all those cores your modern CPU has a workout.
The other, less obvious benefit is memory management: due to the way components are laid out and sorted in memory, it makes ECS very, very efficient at iterating through large numbers of objects. Like when you’re generating a 500×500 map with 5 z-levels and need to create a total of 1.5 million entities to generate that new location your expedition just arrived at.
“That sure sounds great on paper,” I hear you say, “but what does that mean in practice?” Let’s whip up a quick debug map and look at some concrete numbers then:
That’s a 100x5x100 map (the higher levels don’t have terrain, so you’ll have to take my word that they’re there). That makes a total of 50.000 cells. When timing, it takes less than a second to generate. In our OOS-based prototype, we set the test map size to 20x5x20, or 2.000 cells, because the generation times were so long, it got in the way of testing. In fact, even at that size it still took a solid 5-6 seconds to generate. That’s a 25x larger map, in less than a fifth the time, for a total speed improvement of 125x!
“But Max,” you interject, “map generation happens so rarely, I don’t mind waiting a few seconds!” Well this speed gain is not just limited to map gen. Imagine let’s say, a group of raiders spawns on your home map to attack your base. You don’t see them because they’re in the fog of war, but the 3-4 second stutter from generating, spawning, arming and clothing several dozen assailants clues you in. Nothing breaks immersion quite like “oh, a lag spike, must be another raid I should go out and spot.”
Or what about that time you’re trying to loot a 5-story building occupied by raiders and hit the load-bearing column, causing the whole thing to fold? If the building is 15×15 at it’s base, that’s well over a thousand individual entities that need to run support calculations, damage calculations, etc. Bet you’ll appreciate the lack of 2-second stutters then.
And that’s not all! Let’s take a closer look at the stats window of our debug map:
There’s a lot of technical mumbo-jumbo but let’s focus on the one number we care about: FPS. Our camera has the entire map in view and we’re producing around 90 FPS. That’s not impressive for a blank map, is it? Actually, it kinda is. You see, for every individual mesh your camera can see, your CPU needs to issue what’s called a draw call to your graphics card. This is a very expensive operation and a major bottleneck for a lot of games.
How is this relevant? Well, each of those tiles you see above is it’s own individual draw call. And the map is 100×100 so that’s 10.000 draw calls. Try this on our old prototype and you’d be lucky to get 10 FPS, just looking at the entire map.
Now for static objects like map tiles there’s an easy optimization to be made where you take all the individual meshes and combine them, then update whenever a change occurs. However, this wouldn’t work for dynamic objects, like let’s say, a thousand mortar shell fragments flying through the air and hitting stuff. But ECS rendering is so efficient at parallelizing and iterating that we could probably put 10.000 shell fragments on the screen before seeing significant performance drops.
So this the reason we’re taking time to port our entire system over to Unity ECS before the EA release instead of just polishing up our prototype code. And while this programmer talk might not have as many flashy screens to show you, in the end it’ll still produce some very exciting results.