ECS Pattern: Entity Follow Entity

When I write games I have always something following something else. Camera follows hero. Light follows ghost. Goody follows space ship. Weapon follows player. And so on and so on. This is a very common pattern in my opinion. I do like to show you how to do it with an entity component system. Show you the beauty and reusability of this pattern.

In object oriented way you would give the items to the corresponding object somehow by reference or something. In an entity component system you go the other way around the items does have a link to the object they shall follow. Once you have your system which makes it happen, that every item entity with a link to an other entity will follow that other entity you will be able to let nearly every thing follow anything else.

Lemings

The objects which leads and those which follow need a position component, if we take the invader game in my previous posts the position component would look like this

package mygame;

import com.jme3.math.Vector3f;
import com.simsilica.es.EntityComponent;

public class Position implements EntityComponent {

    private final Vector3f location;

    public Position(Vector3f location) {
        this.location = location;
    }

    public Vector3f getLocation() {
        return location;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + location + "]";
    }
}

Now the object which shall follow an other object needs a link component

package mygame;

import com.simsilica.es.EntityComponent;
import com.simsilica.es.EntityId;

public class Link implements EntityComponent {
    private final EntityId entity;

    public Link(EntityId entity) {
        this.entity = entity;
    }

    public EntityId getLink() {
        return entity;
    }
    
    @Override
    public String toString() {
        return "Link[" + entity + "]";
    }
}

I basically just store the entity id I want to follow and that's it. Now we need a system which handles the linked objects. I will have to take care for all entities with a position and a model component and all entities with a position, link and model component. I will call them leaders and followers. The leaders I do manage localy in a hash map to have a fast lookup of entities.
The followers will be iterated and checked if they have a link to one of the followers. The code might look like this. We can argue about the namings I use here but I guess the idea is clear.

package mygame;

import ch.artificials.invaders.component.Link;
import ch.artificials.invaders.component.Model;
import ch.artificials.invaders.component.Position;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.simsilica.es.Entity;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;
import com.simsilica.es.EntitySet;
import java.util.HashMap;
import java.util.Set;
import java.util.logging.Logger;

public class FollowAppState extends AbstractAppState {

    private static final Logger logger = Logger.getLogger(DecayAppState.class.getName());
    private SimpleApplication app;
    private EntityData ed;
    private EntitySet leaders;
    private EntitySet followers;
    private final HashMap<EntityId, Entity> modelEntities;

    public FollowAppState() {
        modelEntities = new HashMap<>();
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);

        this.app = (SimpleApplication) app;
        this.ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();

        leaders = ed.getEntities(Model.class, Position.class);
        followers = ed.getEntities(Model.class, Link.class, Position.class);
    }

    @Override
    public void update(float tpf) {
        if (leaders.applyChanges()) {
            removeEntities(leaders.getRemovedEntities());
            addEntities(leaders.getAddedEntities());
        }

        followers.applyChanges();
        for (Entity e : followers) {
            Link link = e.get(Link.class);
            Entity invaderEntity = modelEntities.get(link.getLink());
            if (invaderEntity != null) {
                Position position = invaderEntity.get(Position.class);
                e.set(new Position(position.getLocation()));
            }
        }
    }

    private void removeEntities(Set<Entity> entities) {
        for (Entity e : entities) {
            modelEntities.remove(e.getId());
        }
    }

    private void addEntities(Set<Entity> entities) {
        for (Entity e : entities) {
            modelEntities.put(e.getId(), e);
        }
    }

    @Override
    public void cleanup() {
        super.cleanup();
    }

}

Smarter Lemings

The problem with that is that it follows to narrow. That can be enough if you have goodies inside spaceships which will fall down if you destroied that spaceship, but not if for example a light follows our hero. So we need some kind of an offset which we will also take in account.
The offset component looks like this

package mygame;

import com.jme3.math.Vector3f;
import com.simsilica.es.EntityComponent;

public class Offset implements EntityComponent {
    private final Vector3f offset;

    public Offset(Vector3f offset) {
        this.offset = offset;
    }

    public Vector3f getOffset() {
        return offset;
    }

    public String toString() {
        return getClass().getSimpleName() + "[" + offset + "]";
    }
}

The follower will also have now an offset, so we can slightly improve the follower set

    followers = ed.getEntities(Model.class, Link.class, Position.class);

And the follow algorithme must be extended to add the offset to the leaders position

    followers.applyChanges();
    for (Entity e : followers) {
        Link link = e.get(Link.class);
        Offset offset = e.get(Offset.class);
        Entity invaderEntity = modelEntities.get(link.getLink());
        if (invaderEntity != null) {
            Vector3f location = invaderEntity.get(Position.class).getLocation();
            e.set(new Position(location.add(offset.getOffset())));
        }
    }

and we are done.

Of course we could now also make that entity rotate around the leader, so you would have kind of a rotation system which rotates that offset vector. Also you could take in account the direction of the leader for example if the follower is a sword. So from this point on you can make a lot of improvements.

Comments

comments powered by Disqus