ECS: Systems operating on different component types.

Started by
6 comments, last by haegarr 8 years, 5 months ago

I'm trying to wrap my head around ECS for toy projects, and have run into an issue a couple of times that I'm having trouble solving. I'm not a purist, but would still like to know how to deal with this.

At a high level, how do I create a system where one type (entity with a specific component) has a specific effect on another type (entity with some other component), when systems in most ECS implementations are designed to process a single flat list of entities with one or more matching components.

Examples:

Most recently, I had an object which reduces the player speed (and other types of entities) when they're nearby, based on proximity.

I have two components, one indicating that it makes other entities slow (slow_emitter or some such), and another indicating that an entity may be affected by this slowness (slow_receiver).

In a non-ecs system, I might loop over every slow_emitter, and apply some effect to each slow_receiver based on range.

A similar issue might be: When a player is nearby a button and presses the 'use' key, something should happen. An input component might be responsible for marking the player as `using`. Any entity with the `button` component, within some range of an entity with the `using` component should activate. This probably makes more sense as an 'event', but shows the issue.

Hope that makes sense.

Context: This is a love2d game. My components and entities are simple data objects.

Advertisement

It is a good question! I guess they mangle this situation by 'matching components'.

In most (not really game-related) component-oriented architectures, there are specific semantics attached to components and therefore specific behavior can be built on top of those. For example, the standard tick of a MovableRigidBody could involve iterating on all its attached Velocity and Acceleration components.

Previously "Krohm"

Thats an issue I often find myself trying to solve.

See the thing is, you totally can (and I've seen it) create a "messaging" api between systems. Ie, one system fires up an event that goes straight to another system. Which would be the case of the "activate" event for example.

Thing is, I feel like event/messaging api is bolted on. The system "should" write data into an entity's component, and the system that handles the response should fetch the data when its their turn. As you described, this isn't something simple to do in many cases.

The specific issue here is when this happens. With event systems you need to figure it out because its very important, imagine that if the physics system casts a ray and fires an event, the receiver does something with it, but it probably will be a system that also iterates over its entities. If you make a single event system, they might get processed at the same time, if you do it per system ,you could make a queue that only gets processed when that system is processed, or you could try to do without events and just write data, read data, and make it part of the normal entity-component iteration.

Order of system processing and system inter-dependencies are very important for getting consistent results, finding bottlenecks and possibly multi threading some of it.

Its still one of the things I'm not decided on, thus why I haven't added an event api on top of dustArtemis.

One idea I had for this kind of problem is to have one system in charge of maintaining a needed spatial structure. When entities get added/removed, system iterates over the affected entities and maintains its spatial structure.

Different systems can hold a reference to that system, and issue queries to it. So the system thats in charge of activating stuff (SystemA) can query "well, for this direction and reach, SystemB, give me what I am hitting". Thats a data query, and means that SystemA depends on SystemB to work on, that means that SystemA has to be executed after SystemB has updated its internal spatial structure for that frame. That makes dependencies obvious.

Then it would be a matter of:

.


for (entity in entitiesBeingHitted)
{
  if (entity.hasComponent(ActivableBehavior)
  {
    entity.addComponent(ActivatedTag);
  }
}

.

Then the system in charge of activated stuff can iterate over the activated entities in that frame and (possibly) execute trigger scripts or something, which in turn could plug other events in other subsystems. In this case I think adding/removing components to entities, and whatever consequence that has in the engine, should be a fast operation for this to work properly.

It might get overly complex, and maybe a straightforward but well defined messaging api between systems is better, as I said, I'm evaluating my options.

"I AM ZE EMPRAH OPENGL 3.3 THE CORE, I DEMAND FROM THEE ZE SHADERZ AND MATRIXEZ"

My journals: dustArtemis ECS framework and Making a Terrain Generator

In a non-ecs system, I might loop over every slow_emitter, and apply some effect to each slow_receiver based on range.

Why can't you do that?
Fistly, ECS doesn't exist except as a buzzword that has very different definitions from every single person who blogs about it. Secondly, don't blindly try and copy someone else's framework that constrains the way that you solve problems, such that your problem becomes "how do I use this framework that I've caged myself within". Thirdly, algorithms should be able to operate on more than one type of component at a time.


void SlowSystemUpdate( const slow_emitter* emitters, int emittersCount, slow_receiver* receivers, int receiverCount )
{
  for( const slow_emitter* emitter = emitters, *endEmitter = emitters+emittersCount; emitter != endEmitter; ++emitter )
  {
    for( slow_receiver* receiver=receivers, *endReceiver = receivers+receiverCount; receiver != endReceiver; ++receiver )
    {
      float dist = Distance(receiver->position, emitter->position);
      if( dist < 100 )
        receiver->speedMultiplier = (dist/100)*0.9 + 0.1;
      else
        receiver->speedMultiplier = 1;
    }
  }
}

What is the harm in doing something like this? I would think it'd be required in any system:


// SlowSpeedSystem::Update() gets called once for each SlowSpeedComponent in the game
void SlowSpeedSystem::Update(SlowSpeedComponent* slowSpeedComponent)
{
  // find each entity near this slow speed component's location
  TEntityList EntitiesNear = GetEntitiesNear(slowSpeedComponent->Location);
  for (auto entityNear : EntitiesNear) {
    // if the entity is moveable, then apply the slow speed 
    if (entityNear->HasComponent("moveable") {
      entityNear->GetComponent("moveable")->ModifySpeed(slowSpeedComponent->SpeedAdjustment());
    }
  }
}

Or something like that.

My Gamedev Journal: 2D Game Making, the Easy Way

---(Old Blog, still has good info): 2dGameMaking
-----
"No one ever posts on that message board; it's too crowded." - Yoga Berra (sorta)

I wouldn't have a Slow_Reciever/Slow_Emitter, instead having a ScriptSystem, and with your slow-trap/whatever having a script attached.

Further, for variables that you want to commonly modify for ingame logic, I'd have 'modifiers' for those variables, that way you can stack modifiers; maybe you're wearing a boots of +10 speed, but you're slowed, which decreases speed by 50%, but you're also walking on sand, which further decreases you by 15%.

So your movement component might look like this:


struct MovementComponent
{
    Vev3 velocity; //velocity = (direction * (acceleration * (slowModifier * (1.0 - slowResistance))));

    Vec3 direction;
    float acceleration;
    float slowModifier;
    float slowResistance;
};

And I'd define something like a "Logic tick" where your more general gameplay script logic gets updated every so often. Perhaps twice a second.

You'd clear all the modifiers, then re-apply them from the scripts.

Your slow-trap/whatever entity could have a script like this:


SlowTrapScript

function onLogicTick()
{
    Array<Entity> nearbyEntities = World.GetEntitiesNear(this.position, effectRadius);   
    nearbyEntities = ExcludeEntitiesWithoutComponent(nearbyEntities, MovementComponent);
    
    for(Entity entity in nearbyEntities)
    {
        entity.GetMovementComponent().slowModifier += 0.5;
    }
}

The reason being, here you have custom logic for the entity, so you don't want to hardcode it, you want to script it. Entity behavior should be scriptable, but with the commonalities being hard-coded (and toggable with components or with flags).

But it depends what level of abstraction you have your ECS at; I'd have the ECS be a high level "foundation" with common logic, with scripts building on top of it.

It sounds like your ECS *is* the highest level of abstraction (which makes sense in retrospect since your entire game is written in a scripting language...). In that case, instead of having a "ScriptSystem", I'd do a "LogicSystem" and "LogicComponent", where each entity-type has their own function ran with custom logic. This could be handled via function pointer or closure or whatever.

Fistly, ECS doesn't exist except as a buzzword that has very different definitions from every single person who blogs about it. Secondly, don't blindly try and copy someone else's framework that constrains the way that you solve problems, such that your problem becomes "how do I use this framework that I've caged myself within".


Can you post that again so I can upvote it again? smile.png

Sean Middleditch – Game Systems Engineer – Join my team!

Many of such "problems" disappear immediately if one thinks of a standard solution (here e.g. simple lists if only few objects are involved, space partitioning, influence maps, …) encapsulated within systems with appropriate interfaces, and understand components as data suppliers. There is no enforcement that component structures used internally are the same as those used externally of sub-systems.

For example, when an entity with an "interactive usage" component is instantiated, the component causes the "interaction sub-system" to prepare its internal representation so that it considers the component or its meaning at least. Because it provides an interface that allows to query, other sub-systems can ask for, say, any interaction possibility within a given range. When the player controller detects the input situation "interactive use" when running its update, it queries the interaction sub-system for suitable devices.

This topic is closed to new replies.

Advertisement