Pure Data ECS - Can I use that layout in babylon?

Hi!

I’ve been trying out Unity Tiny, and writing my code using Entity, Component, System, seems a lot more modular and composable.

According to this video YouTube Unity seems to be going in that direction also.

Is this something we could implement in Babylon also?

Here a Unity Tiny youtube workshop, if you want to get to know pure data ECS

Very cool series of videos. I didn’t know about Unity Tiny - writing in TypeScript and that it runs in webpages! Very interesting development.

There are some Behaviors in BJS right now - you can find ie: on Camera - I would not put in same category as ECS behavior.

The game that I’m building now is ECS oriented. I use a factory method to create all of my game objects. Those objects end up in a searchable repository. I have services acting on those repositories in my game loop.

I’m sure you noticed that StreamingService instantiate was their factory method. Their world is like a BJS Scene. We can add tags to meshes. Their are a lot of parallels, but you have to build it all yourself. Once you have an Entity repository you can add all kinds of lifecycle methods and hooks to create the same concepts - the caveats they were showing at the end point to that they are doing something similar. Their query engine is a more involved - as the entity can have multiple composed components. I am using lambda expressions on strongly typed objects, which misses that benefit. You can make it dynamic by iterating all object properties, but that seems slow - you can get around that by generating object descriptors. I use a technique like that in my React Renderer for BabylonJS with object descriptors. If you were to build an ECS system as a BabylonJS plugin then I would be happy to review and contribute.

I’ve always felt that an ECS is a style more so than it is a framework. Anything where an entity is essentially just an id and data-driven components qualifies as a ‘pure’ ecs in my book. Meanwhile the older unity style of monobehaviors was just OOP with a preference to composing objects out of multiple objects instead of building an inheritance chain (still an improvement though!). Here’s my take on a modern ecs style that can mix with BJS:

/// class example, but a raw object is fine too
class Entity {
   constructor(id) {
      this.id = id
      this.mesh = babylonMeshStuff
      this.living = {
        hitpoints: 100,
        resists: { fire: 15, ice: 24, poison: -8 } 
      }
      this.ai = {
        aggro: false,
        target: null,
        foo: 230
      }
      this.anotherComponent = {}
   }
}

In this manner an entity is “just an id and components,” which is the common sales pitch of the ECS

The other benefit of the above is that the game logic is pretty much pure vanilla js, without being affected by a framework. Babylon merely contributes a mesh to the above, and presumably gets featured again in a collision/physics system when we need some of those intersections/raycasts etc to operate on the mesh component. I don’t follow pure ECS (anymore…) but I use a pattern very similar to the above to mix code with babylon (and pixi… and other 3rd party libs).

This entirely skips the life cycle stuff (which, subjectively I’d say is a good thing). Things like hooks around addComponent and removeComponent are a great way to make a buff system, but I’d argue that the full set of hooks are awkward on other types of components… like position, or physics, or the living component above. I’d rather make it deliberately than have the side-effecty feeling. But hey, it is totally subjective.

And then here’s a system (can be a class or function, whichever is the more minimal for its task):

// can accept the component, e.g. entity.ai if component's state is all that matters
// can accept the entity, if multiple components' states matter
// can accept a specific set of components, for granular stuff
function processAi(entity) {
   if (entity.ai.aggro && entity.ai.target) {
       // move entity.mesh.position towards target position, or something
   }
}

At the engine level our ecs might look like:

const entities = []
...
function update(delta) {
    applyPlayerInput(playerControlledEntity)
    entities.forEach(x => processAi(x))
    MovementSystem.update(entities, delta)
    CollisionSystem.update(entities, delta)
    fooUpdate(entities.filter(x => x.foo)) // run only on entities that have a foo component
    barUpdate(cachedBarEntities)
    scene.render() // a babylon thing
}

That’s just a bunch of made up stuff where each line is a hypothetical system – each its own unique little snowflake that still nevertheless can behave with purity. Once again though, we end up with a game that uses babylon as opposed to being “in” babylon.

The trouble in the end for every ECS is that at some point a system needs to tell another system something, thus partially reviving the type of coupling that drove most people away from whatever and towards ECS to begin with. The two most common solutions are to raise events or let systems invoke code from other systems. Both those, in my opinion, reduce the ECS to a monolith once more… with events being particularly shady in how they do it.

The best I’ve come up with is to copy functional programming chains where each system produces state to be used by the next system. For example the collision system can produce a list of entities that walked into spikes (and any other collision-related occurrences), and that list can be passed to the following system which handles dealing damage and deciding if things died. This keeps the benefit of FP and ECS where if we run into a bug we only have to review a small section of code – as opposed to firing off events, which makes us feel like the whole application was written in global scope and the problem could be in many places.

8 Likes

In this manner an entity is “just an id and components,” which is the common sales pitch of the ECS

I disagree - it’s not a sales pitch as much as it is a way to enforce loose coupling between the different features that are being developed. It is the essence of relational modeling, which seeks to reduce the development friction incurred by changes to the data model. The design that you have proposed, one could argue, is just an “entity system” architecture, because it does not enforce the loose coupling that the ECS architecture provides hard constraints for.

Having components entirely decoupled from the entities provides a wide array of benefits (no pun intended), most of which can drastically reduce resistance to change and tends to produce more modular and reusable code. As with relational modeling, applying normalization to your data (components) enhances the modularity and reusability of your components. You can think of each type of component as a table in a database, and each property as a column.

In your proposed design, refactoring any property on the entity will require that you also refactor it anywhere that that property is in use, which can be scattered all over the project depending on what systems use it. On the contrary, entirely decoupled components enforce a style in which, if you want to refactor some state on an entity, you only have to touch one place: where the entity is declared to have that component. The rest takes care of itself, as systems do not have to be refactored at all due to the fact that systems do not operate on entities that don’t have the required components.

This brings up another powerful side-effect of this decoupling: components and systems can be registered with the engine during runtime. I cannot even begin to describe the power that comes with runtime composition. From the ability to “hot-reload” your code while the game is running (really just redefining systems/components), all the way to assisting in the debugging of complex features by toggling systems on and off at any point while the game is running (this can help isolate the systems and features that bugs have originated from).

The ability to add/remove components to/from an entity during runtime allows you to declare whether or not an entity exhibits a specific behavior, dynamically and declaratively. If an entity does not have a component, it does not exhibit the behavior of the systems it does not meet the requirements for. In the entity system architecture you proposed, you would have to explicitly check for a property’s existence in any system that uses it to gain this useful side-effect which ECS gives for free.

The trouble in the end for every ECS is that at some point a system needs to tell another system something, thus partially reviving the type of coupling that drove most people away from whatever and towards ECS to begin with.

I think this probably the most misunderstood concept within ECS. There is a fundamentally different way to think about this, and that can be made more clear by first identifying the difference between imperative logic and declarative logic.

Imperative logic is used when writing the update loops of systems, and when defining event listeners that any given systems may be firing (e.g. engine.on('collision', (a,b) => {...}) where ... is the imperative logic. This is usually the majority of the game’s code.

Declarative logic, on the other hand, is in use when emitting and reacting to events. Adding systems to the engine, and components to entities are also other declarative actions involved, as we are declaring the behavior that entities exhibit. Meanwhile, “under the hood” we imperatively explain exactly what that behavior makes the entity with the component do.

So the question of how systems should or should not communicate with one another is only answerable by the developer. ECS gives enough room to leave it up to the developer’s best discretion. It is entirely up to the design of the systems and how one chooses to make them work together.

Personally, I like to follow these rules when it comes to systems:

  1. Data in: components only
  2. Data out: events & components

If you need one system to convey information to another, it should either be via an event (which delegates the decision-making up to the gamemode), or via a component (which conceals the data from the gamemode logic layer, allowing another system to receive information from the component data). If you use events to delegate certain decisions to the gamemode-specific layer of code, you begin to be able to draw a line in between what is reusable for other games, and what is only specific to the game you’re creating at that time. This, of course, is just how I personally go about things. Maybe there are situations in which it is sane to have systems communicate directly via events, maybe in the case of syncing up the state between two systems (alternatively, use a service pattern, but this will couple the systems together). That begins to creep in on multi-threaded ideas, which the ECS architecture, and data-oriented design in general, are completely setup to handle.

One final benefit I will touch on is the performance aspect. Because components become essentially what are no different than C structs, pure data, the CPU can more efficiently loop over large arrays of them. This is due to the way CPU caches are designed, but in general the idea is that when the data is laid out in memory, with arrays containing structs of the exact same same shape and size, the CPU caching logic is able to better predict what it should fill the caches with next. When iterating over deep entity objects the memory can be less predictable and thus cache misses have the potential to occur much more frequently. This can be avoided in the entity system architecture you propose, but it is not enforced. That said, this is JavaScript, so there is only so much we can do about how memory is accessed. This part of ECS and data-oriented design is a more significant aspect in languages with more control over memory management.

In the end, the benefits of the design are:

  • More modular code
  • More reusable features
  • Drastically reduced development iteration times
  • Reduced and sometimes no need for refactoring when changing the behavior of the game
  • Less change-resistant code
  • Generally more performant code

Most of these benefits are almost exclusively due to the data-oriented design constraints, which ECS so elegantly lays on top of.

All that said, to answer OP’s question: I don’t know, but I will soon find out how possible this is!

2 Likes

hi @n8bit, welcome to the forum :smiley:

Sorry I did not mean to imply “sales pitch” as being deceptive. Perhaps I should have said “selling point.” I myself have been running around refactoring games to be data driven over the last couple years. Definitely having entities be essentially an id + components is a big perk for modularity, feature reuse, and the ability to make changes to a game (provided the systems don’t reinvent coupling by accident).

Certainly the term ECS has been used a bit abstractly with different opinions as to what qualifies. I think it is perhaps fair to say if there is no ‘system’ whatsoever, and that methods are on entities and components themselves, that’s more like the style popularized by unity. Unity does have systems but they’re under the hood and it isn’t the style that the game developer using that engine employs – instead the game dev writes components and monobehaviors which mixes data and logic (though maybe this will change with the new additions to unity).

The ECS structure I believe we are both advocating is 1) components are just data 2) components can be associated with an entity, 3) systems operate on components.

As for whether a component is stored in an object, array, or look up table, I don’t think it is going to change too much about the rest.

addComponentToEntity(id, 'POSITION')

vs

entity.position = Vector3.Zero()

Presumably the first example there is about as pure as one can get. Behind the scenes it would probably add maintain a collection of position components or at least references.

Meanwhile #2 leaves us either having to add that position object to a collection (like #1) or when looping through entities only operating on those that have a position component. I’m not sure I would say that it can no longer be an ECS. Perhaps idiomatically it resembles object oriented code. In fact it looks so similar at first that one would have to go look through the systems to see if the game is actually data driven and made out of composable units.

I look forward to your work on ECS!

1 Like

I think the important part is enforcement. With a pure ECS, constraints are employed which guide design to just naturally turn out more modular, reusable, and performant by default. By design it will punish you if you try to do the things which an “entity system” architecture allows you to do with no complaint. No matter how much the entity system architecture tries to remain decoupled and adhere to a design that minimizes resistance to change, ECS will practically force it.

As for Unity, OP is specifically talking about Unity Tiny which is built on top of a pure ECS, one just like I described, that the Unity team has been working on for quite awhile now called DOTS. You can see the blog post about that here.

My work on ECS is highly inspired by it :smiley:

I’ve thought of another benefit: easy state serialization. The whole state of the game is stored in contingent lists of simple structs, all with the same shapes. This has huge benefits when an external store becomes desired, such as Redis. Writing an adapter for something that is so data-oriented is extremely straightforward in most cases.

2 Likes