ECS Pattern: Model Visualization

To understand and try out entity component system approach the visual system was the first hurdle I had to take. Without that system you see nothing on the screen. Further more this system makes you understand how you should separate game logic from the visual stuff.

Anti Pattern

Don't store your spatial, the visual thing, in a component. Never ever. It is as if you would save Java classes in databases. It is backward. When ever you face your self to store a real thing in component it is a sign of wrong design.
Also think a bit bigger, if you start with a simple drawn character and you want later improve it all your saved game states are garbage then, because they still held the old crappy character. So a good decoupling helps you also to exchange your model at any time.

Model Component

First thing you will need is a model component. The model component just contains the name of the model and not the model data itself. If you have the name you can load the model by this name.

package my.game;
import com.simsilica.es.EntityComponent;

public class Model implements EntityComponent {
    private final String name;

    public Model(String name) {
        this.name = name;
    }

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

Position Component

The next thing you need is the position information. I normally place rotation and location in this component, but there might be cases where you don't want them combined.

package my.game;

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

public class Position implements EntityComponent {

    private final Vector3f location;
    private final Quaternion rotation;

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

    public Position(Vector3f location) {
        this(location, new Quaternion());
    }

    public Position() {
        this(Vector3f.ZERO);
    }
    
    public Vector3f getLocation() {
        return location;
    }

    public Quaternion getRotation() {
        return rotation;
    }

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

I will not go into more details here as I did it in other blog posts, I guess it is clear anyway.

Visual System

Now we need the system which cares about entities with a model and a position component and updates it when ever the position changes, an endity disappears or a new entity appears. The code of that looks like so

package my.game;

import ch.artificials.rose.components.Model;
import ch.artificials.rose.components.Position;
import ch.artificials.rose.components.Type;
import ch.artificials.rose.models.ModelFactory;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
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 VisualAppState extends AbstractAppState {

    private final Map<EntityId, Node> models;

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet visibleEntities;
    private final Conditional<Type> selectable;
    private BatchNode batchTiles;

    public VisualAppState(Conditional<Type> selectable) {
        this.models = new HashMap<>();
        this.selectable = selectable;
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.app = (SimpleApplication) app;
        ed = this.app.getStateManager().getState(SavableEntityDataState.class).getEntityData();
        visibleEntities = ed.getEntities(Model.class, Position.class);
    }

    @Override
    public void cleanup() {
    }

    @Override
    public void update(float tpf) {
        if (visibleEntities.applyChanges()) {
            removeVisibles(visibleEntities.getRemovedEntities());
            addVisibles(visibleEntities.getAddedEntities());
            updateVisibles(visibleEntities.getChangedEntities());
        }
    }

    private void removeVisibles(Set<Entity> entities) {
        entities.stream().forEach((e) -> {
            removeVisible(e);
        });
    }

    private void removeVisible(Entity e) {
        Spatial s = models.remove(e.getId());
        if (s == null) {
            logger.warn("Model not found for removed entity: " + e);
        } else {
            s.removeFromParent();
        }
    }

    private void addVisibles(Set<Entity> entities) {
        for (Entity e : entities) {
            addVisible(e);
        };
    }

    private void addVisible(Entity e) {
        Node node = createVisual(e);
        this.app.getRootNode().attachChild(node);
        updateVisible(e, node);
        private final Map<EntityId, Node> models;
    }

    private void updateVisibles(Set<Entity> entities) {
        for (Entity e : entities) {
            updateVisible(e, models.get(e.getId());
        }
    }

    private void updateVisible(Entity e, Node node) {
        Position position = e.get(Position.class);
        node.setLocalTranslation(position.getLocation());
        node.setLocalRotation(position.getRotation());
    }

    public Node createVisual(Entity e) {
        Model model = e.get(Model.class);
        Node node = app.getAssetManager.load("Models/" + model.getName());
        return node;
    }
}

The most important part of this is

private final Map<EntityId, Node> models;

Which is for the local book-keeping of the models, which is done by the addVisibles, removeVisibles and updateVisibles methods. When ever an entity appears, disapears or changes it's position this system will be get triggered to update the view. Be aware that changing the model will trigger the update method but do have no effect, you would have to improve the update visible to see changed models as well (for simple games this is not needed).

That's it.

Comments

comments powered by Disqus