大烟枪

Now we can control our ship and even the invaders move around, but nobody shoots. Let's change that in our example game. We want to shoot those invaders. There is quite a couple of things involved by that. We need a bullet blender model first. To spare you some time you can have mine from my google drive as usual. Place this beside the ship and the invaders models. Don't forget to convert it to j3o by right clicking the blend file.

Let's fire then

First add the model name to the model component Model.class

    public final static String Bullet = "Bullet";

Now we need some additions in the control app state. Add the following private variable to the ControlAppState.class

    private static final String SHOOT = "SHOOT";

As well add the following line to the initialize method

        this.app.getInputManager().addMapping(SHOOT,
                new KeyTrigger(KeyInput.KEY_SPACE),
                new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
                
        this.app.getInputManager().addListener(actionListener, SHOOT);

We add first a mapping for the shooting to the space bar and the left mouse pointer to generate a SHOOT event. Further more we need an actionListener instead of an analogListener to detect your key press. To make it work you have to add as well a private actionListener instance

    private final ActionListener actionListener = (String name, boolean isPressed, float tpf) -> {
        if (name.equals(SHOOT) && !isPressed) {
            ship.applyChanges();
            ship.stream().findFirst().ifPresent(e -> {
                Vector3f shipLocation = e.get(Position.class).getLocation();
                EntityId bullet = ed.createEntity();
                ed.setComponents(bullet,
                        new Model(Model.Bullet),
                        new Position(new Vector3f(shipLocation.getX(), shipLocation.getY() + 3.5f, 0)));
            });
        }
    };

I think the purpose of this action listener is cleaer: It sets a bullet 3.5 units above our ship. Hit F6 and see what happens if you fire your bullets. Yes they are all motionless and freezed on the screen.

Move move move

What do we need to make those bullest move upwards? We need at least a speed component. This speed comonent gives the bullet as well a direction.

package mygame;

import com.simsilica.es.EntityComponent;

public class Speed implements EntityComponent {

    private final float speed;

    public Speed(float speed) {
        this.speed = speed;
    }

    public float getSpeed() {
        return speed;
    }

    @Override
    public String toString() {
        return "Speed[" + speed + "]";
    }
}

And we have to add a speed component to the generated bullet above in the action listener.

                ed.setComponents(bullet,
                        new Model(Model.Bullet),
                        new Position(new Vector3f(shipLocation.getX(), shipLocation.getY() + 3.5f, 0)),
                        new Speed(20));

Our bullet will reach the top most invader within 2 seconds to damage them. But still this won't move. So what shall we do? You guess it, we need a system for this. I call it BulletAppState.

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

public class BulletAppState extends AbstractAppState {

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet bullets;

    @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();

        bullets = ed.getEntities(Model.class, Position.class, Speed.class);
    }

    @Override
    public void update(float tpf) {
        bullets.applyChanges();
        bullets.stream().forEach((e) -> {
            Position position = e.get(Position.class);
            Speed speed = e.get(Speed.class);
            e.set(new Position(position.getLocation().add(0, tpf * speed.getSpeed(), 0)));
        });
    }

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

And now register that new BulletAppState to our Main

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

But what happens with those bullets? The do never disappear from the game if they do not hit anything. And at the moment they are not programmed to hit anything. So what shall we do with them? We could have some guards in the bullet app state. If you are new to this entity system then I guess you would do this in the BulletAppState. But there is a much more elegant way to handle that. It is time for the second desgin pattern in entity system programming. The decay system. This decay system can be used for all kind of garbage we want to remove from entity system and with that from the visual area as well. The decay system can be used not only for the bullets but for many other things, it is like magic, just need a decay component to mark something as a decay and the decay system will take care of it. this is my favorit design pattern.

Get ride of it

So what do we need to get ride of those bullets we fired at the invaders. We need a decay component

package mygame;

import com.simsilica.es.EntityComponent;

public class Decay implements EntityComponent {
    private final long start;
    private final long delta;

    public Decay( long deltaMillis ) {
        this.start = System.nanoTime();
        this.delta = deltaMillis * 1000000;
    }

    public double getPercent() {
        long time = System.nanoTime();
        return (double)(time - start)/delta;
    }

    @Override
    public String toString() {
        return "Decay[" + (delta/1000000.0) + " ms]";
    }
}

The decay component holds the percentage of the decay, you can handover how long the decay should last until gone. So the bullet generation also needs a decay component, so the decay system comming soon can get ride of the bullets not needed any more.

                ed.setComponents(bullet,
                        new Model(Model.Bullet),
                        new Position(new Vector3f(shipLocation.getX(), shipLocation.getY() + 3.5f, 0)),
                        new Speed(20),
                        new Decay(2000));

The bullet shall life for 2 seconds, so we are sure it will reach the last invader before getting removed. And here comes the decay system

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

public class DecayAppState extends AbstractAppState {
    private SimpleApplication app;
    private EntityData ed;
    private EntitySet decays;

    public DecayAppState() {
    }

    @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();
        this.decays = this.ed.getEntities(Decay.class);
    }

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

    @Override
    public void update(float tpf) {
        decays.applyChanges();
        decays.stream().forEach((e) -> {
            Decay decay = e.get(Decay.class);
            if (decay.getPercent() >= 1) {
                ed.removeEntity(e.getId());
            }
        });

    }
}

You see we are only interested in all entities which do have a decay system, no matter if it is a ship, a bullet or something else you could think of. If we want to get ride of something, we just have to add a decay component to that entity and we are done, the rest is handled by this amazing decay system, which is very small and easy to understand. I like this cleanness which can be found in many places in entity system driven code.