ECS Pattern: Virtual Link

The idea of links is that one entity points to another entity by storing the others entity id in the link component. When I now want to store the entity with the link and later reload that entity but the linked entity did change its entity id (remove/add) for some reason the loaded entity do point now to a wrong entity. This can lead to odd behaviours.

I'm not just a Number

The solution is very simple, I add two new components a virtual link and virtual id component. As well I implemented a system which checks if a virtual link and virtual id do match and then renewed the link components entity id. The benefit is that in the level designer I now really can give things a readable name not just a number. Another benefit is that I can easily say that item foo is linked with item bar even if item bar is in a completely different level. If I now place that item foo to the level where item bar resides the link automatically take place.

Name that thing

Ok let's head into more details and implementation of this idea. Normally a link component stores an entity id and looks that way

package example;

public class Link {
    private final EntityId id;
    
    public VirtualId(EntityId id) {
        this.id = id;
    }
    
    public EntityId getId() {
        return id;
    }
}

But a number is in always problematic. Human beings are normally not good in remember numbers beside some magical ones. A name - no matter how wired - is easier to remember than a number. I always hated those "structured" names for servers to make an example, for example server Obelix is way much easier to remember thant conchbe-123 (con stands for company name, ch for Switzerland and be for Bern). You even can easier assoziate something for Obelix (even if you have no clue who Obelix is) like "this is our solaris build machine in Bern". The same for EntityId which is also just a number. Further more if you shift all entities by 1923, because you removed it to switch to the next room and now switch back you will somehow have to remember that there is a shift by 1923 to correct the reloaded link which maybe still points to 45 but now needs a correction to 1968. Or imaginze you have level builder where you want to group/link elements togehter, if you use pure numbers it is hard in the builder to see the link, of course you might have some kind of a graphical help. Or you have to figure out some nasty problems with a debuger, names would be way much helpfull than numbers.
To avoid those kind of hazzles I introduced names for my entities. I named it virtual id, because the real id is still the EntityId. The counter part is a virtual link, because the real link still exist in my solution and the real one points to an entity id in the end.
Lets start with the virtual id component

package example;

public class VirtualId {
    private final String id;
    
    public VirtualId(String id) {
        this.id = id;
    }
    
    public String getId() {
        return id;
    }
}

The id is simply a string. So far it is simple. The VirtualLink looks pretty the same.

package example;

public class VirtualLink {
    private final String link;
    
    public VirtualLink(String link) {
        this.link = link;
    }
    
    public String getLink() {
        return link;
    }
}

Not much surprises.

Connect me with MrSmith

So to let it happen that they get a link component and which simply contains a entity id we need a virtual link system. The system creates/updates/removes a real link as soon both parts are there the virtual id and the virtual link. This also has the benefit that we are now able to define links between entities which are not yet there.

package example;

import example.Link;
import example.VirtualId;
import example.VirtualLink;
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.Map;
import java.util.Set;

public class VirtualLinkAppState extends AbstractAppState {

    private EntityData ed;
    private EntitySet virtualLinkEntities;
    private EntitySet virtualIdEntities;
    private final Map<EntityId, String> virtualLinks;
    private final Map<EntityId, String> virtualIds;
    private final Map<String, EntityId> virtualIdsToEntityIds;

    public VirtualLinkAppState() {
        this.virtualLinks = new HashMap<>();
        this.virtualIds = new HashMap<>();
        this.virtualIdsToEntityIds = new HashMap<>();
    }

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

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

        virtualLinkEntities = ed.getEntities(VirtualLink.class);
        virtualIdEntities = ed.getEntities(VirtualId.class);
    }

    @Override
    public void update(float tpf) {
        boolean changed = false;
        if (virtualIdEntities.applyChanges()) {
            changed = true;
            removeIdEntities(virtualIdEntities.getRemovedEntities());
            addIdEntities(virtualIdEntities.getAddedEntities());
            updateIdEntities(virtualIdEntities.getChangedEntities());
        }
        if (virtualLinkEntities.applyChanges()) {
            changed = true;
            removeLinkEntities(virtualLinkEntities.getRemovedEntities());
            addLinkEntities(virtualLinkEntities.getAddedEntities());
            updateLinkEntities(virtualLinkEntities.getChangedEntities());
        }

        if (changed) {
            Set<EntityId> vlinkIds = virtualLinks.keySet();
            for (EntityId vlinkId : vlinkIds) {
                EntityId entityId = virtualIdsToEntityIds.get(virtualLinks.get(vlinkId));
                if (entityId != null) {
                    ed.setComponents(vlinkId, new Link(entityId), new Visible());
                } else {
                    ed.removeComponent(vlinkId, Link.class);
                    ed.removeComponent(vlinkId, Visible.class);
                }
            }
        }
    }

    private void removeIdEntities(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            virtualIdsToEntityIds.remove(virtualIds.get(e.getId()));
            virtualIds.remove(e.getId());
        });
    }

    private void addIdEntities(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            virtualIds.put(e.getId(), e.get(VirtualId.class).getValue());
            virtualIdsToEntityIds.put(e.get(VirtualId.class).getValue(), e.getId());
        });
    }

    private void updateIdEntities(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            virtualIdsToEntityIds.remove(virtualIds.get(e.getId()));
            virtualIds.put(e.getId(), e.get(VirtualId.class).getValue());
            virtualIdsToEntityIds.put(e.get(VirtualId.class).getValue(), e.getId());
        });
    }

    private void removeLinkEntities(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            virtualLinks.remove(e.getId());
        });
    }

    private void addLinkEntities(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            virtualLinks.put(e.getId(), e.get(VirtualLink.class).getValue());
        });
    }

    private void updateLinkEntities(Set<Entity> entities) {
        addLinkEntities(entities);
    }

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

It is pretty simple. There are three book keepings one for the virtual ids, one for the virtual links and one for virtual links to entity id. The latter we need to easily get the real entity id for a virtual link via the virtual id.

The beef is in the third part of the upate loop

    if (changed) {
        Set<EntityId> vlinkIds = virtualLinks.keySet();
        for (EntityId vlinkId : vlinkIds) {
            EntityId entityId = virtualIdsToEntityIds.get(virtualLinks.get(vlinkId));
            if (entityId != null) {
                ed.setComponents(vlinkId, new Link(entityId), new Visible());
            } else {
                ed.removeComponent(vlinkId, Link.class);
                ed.removeComponent(vlinkId, Visible.class);
            }
        }
    }

Here we add, remove and update real links upon virtual ids and links.
That's it.

Comments

comments powered by Disqus