24 comments

[ 0.17 ms ] story [ 69.1 ms ] thread
Question for those who've done both web and game dev: am I right in thinking that jQuery's like ECS, and React is more like (what the article calls) "parent, parent, parent"?

Context: I'm an aged web dev with very little game dev experience, and I did less frontend after React appeared. In my limited experiences with React, it's much more about declaring React components (not ECS components) in terms of other components, so it's structure based rather than attribute based. Whereas jQuery feels more like ECS, since code mostly spends its time querying across the DOM and attaching behaviours to objects (nodes) based on attributes.

Obviously I'm massively generalising here, and web apps tend to behave very differently to games. But it does explain a little about why jQuery feels more adaptable as a web app grows.

(comment deleted)
What you're noticing is jQuery is more about querying dom & manipulating it based on those queries as opposed to composing vdom/state

React apps work as you'd like when you put shared state in something like redux & then components query state

No, a React component is not tied to any one parent component. The paradigm is entirely of its own kind. It may react to parent data, or it may react to external data. It's purely a rendering tree management system.

There are no rules on how to manage the logic flow.

I can see what you're getting at and sure, I think there are some parallels. I think the parallels grow stronger if you force your jQuery queries to always be composable. So, no $('.js-fooSubmittButton') but yes $('.submit .clickable .highlightable'). You'd then end up writing generic functions which work against a set of entities returned by intentionally generic queries.

Bevy is still declarative in some ways, though. You create a Sprite, but you don't have to worry about the Sprite's rendering process/lifetime. You just spawn/despawn/update its properties. There's an entire "render world" which is managed by Bevy and is distinct from your app's world. In this way, you're still working through a layer of abstraction prior to rendering, like React, rather than manipulating the DOM directly, and you reap performance benefits by leveraging this abstraction.

Just like jQuery can lend itself to spaghetti code, but can also be a basis for scalable web apps (I have fond memories of the Backbone.js era!)... React can lend itself to a strong coupling between hierarchy and state management, and that's what many projects using "just React" will gravitate towards. But it's also possible to separate those concerns in the React ecosystem.

If you look at systems like React Redux, and the general pattern of a central store to which different pieces of code can subscribe to, it's perhaps the thing that is closest to ECS on the frontend.

See https://redux.js.org/understanding/thinking-in-redux/three-p... and the examples at: https://react-redux.js.org/api/hooks#useselector-examples

Rather than drilling state through hierarchical props, one can create a specification for a TodoListItem that has it subscribe to the "todos" part of storage, and rerender when that changes. And then you have a separate "system" (using the word both figuratively and in the ECS way!) that responds to UI input or network traffic, updates some isolated global state that only that system controls (say, the "todos"), and notifies all entities that are subscribed to the derived state.

So then, a React functional component that religiously uses Redux (or a similar external storage system) for state management becomes akin to an ECS-entity whose set of ECS-components corresponds to which Redux useSelector and useDispatch hooks it chooses to use. And the ECS-systems are what mutate state in response to dispatched actions - you just need to make sure they have good boundaries.

If you adhere to this, the parent-child relationship between components really doesn't matter; you could nest a TodoListItem just about anywhere and it would behave the same. It's just that in web dev it's so common to say "I am the box for an ordered list of things, and as I move and reshape my children should do the same" that while you could in theory have an ECS system that track hierarchies and repositions children using JS as the parent moves, it's more natural to do something a bit more hybrid and allow the natural DOM hierarchy to control certain things - but not all.

I'm working my way through the HandsOnRust book that guides you through creating a simple roguelike using Rust and Legion ECS. It's a really enjoyable experience so far, and I'm looking towards trying out Bevy ECS afterwards to compare the two.

https://github.com/thebracket/HandsOnRust

I've also been enjoying building My First Game™ in Bevy using ECS. The community around Bevy really shines, but Flecs (https://github.com/SanderMertens/flecs) is arguably a more mature, open-source ECS implementation (notably, it supports relationships). You don't get to write in Rust, though, which makes it less cool in my book :)

I'm not very proud of the code I've written because I've found writing a game to be much more confusing than building websites + backends, but, as the author notes, it certainly feels more elegant than OOP or globals given the context. I couldn't imagine trying to build a large game using a more rigid organizational approach.

I'm building for WASM and Bevy's parallelism isn't supported in that context (yet? https://github.com/bevyengine/bevy/issues/4078), so the performance wins are just so-so. Sharing a thread with UI rendering suuucks.

The biggest issues I experience are:

1) How generic should I make my systems? It's easy to make them very specific, but, at a certain specificity, it feels like it's just lazier OOP. More generic and I start spending my time handling edge-cases that don't actually need to exist.

2) Intelligently handling updates when systems run in parallel rather than serially. By default, Bevy runs everything in parallel, and it's heavily encouraged, but my brain finds it a lot easier to just force everything to run serially and to not rely on frame-buffering. This doesn't seem necessarily ECS-specific, though, but is a problem I'm made more aware of due to the defaults given to me.

If anyone wants to browse some code or ask questions, feel free! https://github.com/MeoMix/symbiants

> How generic should I make my systems? It's easy to make them very specific, but, at a certain specificity, it feels like it's just lazier OOP. More generic and I start spending my time handling edge-cases that don't actually need to exist.

Making a game is already hard so my advice is to “make the game” and get specific. Personally it doesn’t matter if you go top-down or bottom-up you really want to go from simple->complex at all levels. Doing this will help structure the game and make it easier to restructure as you go. You can also always generalize later.

Managing dependencies is definitely trickier in the parallel ECS context because often you’re naturally inclined to thinking about Entities interacting but systems update across all Entities in a piece-wise manner so it’s very easy to end up with implicit ordering dependencies between systems that cause subtle weirdness in the game when evaluated out of order. Especially when the execution order isn’t fixed. In some respects the Actor model would be a better fit.

"ECS" in this case means "entity component system" (https://en.wikipedia.org/wiki/Entity_component_system) not "Elastic Compute Service".
ECS actually refers to EC2 Container Service (EC2 is what you were originally thinking of, which stands for Elastic Cloud Compute) :-)
I was hoping it was the Amiga's Enhanced Chip Set
Good article, the biggest paradigm shift for me thinking about ECS was also the bottom-up approach. I'm most intrigued by platforms and frameworks that automatically "ECS-ify" data structures, like Zig's MultiListArray [0]

As an aside, I was surprised that the author goes to Harvard yet his resume looks shockingly normal, not terribly different (in terms of software internships) than the people I went to school with. It's bizarre, given the fact that Harvard/YPSM admits are basically superhuman in every conceivable quality compared to the rest of us mortals.

[0]: https://zig.news/kristoff/struct-of-arrays-soa-in-zig-easy-i...

(comment deleted)
The best ECS I've seen is Haskell's apecs. Type-driven ECS is just so sweet, and tbh you could probably improve upon apecs further if you wanted to and add all those other fancy ECS things using types (archetypes, relationships, etc)

People make a big deal out of ECS, but apecs is so trivial to use that you don't even think twice. You just say "is that it?" and enjoy.

What if I told you that games are actually not that special and similar (not necessarily ECS, but approach centered around data architecture) benefit pretty much any software that works on data, and it's all much more compatible with what you're doing with your database anyway.
In a context where you have thousands of entities, it's very rare that you'll loop all over them from 0 to N. In such context entities are always partitioned in space. You interact with one entity and you update the entities around it. For which you'll query the spatial partition to get list of entities around, and they won't be contigious in memory.

You may have one loop to update all entities but that's it. And will you even update all of them ? What about the ones that are not visible ? Maybe you want to update only the visible ones .. in that case you query the partition to get the ones visible around your camera

So practically you never query the ecs registry to loop over all of them

In a naive ECS system this might be true, but mature ECS systems use archetypes which partition based on usage to maintain the contiguous memory.

I've written about the details here: https://taintedcoders.com/bevy/archetypes/

The gist is that instead of partitioning by type you sub partition by usage. Bevy does this by optimizing your storage based on the bundles (groups of components) you create. Flecs does the same but I'm not sure of the exact mechanism.