Entity Component System Part 2

So where exactly is this entity system then

The first part just dealed with the setup and some rough skeletons and a meager ship on the screen, but still no entity system. Well we did setup one, but did not use so far. In this part we will really use it. Promise.

We want invaders

Indeed an invader game without invaders is not an invader game, so fire up again your blender and model your invader. Of course I have one for you on my google drive to share with you. I called it BasicInvader.

And now my friend we will use the entity system to hold the data of the a bunch of invaders and a star ship on the base. What do we need to specify the ship and the invaders to appear on the screen? We need a position and a model to be able to print it on the screen. Ok normaly you would now start with some kind of game object and inherit from that your ship and your invader and so on and so on. Maybe you would abuse the JME Spatial to even hold some game logic on it and start pretty soon a messy spagetti like architecture. At least this happens to me. So it is a very good idea to separate logic from visualisation. With an entity system you do this in it's extrem. With the entity system we can extend our ship at any time with what ever component we think we need without touching much of the existing code, mostly none at all. Even the data and the methods are separated which is completly agains object oriented programming and if you deep in object oriented programming this approach may or may not confuse you. But be patient with me and with your self.

What's my position

Our ship but as well our invaders need a position and that is done with a position component.

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 + "]";
    }
}

You see the component is immutual and pure data, that helps to run systems in different threads without bodering about synchronization. We will deal with multithreading later.

Who the heck am I

Of course that we now which model we should load we need a model component as well. Looks pretty much like the position component above and is pure data.

package mygame;

import com.simsilica.es.EntityComponent;

public class Model implements EntityComponent {
    private final String name;
    public final static String SpaceShip = "SpaceShip";
    public final static String BasicInvader = "BasicInvader";

    public Model(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    @Override
    public String toString() {
        return "Model[" + name + "]";
    }
}

To avoid typos I introduced some static strings for space ship and basic invader ships. Now how does this come into play?

Generate entites

Finally we will use the entity system to store the data of the invaders and the ship currently no graphical representation.
Entities are just ids nothing more. You have to add components to this entity to give it a meaning. And then you query entities with specific components, in our case Position and Model. It's like a database.
So what we need now is a GameAppState where we build up our levels, do the transitions from start to play, from play to dead and from dead to reborn and all that kind of stuff. But let's keep it stupid simple for the first and flesh it out later.

package mygame;

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.math.Vector3f;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntityId;

public class GameAppState extends AbstractAppState {

    private EntityData ed;
    private SimpleApplication app;

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

        this.app = (SimpleApplication) app;
        this.app.setPauseOnLostFocus(true);
        this.app.setDisplayStatView(true);

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

        EntityId ship = ed.createEntity();
        this.ed.setComponents(ship,
                new Position(new Vector3f(0, -20, 0)),
                new Model(Model.SpaceShip));
        for (int x = -20; x < 20; x += 4) {
            for (int y = 0; y < 20; y += 4) {
                EntityId invader = ed.createEntity();
                this.ed.setComponents(invader,
                        new Position(new Vector3f(x, y, 0)),
                        new Model(Model.BasicInvader));
            }
        }
    }

    @Override
    public void cleanup() {
    }

    @Override
    public void update(float tpf) {
    }

}

So we only generate the ships that's all, enought to show you the first ES system.

Show what you have

And here it comes .... the first entity system to display the generated invaders. And our ship of course. Just replace your current VisualAppState with the following one.

package mygame;

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.light.DirectionalLight;
import com.jme3.math.Vector3f;
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 SimpleApplication app;
    private EntityData ed;
    private EntitySet entities;
    private final Map<EntityId, Spatial> models;
    private ModelFactory modelFactory;

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

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

        ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        entities = ed.getEntities(Position.class, Model.class);

        app.getCamera().lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
        app.getCamera().setLocation(new Vector3f(0, 0, 60));

        DirectionalLight light = new DirectionalLight();
        light.setDirection(new Vector3f(1, 1, -1));
        this.app.getRootNode().addLight(light);

        modelFactory = new ModelFactory(this.app.getAssetManager());
    }

    @Override
    public void cleanup() {
        entities.release();
        entities = null;
    }

    @Override
    public void update(float tpf) {
        if (entities.applyChanges()) {
            removeModels(entities.getRemovedEntities());
            addModels(entities.getAddedEntities());
            updateModels(entities.getChangedEntities());
        }
    }

    private void removeModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.remove(e.getId());
            s.removeFromParent();
        }
    }

    private void addModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = createVisual(e);
            models.put(e.getId(), s);
            updateModelSpatial(e, s);
            this.app.getRootNode().attachChild(s);
        }
    }

    private void updateModels(Set<Entity> entities) {
        for (Entity e : entities) {
            Spatial s = models.get(e.getId());
            updateModelSpatial(e, s);
        }
    }

    private void updateModelSpatial(Entity e, Spatial s) {
        Position p = e.get(Position.class);
        s.setLocalTranslation(p.getLocation());
    }

    private Spatial createVisual(Entity e) {
        Model model = e.get(Model.class);
        return modelFactory.create(model.getName());
    }
}

this is by the way some kind of the first design pattern in entity system programming, as you will use this in all your games for sure at least on the client side.

Details please

So what do we have here. First we define the set of entities we want to operate on

    ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
    entities = ed.getEntities(Position.class, Model.class);

We are interested in all entities with a position and a model. This works for invaders and for our space ship as both have those components.

The next part is the update loop, the heart of every game. JME offers that for every application state you register.

public void update(float tpf) {
    if (entities.applyChanges()) {
        removeModels(entities.getRemovedEntities());
        addModels(entities.getAddedEntities());
        updateModels(entities.getChangedEntities());
    }
}

We check if there are changes in the entity set and then handle the removed entities, the added entities and the changed entities. All spatials and the corresponding entity id is managed with a "local" hash map

private final Map<EntityId, Spatial> models;

That way we can remove an added entity from the visual node later. Let's directly jump into removeModels method.

private void removeModels(Set<Entity> entities) {
    for (Entity e : entities) {
        Spatial s = models.remove(e.getId());
        s.removeFromParent();
    }
}

The spatial is hold in the models hash map and get with the removed entity id, so I can remove it from the visual node. That's all.
Ok befor we can remove something it should be added some time befor of course. Added entites are handled with addModels method

private void addModels(Set<Entity> entities) {
    for (Entity e : entities) {
        Spatial s = createVisual(e);
        models.put(e.getId(), s);
        updateModelSpatial(e, s);
        this.app.getRootNode().attachChild(s);
    }
}

You can see that we hold all created spatials in the models hash map and add them as well to the root node to make them visual.

The updateModel is quite simple and just update the position of a spatial which corresponding entity id signals changes.

In the end you have to register all those new app states to Main like this

public Main() {
    super(new VisualAppState(),
            new GameAppState(),
            new EntityDataState());
}

That's it. Your first ES driven piece of software. I will come up with more additional stuff to make the entites move and the ship controllable. In the next part then. Stay tuned.

Comments

comments powered by Disqus