动起来

如果屏幕中的东西都不会动,是不能称其为做游戏的。现在基地中的太空船都不受任何控制,至少我们应该能够操作它左右移动才对。下面将演示如何通过左右移动鼠标指针,来控制飞船运动。听起来不错,是吧?

当然这只是一种非常简单的做法,如果你想详细了解如何处理用户输入可以看看这个

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() {
    }
}

游戏中可能有多个太空船,而我们只控制其中的一个(实体)。在本例中,我们只创建了一艘太空船。也许我们可以创建三个太空船:一个用来表示活动的太空船,另外的就缩小一点放在屏幕左上角,用来表示玩家还有几条命。

在下面的代码片段中,我使用了过滤器(Filter)来实现根据模型的名称查询太空船实体的功能。

        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?

    @Override
    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.