ECS Question for you fellow tiger hatters


  • Jammer

    So as I worked on my libgdx jam entry, and global game jam this weekend, i’ve noticed something i’ve been doing with an entity. Where I create an entity called Instructions. I might then attach a component to it InstructionsStateComponent. It has a reference to an InstructionState enum.

    I then create an InstructionSystem. Since I just have one Instructions entity in the game, I pass it as a reference through the constructor & store as a property, vs doing the loop inside update:

    for (int i = 0; i < entities.size(); i++) {
      if (entities.get(i) instanceof Instructions) {}
    }
    

    It’s worth noting that Ashley (ECS libgdx framework) has functions to filter the entities array by components, so i’ve already done that.

    So is this smart to do? Should I pass just the one instance to the system’s constructor & store as a property, or should i look it up as if it were a collection?

    I also wonder with something like instruction & instruction state component, if it’s worth having an entity for. Can a system simply store state when it’s small enough? Or is that what leads to bad code over time?


  • LDG

    haha we talked today about how we haven’t answered this yet ;)

    I think there’s some confusion around the nomenclature – are Instructions what they sound like? Is it tutorial code, or some kind of instructions to provide the player?


  • Jammer

    It’s simply an entity. In this case it holds a component that store data, relevant to displaying instructions on the screen.

    Here’s the full code of the game (made for global game jam):

    https://github.com/agmcleod/RitualOfConversation/blob/master/core/src/com/agmcleod/ritual_of_conversation/

    More specifically the entity code:

    package com.agmcleod.ritual_of_conversation.entities;
    
    import com.agmcleod.ritual_of_conversation.components.ComponentMappers;
    import com.agmcleod.ritual_of_conversation.components.InstructionStateComponent;
    import com.badlogic.ashley.core.Entity;
    
    /**
     * Created by aaronmcleod on 2016-01-31.
     */
    public class Instructions extends Entity {
        public Instructions() {
            add(new InstructionStateComponent());
        }
    
        public InstructionStateComponent getInstructionStateComponent() {
            return ComponentMappers.instructionstate.get(this);
        }
    }
    

    Then the component:

    package com.agmcleod.ritual_of_conversation.components;
    
    import com.badlogic.ashley.core.Component;
    
    /**
     * Created by aaronmcleod on 2016-01-31.
     */
    public class InstructionStateComponent implements Component {
        public InstructionState instructionState = InstructionState.NONE;
        public String instructionText = null;
    }
    

    InstructionState is an enum representing different instructions that should be shown at some point in the game.

    In a system, I can set it up to store an ImmutableArray of entities filtered by one or many compontents. I do this in the movement system for example:

    package com.agmcleod.ritual_of_conversation.systems;
    
    import com.agmcleod.ritual_of_conversation.RitualOfConversation;
    import com.agmcleod.ritual_of_conversation.actors.AwkwardBarActor;
    import com.agmcleod.ritual_of_conversation.components.PlayerComponent;
    import com.agmcleod.ritual_of_conversation.components.TransformComponent;
    import com.agmcleod.ritual_of_conversation.entities.Player;
    import com.badlogic.ashley.core.Engine;
    import com.badlogic.ashley.core.Entity;
    import com.badlogic.ashley.core.EntitySystem;
    import com.badlogic.ashley.core.Family;
    import com.badlogic.ashley.utils.ImmutableArray;
    import com.badlogic.gdx.Gdx;
    import com.badlogic.gdx.math.Vector2;
    
    /**
     * Created by aaronmcleod on 2016-01-29.
     */
    public class MovementSystem extends EntitySystem {
        private ImmutableArray<Entity> entities;
        private final float PLAYER_VELOCITY = 500;
        public void addedToEngine(Engine engine) {
            entities = engine.getEntitiesFor(Family.all(PlayerComponent.class).get());
        }
    
        @Override
        public void update(float dt) {
            for (int i = 0; i < entities.size(); ++i) {
                Player player = (Player) entities.get(i);
    
                TransformComponent transformComponent = player.getTransform();
                Vector2 position = transformComponent.position;
                if (player.isMovingLeft()) {
                    position.x -= PLAYER_VELOCITY * dt;
                } else if (player.isMovingRight()) {
                    position.x += PLAYER_VELOCITY * dt;
                }
    
                float hWidth = transformComponent.width / 2;
    
                if (position.x - hWidth < 0) {
                    position.x = hWidth;
                } else if (position.x + hWidth > RitualOfConversation.GAME_WIDTH - AwkwardBarActor.BORDER_WIDTH) {
                    position.x = RitualOfConversation.GAME_WIDTH - AwkwardBarActor.BORDER_WIDTH - hWidth;
                }
            }
        }
    }
    

    The movement system at this point just cares about the player, but it keeps track of entities based on a specific component. For the InstructionsSystem, i didn’t do this, but instead would pass the one entity I wanted to keep track of directly. This is a little big on code, so my apologies:

    package com.agmcleod.ritual_of_conversation.systems;
    
    import com.agmcleod.ritual_of_conversation.components.InstructionState;
    import com.agmcleod.ritual_of_conversation.components.InstructionStateComponent;
    import com.agmcleod.ritual_of_conversation.entities.Instructions;
    import com.badlogic.ashley.core.EntitySystem;
    
    /**
     * Created by aaronmcleod on 2016-01-31.
     */
    public class InstructionSystem extends EntitySystem {
        private final float INSTRUCTION_TIMEOUT = 3.5f;
        private Instructions instructions;
        private float instructionTimer;
        private boolean showingInstruction;
    
        public InstructionSystem(Instructions instructions) {
            this.instructions = instructions;
            showingInstruction = false;
        }
    
        public String getTextForInstructionState(InstructionState instructionState) {
            switch (instructionState) {
                case COLLISION_CHOICE:
                    return "There may be more than one response to give.";
                case AWKWARDNESS_BAR:
                    return "As you respond, the awkwardness of the conversation may go up or down.";
                default:
                    return null;
    
            }
        }
    
        public boolean isShowingInstruction() {
            return showingInstruction;
        }
    
        public void nextInstructionState(InstructionState requestedState) {
            InstructionStateComponent instructionStateComponent = instructions.getInstructionStateComponent();
            InstructionState instructionState = instructionStateComponent.instructionState;
            int compare = requestedState.compareTo(instructionState);
            boolean instructionChanged = true;
            if (compare == 1) {
                switch (requestedState) {
                    case COLLISION_CHOICE:
                    case AWKWARDNESS_BAR:
                        showingInstruction = true;
                        break;
                    case COLLISION_CHOICE_VIEWED:
                    case DONE:
                        showingInstruction = false;
                        break;
                }
                instructionState = requestedState;
            } else {
                instructionChanged = false;
            }
    
            if (instructionChanged) {
                instructionStateComponent.instructionState = instructionState;
                String instructionText = getTextForInstructionState(instructionStateComponent.instructionState);
                instructionStateComponent.instructionText = instructionText;
                if (instructionText != null) {
                    instructionTimer = INSTRUCTION_TIMEOUT;
                }
            }
        }
    
        @Override
        public void update(float dt) {
            if (instructionTimer > 0) {
                instructionTimer -= dt;
                if (instructionTimer <= 0) {
                    InstructionState instructionState = instructions.getInstructionStateComponent().instructionState;
                    switch (instructionState) {
                        case COLLISION_CHOICE:
                            nextInstructionState(InstructionState.COLLISION_CHOICE_VIEWED);
                            break;
                        case AWKWARDNESS_BAR:
                            nextInstructionState(InstructionState.DONE);
                            break;
                    }
                }
            }
        }
    }
    

    The update which is called every frame uses the property of the InstructionsEntity to figure out what state the game is in for the instructions.


  • Tiger Hat

    I think that for the instructions it seems that an entity isn’t really necessary. Unless you are attaching help instructions to other entities, like player, or a mob, or a sign in the game world or something. If it’s just going to be a global state, then it could just be a system I think.

    Tho, I suppose having a single entity in the game world that has the instructions component to keep track of the state doesn’t hurt anything.


  • Jammer

    Yeah i thought about just storing it in the system, but storing that kind of state didn’t feel quite right, but I also agree that it doesn’t really feel like an actual entity. I always felt a bit silly in Unity adding an empty game object, just so i had something to attach scripts to.


  • Tiger Hat

    @agmcleod said:

    I always felt a bit silly in Unity adding an empty game object, just so i had something to attach scripts to.

    You can use a singleton pattern without a gameobject if you don’t inherit from monobehaviour.


Log in to reply