Hacker News new | ask | show | jobs
by dustbunny 2 days ago
The real key pillar to this world view is putting the data first in your design of the algorithm.

So if your working on a physics engine and your optimizing collision detection, you think about the data in -> data out of the problem you are solving as the primary driver of how the code should be written.

You start with defining the data, and build from there.

Different types of applications all have different shapes of data so would have differently shaped optimal code. Eg) a physics engine would use some kind of spatial hash thing which can be optimized differently based on if stuff can be added/removed while it's running. A 3d renderer operates on big buffers of matrices and vertex data. A game is usually composed of some long lived things and a lot of short lived things.

The key message in Mike Acton's talk was:

"If you have different data, you have a different problem."

While ECS systems are not a panacea that solves all problems in a perfect data oriented way, they are generally more malleable than Object Oriented hierarchies. This means it's generally more feasible to write "near optimal" code in an ECS framework than in a mature Object Oriented code base.

But the key message isn't "use X framework", it's "start by defining the data".

5 comments

My experience with ECS is that you shouldn't use an "ECS system". You should just do ECS. Have an array of all your particles and then update all the positions according to the velocities. Don't use a framework where you do something like get_all_entities_with<Particle, Position, Velocity>(). Just have struct particles {vector<vec3> positions, velocities;}. Well, managing those parallel arrays gets pretty annoying, but you solve that with a parallel-array class template rather than a whole framework that promises to do everything. (You might not actually need parallel arrays anyway - AoS might work just fine here because you usually touch each field of each particle once per frame and exactly in order.)
But why? This looks like the mother of all boilerplates. And for what purpose?

Also: just having stuff in an array or vector invites you to the ABA problem. You need a generation counter in there too, or else the array indice may get reused if something is deleted and another thing is reinserted at the same index. But that's yet another boilerplate that would easily be overlooked if you had to do everything manually.

Also: you seem to be saying this with C++ in mind. Do you think that applies to bevy_ecs too?

Modern Bevy has relationships to make sure that if an entity has a component that refers to another, it doesn't become dangling. It works a bit like foreign keys in databases. I think this makes ecs much more usable

(as an aside, there is a whole host of analogies between ecs and relational databases. entity archetypes are tables, entities are rows, components are columns, and systems are queries). Nobody tells people to just write their database from scratch though)

> You need a generation counter in there too, or else the array indice may get reused if something is deleted and another thing is reinserted at the same index.

It's really sounding like you're pretending not to know what your program does, which is one of the core OOP ideas that DOD refutes. In OOP you have an array of Shape and you pretend not to know which shapes your program implements, so the only way to draw them is to call ->draw() on each one. And you pretend you don't know anything about the lifetime of a shape so you use smart pointers everywhere to extend it as long as needed. In DOD you assert that you do know what shapes are available and what their lifetimes are.

I certainly don't know "what my program does" if it's a game where, at every frame of the simulation, thousands of entities might or might not be created, deleted and recycled depending on player inputs.

Can you describe a superior replacement for generation counters?

If you don't know what your program does, DOD asserts that you are a bad programmer and you should first figure out what your program does before continuing.

Sounds like you're writing a game engine, not a game?

You don't seem to grasp that handling individual entity state has a cost that has to be minimized and amortized.

A generation counter offers a good deal in many common cases: for the fixed small cost of incrementing the generation counter when an entity ID is assigned a trivial test, also fixed cost, can disambiguate successively recycled entities that share the same number and tell which one is current.

This adds up to work proportional to the number of distinct entities plus, assuming obsolete references are eliminated, work proportional to the number of references to entities, which is clearly very good. Do you recommend a different way to solve the same problem? There is at least one obvious one.

This is such a good explanation of OOP vs DOD
As I recall the reason for managing blocks of data this way was memory cache efficiency.
> This looks like the mother of all boilerplates. And for what purpose?

Isn't most programming? Isn't struct Particle {vec3 position, velocity;} also the mother of all boilerplates?

The problem is that there are a lot of subtleties to an ECS that these frameworks solve, and they perform better than a naive approach too. Your solution of a particles struct doesn’t even support a fundamental feature of ECS’s which is runtime composition. It’s really a different solution altogether, which is fine but it’s not a replacement for an ECS.
Often you don't have runtime composition and don't need it. By prematurely generalizing you're venturing close to OOP territory.
“Prematurely generalizing” is now an OOP thing? Are we just using OOP as a term for anything bad now?

Ooh here is a controversial one. DOD is premature optimization. Most programs don’t have enough data where the storage and access is a factor for performance. In fact it might be slower to use DOD.

I don't think that's all that controversial. DOD comes out of the field of video games where people were frustrated by only being able to process ten thousand things sixty times per second and wanted to process ten million things sixty times per second. While it's also been used to speed up things like the Zig compiler, it's definitely not necessary for all software.

One of the ideas in OOP that DOD is explicitly refuting is pretending not to know what your program does. You know which subtypes exist in your program and you shouldn't treat them generically as supertype instances except when that is actually optimal. On this axis, both ECS ideology and OOP ideology are opposite to DOD ideology. ECS happens to align with DOD in that it prefers big linear arrays, but it does not align in pretending not to know what combinations of arrays are used, which is similar to pretending not to know what subtypes of Shape exist.

You don't always know what subtypes will exist, either at runtime or in future iterations of the program.
No amount of ideology can fill L1 cache.
> Are we just using OOP as a term for anything bad now?

Different paradigms come with different pathological cases. I've had to deal with Scala programmers whose notion of FP is making everything generic on the Monad it abstracts over, even when it'll only ever be instantiated on the one effect system the team uses. Nonsensical levels of generality is one of the classic OOP pathologies, and is the reason why we have aberrations like the AbstractSingletonProxyFactoryBean[0].

0. https://docs.spring.io/spring-framework/docs/current/javadoc...

oh my god. I thought that class was just a joke, but it's real.
>"Nonsensical levels of generality is one of the classic OOP pathologies"

I think it has nothing to do with OOP which I find very convenient for some domains and not so much or even opposite for others. These "Nonsensical levels" of anything is a disease which is called Architecture Astronauts Syndrome and victims apply it to any paradigm

> My experience with ECS is that you shouldn't use an "ECS system"

Why? Flecs, for example, is pretty sick imo.

Because the people who made them got nerd trapped fucking around with ECS and never shipped a product
This is a reductive ad hominem, not a first principals examination.
No it’s not an ad hominem.

The first principles examination is you need an array of structs.

If your goal is to ship a product you would use this proven technique. It’s shipped more AAA and small games than any other.

The ECS is a hobby project which solves problems unrelated to your goal. (Making a cool ECS) And because it was not engineered with the goal of shipping you’re sitting on a pile of unknowns when you try to.

Proven technique is an Appeal to Authority. You have offered no real first principal evidence for your claims. Here's some evidence in favor of flecs:

I find value in its reflection and serialization system, as well as it's pipeline API.

Games require far more than a bunch of arrays. The simulation code itself is dwarfed by the UI, VFX, animation and scene layout code. The APIs in Flecs have a lot of value for these "not the core game" use cases.

Conversely, the amount of boilerplate you have to write to stand something up in Flecs is massive. And the use of macros in Flecs APIs tends to break editor auto complete. And the learning curve is very big. And the output code just looks very different from Orthodox C.

Getting nerd trapped fucking around with ECS is a real risk. The rabbit hole goes deep. But the library itself is valuable, powerful and well done.

This is my honest thoughts on ECS systems derived from first hand experience.

This is basically how I've always designed things, and I really feel like it's the best approach. Pick the right way to model data, and the algorithms will flow naturally from it. The cognitive load of reading "data" code rather than "algorithm" code is a lot lower in my opinion; reading through a bunch of declarative types like struct definitions isn't nearly as much work as reading through functions that operate on them, and moreover, it's a lot easier to spot any potential bugs in them because you don't need to maintain very much "state" in order to understand them. Maybe this is why I've generally found ECS conceptually pretty easy to wrap my head around (which is of course separate from whether it's easy to use or not, as that depends a lot more on both what the framework exposes and how a team chooses to use it).
I feel like DoD is one of those things that only makes sense after you already believe in it. There's a sort of KISS epiphany that you have to go through which I don't think the commonly available info on DoD helps you to reach. It doesn't help that everything about it tends to get hung up on overly-specific C++ optimization advice.
While the idea is itself not without merit the problem is when people design these data oriented systems without abstractions and in fact it's often difficult to find good abstractions around the data the problem comes when the system, the data and functionality needs to change. There will be problems.

So while it's great to think about the data flow it's also important to think about the abstractions around it,.ie the (system) interfaces that let the system evolve without having to propagate changes everywhere while reaping the benefits of data orientation.

This is what ECS frameworks like Flecs can help with. They provide the tools and abstractions so you can write heavily modular code in this model. If you don't need the generic modularity, flecs will not feel like your gaining anything for the boilerplate you gotta write and the stuff you gotta learn, but if your making something huge that needs work for years, learning flecs is probably worth it.
> ECS

“Entity Component System”, for those like me who didn’t immediately think of it.