ECS Pattern: Follow Entity

When I write games I have always something following something else. The camera follows a hero. Light follows ghost. Goody follows the space ship. Weapon follows the player. And so on and so on. This is a very common pattern in games.
In the object-oriented world, I would probably have a reference to the object which is "attached" to me, like the gun or lamp or whatever it is. In an entity component system I go the other way around, the attached item does have a link to the entity it has to follow. Once you have your system which makes it happen, that every item entity with a link to another entity will follow that other entity you will be able to let nearly everything follow anything else with literally one line of additional code, well most often.

Lemings

Precondition

You already know what an entity component system is if not this article EntityComponent System is Crazy Cool migth give you a clue. I do all my examples in Java, but it should easily be adaptable to whatever own language you have. Also, the ECS might differ, I use Zay-ES, but as well you can probably easily make the link to your implementation of an ECS.

Position of Your Leming

The objects which lead and those which follow need a position component to store the location and the orientation of the entity. This component is set on every frame to make the thing move around. And yes it is always a new instance, no reuse. And java seems to be smart and efficient enough not to have any issue with very short-living objects. Components do have intentionally no setter methods!

public class Position implements EntityComponent {

    private final Vec3d location;
    private final Quatd facing;

    public Position(Vec3d loc, Quatd quat) {
        this.location = loc;
        this.facing = quat;
    }

    public Vec3d getLocation() {
        return location;
    }

    public Quatd getFacing() {
        return facing;
    }

    @Override
    public String toString() {
        return "Position[location=" + location + ", facing=" + facing + "]";
    }
}

Link to Another Leming

Now the object which shall follow another object needs a follow component

public class Follow implements EntityComponent {

    private final EntityId parent;

    public Follow(EntityId parent) {
        this.parent = parent;
    }

    public EntityId getParent() {
        return parent;
    }

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

I just store the entity id I want to follow and that’s it.

I Follow You

Now we need a follow system which handles the linked entities. I will have to take care of 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 followers will be iterated and checked if they have a link to one of the leaders.

Set It Up

For this, I only need the entity data and two Zay-ES containers, one to hold the follers and one to hold the leaders.

public class FollowSystem extends BaseAppState {

    static Logger LOG = LoggerFactory.getLogger(FollowSystem.class);
    private EntityData ed;
    private FollowerContainer followerContainer;
    private LeaderContainer leaderContainer;

    @Override
    protected void initialize(Application app) {
        ed = main.getStateManager().getState(EntityDataState.class).getEntityData();
    }

    @Override
    protected void cleanup(Application app) {
    }

Start/Stop It

When we enable this system we instantiate and start the follower and leader container.

    @Override
    protected void onEnable() {
        followerContainer = new FollowerContainer(ed);
        followerContainer.start();
        leaderContainer = new LeaderContainer(ed);
        leaderContainer.start();
    }

    @Override
    protected void onDisable() {
        followerContainer.stop();
        followerContainer = null;
        leaderContainer.stop();
        leaderContainer = null;

Every Frame

On every frame, I update the follower and leader container to get all added, updated, and removed followers and leaders. To update all follower’s positions I simply loop over the followers, see if their leader does exist in the leader container, get the leader position, and store this position in the follower’s position.

    @Override
    public void update(float tpf) {
        followerContainer.update();
        leaderContainer.update();

        followerContainer.stream().forEach(follower -> {
            LeaderData leader = leaderContainer.getObject(follower.leader);
                        if (leader != null) {
                ed.setComponent(follower.id, new Position(leader.location));
            }
                });
    }

Leaders

The leader’s container keeps track of all the entities which do have a model and a position component, the code is very simple and straight forward.

    class LeaderData {
        Vec3d location;
    }

    class LeaderContainer extends EntityContainer<LeaderData> {
        LeaderContainer(EntityData ed) {
            super(ed, ModelType.class, Position.class);
        }

        Stream<LeaderData> stream() {
            return Arrays.stream(getArray());
        }

        @Override
        protected LeaderData addObject(Entity entity) {
            LeaderData result = new LeaderData();
            updateObject(result, entity);
            return result;
        }

        @Override
        protected void updateObject(LeaderData data, Entity entity) {
            SpawnPosition position = entity.get(Position.class);
            data.location = position.getLocation();
        }

        @Override
        protected void removeObject(LeaderData data, Entity entity) {
        }
    }

Followers

The container is even simpler, we are only interested in entities which do have a follow component and store our entity id and the leader’s entity id.

    class FollowerData {
        EntityId id;
        EntityId leader;
    }

    class FollowerContainer extends EntityContainer<FollowerData> {

        FollowerContainer(EntityData ed) {
            super(ed, Follow.class);
        }

        Stream<FollowerData> stream() {
            return Arrays.stream(getArray());
        }

        @Override
        protected FollowerData addObject(Entity entity) {
            FollowerData result = new FollowerData();
            result.id = entity.getId();
            result.leader = entity.get(Follow.class).getParent();
            return result;
        }

        @Override
        protected void updateObject(FollowerData t, Entity entity) {
        }

        @Override
        protected void removeObject(FollowerData t, Entity entity) {
        }
    }
}

Finally

You can extend this with an offset as at the moment the follower and the leader will have the same position, which might not be wished as you probably want to have the lamp, weapon, or tool in front of your character and not on the very same position. As well you could implement a smoothing when following something.

Thanks for reading. If you have questions, suggestions, or an oppinions leave a comment below.

cli

Software Developer

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments