最后

Some simple effects would be cool. But how to achive that? For example we may want a crackle when a bullet hit an invader or some cool 8 bit shot sounds when we fire. And for sure we want to see some nice explosion and hear an impressive blast. You got the point.

Silent beauty

The exploison system will deal with all kind of dying entities. We need a tagging die component and a position to display a nice explosion. First some preparation. For the effect we need an explosion texture, you can take the one on my google drive as usual or design one your self, if you want to use my code 1:1 then your on one must be a 2x2 effect texture. Place that in the assets textures folder as flame.png

So let's start with the die component, pretty simple as it is only a tag.

package mygame;

import com.simsilica.es.EntityComponent;

public class Die implements EntityComponent {

    @Override
    public String toString() {
        return "Die";
    }
}

And then we need as well the explosion system, which looks similiar to the visual system we implemented in the beginning.

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.audio.AudioData.DataType;
import com.jme3.audio.AudioNode;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
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 ExplosionAppState extends AbstractAppState {

    private SimpleApplication app;
    private EntityData ed;
    private EntitySet entities;
    private final Map<EntityId, ParticleEmitter> models;
    private AudioNode sound;

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

    @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.entities = this.ed.getEntities(Die.class, Position.class);
        
        this.sound = new AudioNode(app.getAssetManager(), "Sounds/Explosion.wav", DataType.Buffer);
    }

    @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) {
            ParticleEmitter emitter = createExplosion();
            models.put(e.getId(), emitter);
            updateEffects(e, emitter);
            this.app.getRootNode().attachChild(emitter);
            this.sound.playInstance();
        }
    }

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

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

    private ParticleEmitter createExplosion() {
        ParticleEmitter fire = new ParticleEmitter("Emitter", ParticleMesh.Type.Triangle, 30);
        Material mat_red = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md");
        mat_red.setTexture("Texture", app.getAssetManager().loadTexture("Textures/flame.png"));
        fire.setMaterial(mat_red);
        fire.setImagesX(2);
        fire.setImagesY(2); // 2x2 texture animation
        fire.setEndColor(ColorRGBA.Red);
        fire.setStartColor(ColorRGBA.Yellow);
        fire.getParticleInfluencer().setInitialVelocity(new Vector3f(0, 1, 0));
        fire.setStartSize(1f);
        fire.setEndSize(6f);
        fire.setGravity(0, 0, 0);
        fire.setLowLife(0.4f);
        fire.setHighLife(1f);
        fire.getParticleInfluencer().setVelocityVariation(0.4f);

        return fire;
    }
}

So basically we hold all currently displaying effects in a map, like we did with the spatials in the visual state. This is kind a desing pattern you see often in entity systems. The updateEffects method is not really used in this example, but I leave it for the sake of completeness. The explanation for the createExplosion can be found in jME Hello Effects in the beginners section. Of course you have to register that state in the Main.class like all the others.

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

If you start your game you will not see any new. Yes of course not, we have to add a die entity for this. And we add this die entity where we delete our destroied invaders in the collision state. There is maybe a cleaner way to do this, but this is the simplest way, so let's do it simple.

We add a die entity with the die component and the position of the ship we delete in the update method of the CollisionAppState.class

                    if (defendingPart.get(Defense.class).getPower() <= 0) {
                        ed.removeEntity(defendingPart.getId());
                        Vector3f location = defendingPart.get(Position.class).getLocation();   //<---
                        ed.setComponents(ed.createEntity(),              // <---
                                new Die(),                               // <---
                                new Position(location, Vector3f.ZERO));  // <---
                    }

If done start your game again. You happily see the explosion... but you realize that it never ends. Not exactly what we wanted. The explosion doesn't need to last for ever it is enough to last for 1 second. So let's fix this.

Point of no return

The first idea you might have is to fix it in the explosion state, right? So there is the explosion so we do something with measuring time and stuff to get rid of it and handle this in the update method. But ... hold on, isn't there a simpler solution? A control maybe? Nope. We can add a decay component and let handle this by the decay system. That is done with one single line! And exploring this simplicity and reusabiltiy was my personal point of no return. After that I could not think of implementing a game without an entity component system in hand.

So let's do it, add a decay of 1 seconds to this die entity

                        ed.setComponents(ed.createEntity(),
                                new Die(),
                                new Decay(1000),   // <---
                                new Position(location, Vector3f.ZERO));

Hit F6 and see.

The source code for this case study can be found here.