On Behavioral Entity Component Systems

shaw
6 min readJul 13, 2020

This is a proposal for the introduction of an additional design guideline and interface to augment some aspects of traditional ECS where the pattern might be more cumbersome than it has to be.

The core idea is this:

— all data transformations can be encapsulated in a function

— functions in ECS perform data per-entity in a loop and functions exist outside of systems so that they can be contextual, reusable, modular and only referenced once.

— because all functions basically apply a transformation of data on one component to another, they can fit a defined interface of entityIn, args with options for delta and entityOut

— we call modular functions that follow this interface behaviors

This might sound initially like OOP, but if we were to implement a batching system to order all transformations by function type we could execute all functions (move, jump, handleMouseButton, etc) in order and we could take full advantage of linear execution in lower level languages. I’ll be honest, in writing our Typescript engine I haven’t tried, since it adds a lot of complexity.

To refresh, the current ECS rules, roughly, are as follows:

— Entities are nulls which, in pure form, are just a unique number (UUID, auto incremented, etc)

— Components are classes which contain data, but no logic (position, rotation)

— Systems query groups of components and find all entities that have components matching the query (query: [Player, Alive, Health])

— Systems perform logic on entities by proxy of transforming data on components (i.e. entity.getComponent(Position).value += entity.getComponent(Velocity).value * delta)

If you follow these rules, you end up with systems that only care about the components they touch, and components that don’t care about the systems at all.

So let’s say I want a physics system in my game, because I don’t have one yet — but you do. Assuming that we’re both using the same programming language, you can send me that system. The strictness of the ECS pattern also means that I could probably rewrite your physics system from one ECS framework to another pretty easily.

But physics is an ideal example — along with rendering, post processing, particles, audio and other domain-complete systems, they don’t need to care much about other systems or components other than the specific handful relating to them and only them. These systems complete their own circle, in terms of separation of concern. But not all systems are domain specific. Let’s take Movement for example.

Movement has these curious attributes:

— Movement buttons can oppose each other, forward and backward simultaneously need to be handled, and movements also stack, for instance forward + strafe left

— Some inputs cancel or override other inputs (jumping and crouching) while modifying movement itself (crouching slows movement, sprinting speeds it up).

— Movement can be controlled by the player or by AI, or by AI steering overtaking the player with a Fear spell.

— Movement often maps to animation, but since you can move forward and left simultaneously, animation state needs to be described as a blend (in Unreal this is called a blendspace)

These are all dilemmas you could easily solve with logic in your system. Hurray! All is great, you’ve made a system for movement… well, some movement. I made a really cool swimming system, maybe we can put it in before your movement system, or after, or we need to combine them…

ECS starts to fall down because our solution to modularity is, instictively, to create more systems, atomize our components, share more data. It’s a good instinct — but the way most ECS systems are querying is by iterating through a list, checking for what changed this frame and what didn’t and making sure all query lists are up to date and valid. When you start managing 50 systems, 150 queries and 250 components — i.e. a small game — things start to get burdensome. Once you’re dealing with a character who can crouch-jump off a wall they were climbing into water, unless they have the flight capability, well it becomes apparent that a query matrix for complex state, or branch code (if(isJumping && !isCrouching)) — isn’t going to scale on the developer side, and it could have performance implications that need to be handled by separate, complex solutions, like archetypes.

ECS is great for keeping things fairly naive of each other, but you pay for that naivete by having to query your queries with getComponent , or through a complex query matrix— and for handling something like a Finite State Machine, it’s not pretty.

Remember the rules I stated before, for ECS. I would like to change some of the conventional logic of those rules, and add Behaviors into the mix. We are calling it Behavioral ECS, or BECS, because we are shifting all of the game logic onto our behaviors.

First, I will present the rules of the BECS pattern:

— Entites are the same as before.
— Components hold all data related to the component, including reference to how the data should be transformed — this is called the “behavior map”, they are each slightly different per system but follow a very similar pattern.

— Systems have no game logic, instead their main function is to execute user-defined behaviors from an associated component’s behavior map.

— Behaviors are pure game logic. They are single static functions that transform data from one entity to another (in the case of “system behaviors”), one component to another (in the case of “entity behaviors”) or onto the same component (“component behaviors”).

More concretely, a snippet from a behavior map:

inputAxisBehaviors: {  [DefaultInput.MOVEMENT_PLAYERONE]: {    behavior: move,    args: {      input: DefaultInput.MOVEMENT_PLAYERONE,      inputType: InputType.TWOD     }   }
}

… and a function header for a behavior

export const move: Behavior = (entityIn: Entity, args: { input: InputAlias; inputType: InputType; value: NumericalType }, delta: number): void => {
// Get some components, make velocity affect position, hurray
}

… so what have we done?

Movement buttons and joysticks call move(), which changes the transform’s velocity. Similar behaviors for jump, crouch, turn, etc. are implemented purely as functions, matching the behavior of the move function above.

Our entity can also have a subscription component that velocityToPosition is subscribed to, so every time the onUpdate() hook is called by the subscription system, the position actually gets updated, but not until all inputs and states have been collected — our systems guarantee execution order and a general flow of data. The subscription system doesn’t know anything about components.

If you were to use an engine implementing this pattern, you could build your entire game purely with behaviors. Map the behavior to a thing — a state lifecycle hook, an input, a point scored, whatever — and all you have to worry about is the logic. No need to write systems at all.

In reality, this doesn’t make sense, and I don’t recommend it, but as a pattern for covering a portion of your game, especially where you are running into systems that manage a lot of overlapping data — input, state, transformation, basic setup and teardown — I recommend the pattern.

And should you want to build a game or experience where you have visual tooling or users can script inside of the app, adapting the Behavior Map pattern to node graph or hierarchical editors is as easy as working with any other serializable object. My personal goal is to put an entire game inside a glTF.

As we are all adventuring and exploring novel patterns together, I welcome your input. I believe there’s much room for refinement, and I hope people will try to apply the pattern and get back to me with ways to improve it (please email me if you do, or find me on the ECSY or xr3ngine Discords!)

--

--