Skip to content

Entity Component System

Jack Goldsworth edited this page Jul 7, 2019 · 13 revisions

Table of Contents

Entity Component System

This going to be a very basic overview of the DumbLibrary implementation of an Entity Component System, and how to use it.

To begin, let's iterate through what an Entity Component System, or ECS for short, actually is: In short, ECS is an architecture used to distribute properties to entities. It has two main parts: A Component, and a System. A component is the property itself, and is used to store values and configurations for said property. For example: Metabolism or a certain AI. A system is what makes changes to the corresponding component or entity. In DumbLibrary, the systems are ran every tick to update the components or entity.

If you're interested in reading more about ECS, click here for the wiki article.

Composable Creature Entity


Before we implement a component and system, you may want to have the entity you want to support components on extend the class ComposableCreatureEntity. This class extends the EntityCreature class and just makes things a little easier for you. If you really want, you can just implement the ComponentWriteAccess interface and add your own implementation.

Component Implementation


Let's walk through a very basic implementation of a component. For this example, I'm going to use the idea of our metabolism component.

This is what a basic component looks like:

public class MetabolismComponent implements FinalizableComponent {
    
    @Override
    public NBTTagCompound serialize(NBTTagCompound compound) {
       return null;
    }

    @Override
    public void deserialize(NBTTagCompound compound) {
    }

    @Override
    public void finalizeComponent(ComponentAccess entity) {
    }
}

Note: that the component class doesn't have to implement FinalizableComponent and may instead implement EntityComponent. FinalizableComponent implements EntityComponent and adds a method that will be called on the compontents initialization.

Now let's add some functionality to this component. What are some of the things a metabolism has? Food, Water, and the rate they decrease. So let's add these types of things into our component.

public class MetabolismComponent implements FinalizableComponent {

    public int food;
    public int water;
    public int foodRate;
    public int waterRate;

    @Override
    public NBTTagCompound serialize(NBTTagCompound compound) {
        compound.setInteger("food", this.food);
        compound.setInteger("water", this.water);
        return compound;
    }

    @Override
    public void deserialize(NBTTagCompound compound) {
        this.food = compound.getInteger("food");
        this.water = compound.getInteger("water");
    }

    @Override
    public void finalizeComponent(ComponentAccess entity) {
        // Other stuff you wanted to be done before the component is made.
    }

If you wanted to stop here, this could be your entire component. Remember that a component is really just an entity property. Most of the time they aren't going to be very large. However, we are going to add in some more functionality. DumbLibrary supports Component storage classes, which allows you to easily create components from Json values. Let's take a look at the storage class for the metabolism component.

public static class Storage implements EntityComponentStorage<MetabolismComponent> {

        // Max Food and water that the entity can have.
        private int maxFood;
        private int maxWater;
        // Rate that the food and water decrease every second
        private int waterRate = 1;
        private int foodRate = 1;

        @Override
        public MetabolismComponent construct() {
            MetabolismComponent component = new MetabolismComponent();
            component.food = this.maxFood;
            component.water = this.maxWater;
            component.waterRate = this.waterRate;
            component.foodRate = this.foodRate;
            return component;
        }

        @Override
        public void readJson(JsonObject json) {
            this.maxFood = json.get("max_food").getAsInt();
            this.maxWater = json.get("max_water").getAsInt();
            this.waterRate = json.get("water_rate").getAsInt();
            this.foodRate = json.get("food_rate").getAsInt();
        }

        @Override
        public void writeJson(JsonObject json) {
            json.addProperty("max_food", this.maxFood);
            json.addProperty("max_water", this.maxWater);
            json.addProperty("water_rate", this.waterRate);
            json.addProperty("food_rate", this.foodRate);
        }
    }

Remember that this is a nested class inside of the metabolism component.

Note: If there is anything about this that you don't understand or don't find intuitive, please ask us questions or snoop around the dumblibrary code to get a better understanding.

Registering a Component


Registering a new component is fairly easy. Just register it through a normal event like so:

@Mod.EventBusSubscriber(modid = YourMod.MODID)
@GameRegistry.ObjectHolder(YourMod.MODID)
public class EntityComponentTypes {

    public static final EntityComponentType<MetabolismComponent, MetabolismComponent.Storage> METABOLISM = InjectedUtils.injected();

    @SubscribeEvent
    public static void onRegisterComponents(RegisterComponentsEvent event) {
        event.getRegistry().register(
                SimpleComponentType.builder(MetabolismComponent.class, MetabolismComponent.Storage.class)
                        .withIdentifier(new ResourceLocation(YourMod.MODID, "metabolism"))
                        .withStorage(MetabolismComponent.Storage::new)
                        .withConstructor(MetabolismComponent::new)
                        .build()
        );
    }
}

Adding a Component to an Entity


First make sure properly registered your components. Once that's done, in your entities class, override the method attachComponents() and attach a component like so:

@Override
protected void attachComponents() {
    this.attachComponent(WhereYouRegisteredYourComponent.COMPONENT_NAME);
}

Then that's it, you have added your component to the entity.

System Implementation


Now let's walk through a basic implementation of a system. I'm going to continue to use the metabolism as an example. It's system can be found here

This is what the basic implementation of a system looks like:

public enum MetabolismSystem implements EntitySystem {
    INSTANCE;

    @Override
    public void populateBuffers(EntityManager manager) {
    }

    @Override
    public void update() {
    }
}

Note: We are using an enum because an enumeration is a singleton, and systems are something that need to always exist when the mod is running because they are called continuously.

Now let's add some functionality and explain what each line does.

public enum MetabolismSystem implements EntitySystem {
    // Grabs the instance of this enumeration.
    INSTANCE;

    // Creates a temporary MetabolismComponent Array
    private MetabolismComponent[] metabolism = new MetabolismComponent[0];
    // Creates an temporary Entity array
    private Entity[] entities = new Entity[0];

    @Override
    public void populateBuffers(EntityManager manager) {
        // Resolves all the information about the component, such as the entities with the component.
        EntityFamily family = manager.resolveFamily(EntityComponentTypes.METABOLISM);
        // Changes the metabolism array to a new array with the size of entities with this component.
        this.metabolism = family.populateBuffer(EntityComponentTypes.METABOLISM, this.metabolism);
        // Changes the entities array to an array containing all of the entities with this component.
        this.entities = family.getEntities();
    }

    @Override
    public void update() {
        // Goes through every component once a second and updates the food and water
        // based on the rates inside the metabolism.
        for (int i = 0; i < this.metabolism.length; i++) {
            if(this.entities[i].ticksExisted % 20 == 0) {
                MetabolismComponent meta = metabolism[i];
                meta.food -= meta.foodRate;
                meta.water -= meta.waterRate;
            }
        }
    }
}

Registering a System


To register a system, you need to use the RegisterSystemsEvent Like so:

@SubscribeEvent
public static void register(RegisterSystemsEvent event) {
    event.registerSystem(AgeSystem.INSTANCE);
    event.registerSystem(MultipartSystem.INSTANCE);
    event.registerSystem(MetabolismSystem.INSTANCE);
    event.registerSystem(HerdSystem.INSTANCE);
}