Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Dwarf Fortress is one of my favorite games if not my favorite so props for citing its internal representation. I definitely think ECS makes sense in many contexts, and I think it's at its most powerful in tandem with a more encapsulated actor system. Example -

  struct Player: public Actor {
    Component<Sprite> sprite;
    Component<Transform> transform;
    void update() override {...}
  };
Where Component<T> is a handle to some backing storage indexed by an Actor's ID. This way, you can go the traditional route of having Player update itself in its own update() method as well as being able to Component<Sprite>::iter() along with other components for the render loop ECS-style.

My point now and my point then was going all-in on ECS as the basis for your entire architecture rather than taking a more principled approach drawing the strengths of OOP and DOD is dogmatic and difficult.

It's interesting that you mention web services - I have a lot of respect for databases and I enjoy the process of using them, however, I wouldn't ever program a web app in SQL. That's what pure ECS feels like to me. I agree that having objects manipulate the state via upholding internal invariants is the way to go. Whether you call them Actors, or Systems, or Controllers, it's kinda one and the same. That's why I love DOD as a base architectural layer to be abstracted upon, but dislike ECS as a programming paradigm.



>I wouldn't ever program a web app in SQL

My point is that ECS isn't writing SQL -- it's storing data/state in SQL [components], but operating [systems] with whatever normal programming language, in a stateless environment (at least, stateless between HTTP requests / ECS system definitions). It specifically separates the data from the business rules, which is exactly normal anytime you use a DB, but highly unusual in the context of a self-contained program (where OOP defines an object as pretty much precisely the conflation of the two -- to benefit and detriment. A class definition stores both the data, and the logic that operates/maintains it). The ECS system's query fetches the relevant dataset to work on (ala SQL queries), and the system's definition defines the business logic (ala JS/python/etc webserver).

>Where Component<T> is a handle to some backing storage indexed by an Actor's ID. This way, you can go the traditional route of having Player update itself in its own update() method as well as being able to Component<Sprite>::iter() along with other components for the render loop ECS-style.

The main problem with your model, versus e.g. bevy, I think is:

1. You lose the cache coherency gains -- objects and their handle location have no relationship to each other; so you end up hopping across the arrays randomly to find the relevant data as you call update() per object. This can probably be solved regardless, and anyways I don't care much about this -- the data modeling is more interesting, and performance just needs to approach "sufficient"

2. Defining update() per class, which happens to use the components, makes it significantly less flexible -- adding a Component<weight> to multiple classes means duplicating the logic to each class as well. You can move the logic out to a general function, and add the one call to each class that wields the component, but to make that function shareable, you're going to drop the reference to the originating class, taking only the component as input. And now you're back to systems (components alone tell you the operation to apply). Taking multiple components as input, or optional components, gives you the same structure. One difference is you can choose to not call a system despite having the components, but I think the normal ECS strategy would be to have a marker component that gets checked by the system's query for not-set.

I think ultimately, data modeling wise, they're largely the same. The update() call should be largely defined by the components the entity has -- ECS just enforces that it must be defined by it. The loss is that in your model, you could read update() alone to tell you all the logic in play, but the gain is that the update() doesn't have to be defined repeatedly (where components/logic is shared), and changing update() is done by changing Component -- which your model prefers but does not enforce.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: