ECS Pattern: Decay

This blog post is based on Entity Component System are Crazy Cool.
There are so many things in-game that have a lifetime. Bullets, effects, debris, blood, vanishing tiles in a can't stop running-game, and many more things which have a lifetime.
In the past, I would have done it separately depending on which situation something hast to vanish. With the entity component system you can solve all of them with one component and one system. The decay component and decay system.

I use jMonkeyEngine with Zay-ES written in Java.

Component

Bullets, debris, blood are represented by entities with a certain set of components in our game world. A bullet entity for example has a model component, a position component, and a damage component. For the decay system, we need a decay component we can attach to those entities to make them disappear after a while. Instead of detecting where the bullet is, you simply remove the bullet after a while if it didn't hit something. No need to detect where the bullet is right now, just remove it after let's say a second.

public class Decay implements EntityComponent {

    public long timeout;

    public Decay(long timeoutMs) {
        this.timeout = timeoutMs;
    }

    public long getTimeoutMs() {
        return timeout;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "[" + timeout + " ms" + "]";
    }
}

On creation, you define the timeout for the decay component. The timeout in ms does define how long the entity shall live.

System

The decay system does the calculation if the entity's lifetime has been reached. It doesn't matter if it is a bullet, an effect, debris, or even the space station. And that is the shiny beauty of an entity component system. You can give something a lifetime in your game at any time. You can decide to add a lifetime to something even very late in the development process. You can decide this a day before you launch the game as it is simply a one-liner by adding a decay component to that entity. No refactoring or code moving involved at all.

Skeleton

Let's start with a very basic system and set up everything we need, like the entity data and the decay container.

public class DecaySystem extends BaseAppState {

    private EntityData ed;
    private DecayContainer decayContainer;

    public DecaySystem() {
    }

    @Override
    protected void initialize(Application app) {
        Main main = (Main) app;
        ed = main.getStateManager().getState(EntityDataState.class).getEntityData();
    }

    @Override
    protected void cleanup(Application app) {
    }

    @Override
    protected void onEnable() {
        decayContainer = new DecayContainer();
        decayContainer.start();
    }

    @Override
    protected void onDisable() {
        decayContainer.stop();
        decayContainer = null;
    }

    @Override
    public void update(float tpf) {
            decayContainer.update();
        }

    private class DecayData {
    }

    private class DecayContainer extends EntityContainer<DecayData> {

        DecayContainer() {
            super(ed, Decay.class);
        }

        Stream<DecayData> stream() {
            return Arrays.stream(getArray());
        }

        @Override
        protected DecayData addObject(Entity entity) {
            DecayData data = new DecayData();
            return data;
        }

        @Override
        protected void updateObject(DecayData data, Entity entity) {
        }

        @Override
        protected void removeObject(DecayData data, Entity entity) {
        }
    }
}

It does nothing yet. We will now fill in the needed code. Which is not really a lot. Very simple.

Decay Entity Container

Let's begin with the Zay-ES entity container to get all the added, removed, and updated entities with a decay component attached. In the addObject() method we create our local decay data to handle the timeout. I almost always have a local data representation with some additional logic. We store the entity id and the timeout in milliseconds. That's it.
You can add as well logic for the updateObject() method to be able to update the decay of an entity during your gameplay, think of a plant that got some water or fertilizer or tool which got repaired.

    private class DecayContainer extends EntityContainer<DecayData> {

        DecayContainer() {
            super(ed, Decay.class);
        }

        Stream<DecayData> stream() {
            return Arrays.stream(getArray());
        }

        @Override
        protected DecayData addObject(Entity entity) {
            long timeoutMs = entity.get(Decay.class).getTimeoutMs;
            DecayData data = new DecayData(entity.getId(), timeoutMs);
            return data;
        }

        @Override
        protected void updateObject(DecayData data, Entity entity) {
                }

        @Override
        protected void removeObject(DecayData data, Entity entity) {
        }
    }

Local Decay Data

The local decay data class handles the timeout. We store the start time and the timeout and with getPercent() method we return the decay as a value 0 (no decay) and 1 (fully decayed). Very simple logic.

    private class DecayData {

        EntityId id;
        private long start;
        public long timeout;

        DecayData(EntityId eId, long timoutMs) {
            this.id = entity.getId();
            this.start = System.nanoTime();
            this.timeout = timeoutMs * 1000000;
        }

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

Update

And finally, in the update method, we loop over all decay data and remove those which are fully decayed.

    @Override
    public void update(float tpf) {
        decayContainer.update();
        decayContainer.stream().forEach(e -> {
            if (e.getPercent() >= 1.0) {
                    ed.removeEntity(e.id);
            }
        });
    }

Additionally

You can implement as well visualization systems to make the decay of a thing like a plant or a tool visible and give feedback to the player.

The End

That's it. Thanks for reading. If you have suggestions or questions just write a comment below.

Christian Liesch

Software Developer

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.