Skip to content

Move it Around

ia97lies edited this page Oct 4, 2017 · 6 revisions

Yes there is no point in call that a game when nothing moves on the screen. Not even our ship in the base is controlable in any way. At least we should be able to control the ship, move it left and right. I show you a way to do this by moving the mouse pointer left and right. Sounds promising, no?

Ok here it comes a minimalistic approach, further details for those action listerners can be found here.

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.input.MouseInput;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntitySet;
import com.simsilica.es.Filters;

public class ControlAppState extends AbstractAppState {

    private static final String MOVE_RIGHT = "MOVE_RIGHT";
    private static final String MOVE_LEFT = "MOVE_LEFT";

    private SimpleApplication app;
    private EntityData ed;
    private Vector3f position;
    private EntitySet ship;

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

        super.initialize(stateManager, app);
        this.app = (SimpleApplication) app;

        ed = this.app.getStateManager().getState(EntityDataState.class).getEntityData();
        ship = ed.getEntities(
                Filters.fieldEquals(Model.class, "name", Model.SpaceShip),
                Model.class,
                Position.class
        );

        this.position = new Vector3f(0, -20, 0);
        this.app.getInputManager().addMapping(MOVE_LEFT, new MouseAxisTrigger(MouseInput.AXIS_X, true));
        this.app.getInputManager().addMapping(MOVE_RIGHT, new MouseAxisTrigger(MouseInput.AXIS_X, false));

        this.app.getInputManager().addListener(analogListener, MOVE_LEFT, MOVE_RIGHT);
    }

    private final AnalogListener analogListener = (String name, float value, float tpf) -> {
        if (name.equals(MOVE_LEFT) || name.equals(MOVE_RIGHT)) {
            Vector2f mousePos = app.getInputManager().getCursorPosition();
            float x = FastMath.clamp((mousePos.getX() - app.getCamera().getWidth() / 2) * 0.05f, -22, 22);
            position = new Vector3f(x, -20, 0);
        }
    };

    @Override
    public void update(float tpf) {
        ship.applyChanges();
        ship.stream().findFirst().ifPresent(e -> {
            e.set(new Position(position));
        });
    }

    @Override
    public void cleanup() {
    }
}

We could have more than one ship, so we just take the first we find. In our example we only have one ship as we did not add more than one ship. But maybe we could use that to have three ships, a ship for a live for example and display the awailable ships a little smaller on the top left of the screen. The following snippet gets the entities by model name, I used a filter for this.

        ship = ed.getEntities(
                Filters.fieldEquals(Model.class, "name", Model.SpaceShip),
                Model.class,
                Position.class

Just that you know something like that exist. The filter only works if we also have the Model.class in the entity query. We also could use a tag like component with no data at all just an empty component to tag the active ship we want to use. I actually don't know which way is better and it mostly depends. For now this approach above is good enough.

In the analog listener we read out the x axis of the mouse pointer in the game screen and use this to calculate the ship x position. It's the cheapes way and good enough for this article though. You can improve this and make it more "pysical" than my version if you like and feel the need. But this position itself will not move our ship, this is done here in the update loop of this state. Remember? All states do have an update loop?

    public void update(float tpf) {
        ship.applyChanges();
        ship.stream().findFirst().ifPresent(e -> {
            e.set(new Position(position));
        });
    }

As you can see I just pick the first ship I can find in the ship entity set (ok I know there is only one in it anyway) and set the new position. Immutual, remember?

Register the controler system to the Main.class

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

And do now my ship move? Really? Really, with the help of the visual state app we hacked in the last blog article. Cool isn't it? I can move that ship without touching a single line in any other state we have so far, just add this system. But of course if we will have new systems which needs different components on the ship, invaders, what so ever, we have to touch code to inject those components. Resist to solve that in an object oriented way or you will end up pretty soon in a mess you can hardly wade out of. Entity system is not object oriented it is data driven, don't mix. At least don't mix on the same abstraction level.

Flyin' invaders

The ship moves now controlled by our mouse movement but the invaders still are completly motionless. To make the invaders move I will add some sort of a very very simple AI state.

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.Entity;
import com.simsilica.es.EntityData;
import com.simsilica.es.EntitySet;
import com.simsilica.es.Filters;

public class InvadersAIAppState extends AbstractAppState {

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet invaders;
    private float xDir;
    private float yDir;

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

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

        invaders = ed.getEntities(
                Filters.fieldEquals(Model.class, "name", Model.BasicInvader),
                Model.class,
                Position.class);
        xDir = 1f;
        yDir = -1f;
    }

    @Override
    public void update(float tpf) {
        invaders.applyChanges();
        wabbeling(tpf);
    }

    private void wabbeling(float tpf) {
        float xMin = 0;
        float xMax = 0;
        float yMin = 0;
        float yMax = 0;

        for (Entity e : invaders) {
            Vector3f location = e.get(Position.class).getLocation();
            if (location.getX() < xMin) {
                xMin = location.getX();
            }
            if (location.getX() > xMax) {
                xMax = location.getX();
            }
            if (location.getY() < yMin) {
                yMin = location.getY();
            }
            if (location.getY() > yMax) {
                yMax = location.getY();
            }            
            e.set(new Position(location.add(xDir * tpf * 2, yDir * tpf * 0.5f, 0)));
        }
        if (xMax > 22) {
            xDir = -1;
        }
        if (xMin < -22) {
            xDir = 1;
        }
        if (yMax > 20) {
            yDir = -1;
        }
        if (yMin < 0) {
            yDir = 1;
        }
    }

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

Don't forget the register AI system in the Main.class

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

The invaders now do move left to right and up and down in a grid like formating flight. Enough to show you more chilling entity system approaches. But you can see even now how clean the representation and the logic is separated.

Let's improve that to make the visual a little more Wow. To make it look cooler and more vivid rotate the invaders around there y axis. I use the position component and add there as well a rotation. Of course you also can add a rotation component and do it that way, but then you have to touch more code. So just replace your current position component with the following code

package mygame;

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

public class Position implements EntityComponent {

    private final Vector3f location;
    private final Vector3f rotation;

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

    public Vector3f getLocation() {
        return location;
    }

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

Now we have to deal as well with the rotation in the VisualAppState system. So let's slightly change the updateModelSpatial method of it like that

    private void updateModelSpatial(Entity e, Spatial s) {
        Position p = e.get(Position.class);
        s.setLocalTranslation(p.getLocation());
        float angles[] = new float[3];
        angles[0] = p.getRotation().x;
        angles[1] = p.getRotation().y;
        angles[2] = p.getRotation().z;
        s.setLocalRotation(new Quaternion(angles));
    }

Maybe there is a more elegant way doing this, it's just the way I know. By now the visual state can handle as well rotation. To make them rotate we have to improve our InvadersAIAppState slightly. Replace the code of the wabbeling method in InvadersAIAppState with the following snippet.

    private void wabbeling(float tpf) {
        float xMin = 0;
        float xMax = 0;
        float yMin = 0;
        float yMax = 0;

        for (Entity e : invaders) {
            Vector3f location = e.get(Position.class).getLocation();
            if (location.getX() < xMin) {
                xMin = location.getX();
            }
            if (location.getX() > xMax) {
                xMax = location.getX();
            }
            if (location.getY() < yMin) {
                yMin = location.getY();
            }
            if (location.getY() > yMax) {
                yMax = location.getY();
            }            
            Vector3f rotation = e.get(Position.class).getRotation();
            rotation = rotation.add(0, tpf * FastMath.DEG_TO_RAD * 90, 0);
            if (rotation.y > FastMath.RAD_TO_DEG * 360) {
                rotation.setY(rotation.getY() - FastMath.DEG_TO_RAD * 360);
            }
            e.set(new Position(location.add(xDir * tpf * 2, yDir * tpf * 0.5f, 0), rotation));
        }
        if (xMax > 22) {
            xDir = -1;
        }
        if (xMin < -22) {
            xDir = 1;
        }
        if (yMax > 20) {
            yDir = -1;
        }
        if (yMin < 0) {
            yDir = 1;
        }
    }

Only this has actually changed

            Vector3f rotation = e.get(Position.class).getRotation();
            rotation = rotation.add(0, tpf * FastMath.DEG_TO_RAD * 90, 0);
            if (rotation.y > FastMath.RAD_TO_DEG * 360) {
                rotation.setY(rotation.getY() - FastMath.DEG_TO_RAD * 360);
            }
            e.set(new Position(location.add(xDir * tpf * 2, yDir * tpf * 0.5f, 0), rotation));

and gives a 90 degree per second y axis rotation to all invaders. I added some logic to clamp the rotation to 360 degrees.

Previous | Next

Clone this wiki locally