杀光它们

Yes exactly all those bullets and not effect, not hit, no explostion, how boring is that. Let's implement a collision system and let's damage those invaders with the ships bullets. For this we need a couple of things.

In this case study I left out that the invaders can shoot at the ship in the base.

What is my size

Let's introduce a simple collistion shape component where we just store a radius, this is by far good enough for the use case at hand.

package mygame;
import com.simsilica.es.EntityComponent;

public class CollisionShape implements EntityComponent {

    private final float radius;

    public CollisionShape(float radius) {
        this.radius = radius;
    }

    public float getRadius() {
        return radius;
    }

    @Override
    public String toString() {
        return "Radius[" + radius + "]";
    }
}

Now handle the colliders

Further more we need a system to handle the collision, let's start first with a simple one, we will improve this system step by step.

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


public class CollisionAppState extends AbstractAppState {

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet attackingParts;
    private EntitySet defendingParts;

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

        attackingParts = ed.getEntities(CollisionShape.class, Position.class);
        defendingParts = ed.getEntities(CollisionShape.class, Position.class);
    }

    @Override
    public void update(float tpf) {
        attackingParts.applyChanges();
        defendingParts.applyChanges();
        attackingParts.stream().forEach((attackingPart) -> {
            defendingParts.stream().forEach((defendingPart) -> {
                if (hasCollides(attackingPart, defendingPart)) {
                    // TODO: more actions will follow here
                }
            });
        });
    }

    private boolean hasCollides(Entity e1, Entity e2) {
        CollisionShape e1Shape = e1.get(CollisionShape.class);
        CollisionShape e2Shape = e2.get(CollisionShape.class);
        Position e1Pos = e1.get(Position.class);
        Position e2Pos = e2.get(Position.class);

        float threshold = e1Shape.getRadius() + e2Shape.getRadius();
        threshold *= threshold;
        float distance = e1Pos.getLocation().distanceSquared(e2Pos.getLocation());

        return distance < threshold;
    }

    @Override
    public void cleanup() {
        super.cleanup();
        attackingParts.release();
        attackingParts = null;
        defendingParts.release();
        defendingParts = null;
    }
}

To calculate if we have a collision I implemented a helper method. The method just calculates the distance and the threshold of the two involved radius and compares if the distance is inside the threshold. Pretty straight forward isn't it.

Further more I have define two sets of entities which come in handy later. For the moment this sets contains both the same, so I calculate the collision between all involved enties except the ship. I'm aware that this is not very performant if you have tousands of entities to check. We will improve this a litther further down.

One important thing, we now have to get every invader and every bullet a collision shape, else the collision system will never find them! In the GameAppState.class you have to replace the old code for setting the invader with the following one with the new collision shape component.

                this.ed.setComponents(invader,
                        new CollisionShape(1),  // <---
                        new Position(new Vector3f(x, y, 0)),
                        new Model(Model.BasicInvader));

As well in the ControlAppState, where you create those nice bullets above your ship, you have to add a collision shape component

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

Dont forget to add that new system to your Main.class

    public Main() {
        super(new VisualAppState(),
                new DecayAppState(),
                new ControlAppState(),
                new InvadersAIAppState(),
                new CollisionAppState(),  // <---
                new BulletAppState(),
                new GameAppState(),
                new EntityDataState());
    }

Fire up your game with F6. Yeah I know there is no new behaviour visible. We have to do a little more, at least the TODO we should fill with life, but even that is not enough.

Might the force be with you

My idea to deal with only the interested entities is to have an attack and a defence component. The bullet only have a attack component and the invaders only have a defense component. On collision both component will be calculated against each other. If either the attack or the defense is used up that component will be deleted. Let's say our bullet do have attack of 1 and the invader do have defense of 2 on collision. After calculation the bullet do have an attack of 0 and the invader do have a defense of 1. The bullet disappears. That easy.

We need an attack component...

package mygame;

import com.simsilica.es.EntityComponent;

public class Attack implements EntityComponent {

    private final double power;

    public Attack(double power) {
        this.power = power;
    }

    public double getPower() {
        return power;
    }

    @Override
    public String toString() {
        return "Attack[" + power + "]";
    }
}

... and a defense component.

package mygame;

import com.simsilica.es.EntityComponent;

public class Defense implements EntityComponent {

    private final double power;

    public Defense(double power) {
        this.power = power;
    }

    public double getPower() {
        return power;
    }

    @Override
    public String toString() {
        return "Defense[" + power + "]";
    }
}

Looks quite straight forward and holds just the attack respective defense power.

We are nearly done. We need to adjust the collistion system to be able to deal with attack and defense component and react if one or the other is used up.

Lets first change the query of the attackParts and the defendParts by Attack.class and Defense.class to get only the interessted parties and not everything.

        attackingParts = ed.getEntities(Attack.class, CollisionShape.class, Position.class);
        defendingParts = ed.getEntities(Defense.class, CollisionShape.class, Position.class);

This as well reduce the amount of elements we have to compare against each other.

This alone is not sufficent, as you track now the attack and defense component as well we have to improve the creation of the invaders and the bullets once again with the following change

GameAppState.class

                this.ed.setComponents(invader,
                        new Defense(2),  // <---
                        new CollisionShape(1),
                        new Position(new Vector3f(x, y, 0)),
                        new Model(Model.BasicInvader));

ControlAppState.class

                ed.setComponents(bullet,
                        new Model(Model.Bullet),
                        new Attack(1),  // <---
                        new CollisionShape(0.25f),
                        new Position(new Vector3f(shipLocation.getX(), shipLocation.getY() + 3.5f, 0)),
                        new Speed(20),
                        new Decay(2000));

Finally we replace the TODO in the collision app state with the following snippet

                    Attack attack = attackingPart.get(Attack.class);
                    Defense defense = defendingPart.get(Defense.class);
                    attackingPart.set(new Attack(attack.getPower() - defense.getPower()));
                    defendingPart.set(new Defense(defense.getPower() - attack.getPower()));
                    if (attackingPart.get(Attack.class).getPower() <=0 ) {
                        ed.removeEntity(attackingPart.getId());
                    }
                    if (defendingPart.get(Defense.class).getPower() <= 0) {
                        ed.removeEntity(defendingPart.getId());
                    }

And we are done. We now calculate the defense and the attack against each other, if one of them is equal or below zero we remove it. Looks simple, right? It is simple.