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.