2
\$\begingroup\$

I'm currently working on my own game engine for learning purposes, so I've implemented the first iteration of an entity component system using Java 8.

For my approach, I decided to not use any entity class for the system as an entity only holds data and so there is no class required to list all the components itself. This also means that I've got a Component which holds the data and a System which can have access to multiple components (for example a movement system which required data from the position and velocity component).

My systems also have a render method available (thought render systems are the only one who uses them), because the frame rate is not the same as the ticks.

In my ExitSystem, the InputMultiplexer is just for testing purpose there.

How can I improve the code and mainly the system I've got?

The EntityManager:

public class EntityManager {

private ArrayList<Integer> entities = new ArrayList<>();
private ArrayList<ISystem> componentSystems = new ArrayList<>();

public void addEntity(int ID, ISystem... system) {
    this.entities.add(ID);
    this.componentSystems.addAll(Arrays.asList(system));
}

public void init() {
    for(ISystem system : this.componentSystems)
        system.init();
}

public void update() {
    for(ISystem system : this.componentSystems)
        system.update();
}

public void render() {
    for(ISystem system : this.componentSystems)
        system.render();
}

public void dispose() {
    for(ISystem system : this.componentSystems)
        system.dispose();
}

public int getEntity(int id) {
    return this.entities.get(id);
}
}

The IEntityComponent:

public interface IEntityComponent {

}

The ISystem:

public interface ISystem {

public void init();

public void update();

public void render();

public void dispose();

public IEntityComponent[] get();
}

The ExitData:

public class ExitData implements IEntityComponent {

public int exit = GLFW_KEY_ESCAPE;
public int action = GLFW_REPEAT;
}

The ExitSystem:

public class ExitSystem implements ISystem {

@Override
public void init() {
    InputMultiplexer.getInput().addComponent(new InputComponent.InputKeyComponent() {

        @Override
        public boolean onKeyPress(int key, int action) {
            if(key == ((ExitData)get()[0]).exit && action == ((ExitData)get()[0]).action)
                Game.windowShouldClose();
            return true;
        }
    });
}

@Override
public void update() {

}

@Override
public void render() {

}

@Override
public void dispose() {

}

@Override
public IEntityComponent[] get() {
    return new IEntityComponent[] {new ExitData()};
}
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Please don't do major changes to code after answers have been received. This would invalidate answers, and ruin ongoing work by other reviewers. You could propose an answer to your own question, and explain why those changes are good changes. \$\endgroup\$ – holroy May 13 '17 at 15:35
  • \$\begingroup\$ Ok, I've moved the code now into an answer with explanation as to why I changed the code. Thought the system is still the same with one minor change. Just the components and systems to test the code have changed for visualisiation of component interaction. \$\endgroup\$ – ShadowDragon May 13 '17 at 15:48
  • \$\begingroup\$ Please see what you may and may not do after receiving answers. \$\endgroup\$ – Simon Forsberg May 13 '17 at 16:11
3
\$\begingroup\$

Components should hold information, that is / can be different from one instance to another; in your example hardcoded ExitData values with conjunction of creating new instances on each get call make it little different to just using a global field. Frankly I am not sure if Exit should even be a system as such.

Your systems should be getting components in packs (grouped by entity) from common pool (instead of creating it each for it self), and then depending on the kind of components there are in said pack, do something with them or not.

I don't understand what your entities list is supposed to do, you don't use it. Also you put values by value, but retrieve by index, which smells like a bug.

I think your problem is that you chose a bad testing case. Something more appropriate would be:

  • Make Position (with x and y fields) and Velocity (with xvel and yvel) components.

  • Make Velocity System, which in update loop for entities with Position and Velocity components does x += xvel; y += yvel;

  • Make 'Render' System, which simply prints Position values.

  • Add entities, which all have Position component, optionally have Velocity component, and the starting values of the components is different.

This way you would test against systems interacting on components from same pool, systems choosing to work against component set that matches criteria, grouping components by their entity.

--

By saying Your systems should be getting components in packs (grouped by entity) from common pool (instead of creating it each for it self) I meant to say there is no composition relation between systems and components. Your systems should not have components directly attached to it.

Here is minimal example of ECS with the position, velocity and 'rendering' components. It is not optimized, lacks encapsulation, manager misses few methods (at the very least deleteEntity), but I hope it captures the spirit in least lines of code possible.

public class Component {

    public final static int POS = 1 << 0,
                            VEL = 1 << 1,
                            RENDER = 1 << 2;

    public static class Velocity {
        float velx, vely;
    }

    public static class Position {
        float x, y;
    }

    public static class Rendering {
        String name; // for sake of having at least one field inside.
    }
}

--

public class Systems {

    public static class Velocity {
        public void update(EntityManager manager) {
            int required_components = Component.POS | Component.VEL;
            for (int i = 0; i < manager.size; i++) {
                if ((manager.flag[i] & required_components) == required_components){
                    manager.pos[i].x += manager.vel[i].velx;
                    manager.pos[i].y += manager.vel[i].vely;
                }
            }
        }
    }

    public static class Render {
        public void update(EntityManager manager) {
            int required_components = Component.POS | Component.RENDER;
            for (int i = 0; i < manager.size; i++) {
                if ((manager.flag[i] & required_components) == required_components) {
                    System.out.println(String.format("%s: (%f x, %f y)", manager.rendering[i].name, manager.pos[i].y, manager.pos[i].y));
                }
            }
        }
    }

}

--

public class EntityManager {
    public int flag[];
    public Component.Position pos[];
    public Component.Velocity vel[];
    public Component.Rendering rendering[];
    public final int size;

    public EntityManager(int size) {
        this.size = size;
        pos = new Component.Position[size];
        vel = new Component.Velocity[size];
        rendering = new Component.Rendering[size];
        flag = new int[size];
    }

    public int createEntity(int flag) {
        for (int i = 0; i < size; i++){
            if (this.flag[i] == 0) {
                this.flag[i] = flag;
                if ((flag & Component.POS) > 0) pos[i] = new Component.Position();
                if ((flag & Component.VEL) > 0) vel[i] = new Component.Velocity();
                if ((flag & Component.RENDER) > 0) rendering[i] = new Component.Rendering();
                return i;
            }
        }
        return -1;
    }
}

--

public class GameContainer {

    public static void main(String[] args) {
        new GameContainer();
    }

    EntityManager manager;
    Systems.Render renderingSystem;
    Systems.Velocity velocitySystem;

    GameContainer() {
        manager = new EntityManager(5);
        renderingSystem = new Systems.Render();
        velocitySystem = new Systems.Velocity();

        int id = manager.createEntity(Component.POS | Component.VEL | Component.RENDER);
        if (id > -1) {
            manager.pos[id].x = 10;
            manager.pos[id].y = 10;
            manager.vel[id].velx = 1;
            manager.vel[id].vely = 1;
            manager.rendering[id].name = "player";
        }

        id = manager.createEntity(Component.POS | Component.RENDER);
        if (id > -1) {
            manager.pos[id].x = 0;
            manager.pos[id].y = 0;
            manager.rendering[id].name = "tree";
        }
        startGameLoop();
    }

    void startGameLoop() {
        while(true) {
            velocitySystem.update(manager);
            renderingSystem.update(manager);
        }
    }

}
\$\endgroup\$
  • \$\begingroup\$ Ok, I updated now my code. Could you update your answer to match the new code? Also, what do you mean by Your systems should be getting components in packs (grouped by entity) from common pool (instead of creating it each for it self) \$\endgroup\$ – ShadowDragon May 13 '17 at 15:28
  • \$\begingroup\$ I posted now the new code which was changed with your suggestions as an extra answer as the difference was quite huge with adding a complete new example. \$\endgroup\$ – ShadowDragon May 13 '17 at 15:42
  • \$\begingroup\$ Nope, Exit should definitely not be a system. Just because you're using a Entity-Component-System framework doesn't mean that everything has to be within that framework. Some general GUI stuff should be taken care of outside of the ECS world. \$\endgroup\$ – Simon Forsberg May 13 '17 at 16:23
  • \$\begingroup\$ @ShadowDragon updated \$\endgroup\$ – Coderino Javarino May 13 '17 at 17:09
0
\$\begingroup\$

Just did some changes to the code. The entitieslist does no longer exist now as it was never used. The IEntityComponent has now also a new method to be able to get the entity to which this component belongs.

The next change I did was to change the example systems and entities to completly new examples as these examples give a better overview of the system and the interaction between different components than the code from before had done.

The EntityManager:

public class EntityManager {

private ArrayList<ISystem> componentSystems = new ArrayList<>();

public void addEntity(ISystem... system) {
    this.componentSystems.addAll(Arrays.asList(system));
}

public void init() {
    for(ISystem system : this.componentSystems)
        system.init();
}

public void update() {
    for(ISystem system : this.componentSystems)
        system.update();
}

public void render(double currentTime) {
    for(ISystem system : this.componentSystems)
        system.render(currentTime);
}

public void dispose() {
    for(ISystem system : this.componentSystems)
        system.dispose();
}
}

The IEntityComponent:

public interface IEntityComponent {

public int getEntityID();
}

The ISystem:

public interface ISystem {

public void init();

public void update();

public void render(double currentTime);

public void dispose();

public IEntityComponent[] get();
}

The ComponentPosition:

public class ComponentPosition implements IEntityComponent{

public float posX;
public float posY;
public float posZ;

private int entityID;

// z axis exists for rendering system to determine if the object should be drawn on top
// of something else with a smaller posZ value

public ComponentPosition(int entityID, float posX, float posY, float posZ) {
    this.entityID = entityID;
    this.posX = posX;
    this.posY = posY;
    this.posZ = posZ;
}

@Override
public int getEntityID() {
    return this.entityID;
}
}

The ComponentVelocity:

public class ComponentVelocity implements IEntityComponent {

public float velX;
public float velY;

private int entityID;

// movement on the z axis is not allowed!

public ComponentVelocity(int entityID, float velX, float velY) {
    this.entityID = entityID;
    this.velX = velX;
    this.velY = velY;
}

@Override
public int getEntityID() {
    return this.entityID;
}
}

The SystemMovement:

public class SystemMovement implements ISystem {

private ComponentVelocity vecocity;
private ComponentPosition position;

public SystemMovement(ComponentPosition pos, ComponentVelocity vel) {
    this.position = pos;
    this.vecocity = vel;
}

@Override
public void init() {
    // can be called on initialisiation of the scene
}

@Override
public void update() {
    // each tick (currently 60t/s)

    this.position.posX += this.vecocity.velX;
    this.position.posY += this.vecocity.velY;
}

@Override
public void render(double currentTime) {
    // fps varies
}

@Override
public void dispose() {
    // on deletion to clear for example OpenGL stuff when used for rendering
}

@Override
public IEntityComponent[] get() {
    return new IEntityComponent[] {this.position, this.vecocity};
}
}

The SystemRenderer:

public class SystemRenderer implements ISystem {

private ComponentPosition position;

public SystemRenderer(ComponentPosition pos) {
    this.position = pos;
}

@Override
public void init() {
    // can be called on initialisiation of the scene
}

@Override
public void update() {
    // each tick (currently 60t/s)
}

@Override
public void render(double currentTime) {
    // fps varies
    if(this.position.getEntityID() == 0) // just for testing to see the change in player pos
        return;

    System.out.println("______________________________________");
    System.out.println("Entity: " + this.position.getEntityID());
    System.out.println(this.position.posX);
    System.out.println(this.position.posY);
}

@Override
public void dispose() {
    // on deletion to clear for example OpenGL stuff when used for rendering
}

@Override
public IEntityComponent[] get() {
    return new IEntityComponent[] {this.position};
}
}

The SystemPosition:

public class SystemPosition implements ISystem {

private ComponentPosition position;

public SystemPosition(ComponentPosition pos) {
    this.position = pos;
}

@Override
public void init() {
    // can be called on initialisiation of the scene
}

@Override
public void update() {
    // each tick (currently 60t/s)
}

@Override
public void render(double currentTime) {
    // fps varies
}

@Override
public void dispose() {
    // on deletion to clear for example OpenGL stuff when used for rendering
}

@Override
public IEntityComponent[] get() {
    return new IEntityComponent[] {this.position};
}
}

The TestScene:

public class TestScene implements Scene {

private EntityManager manager = new EntityManager();

@Override
public boolean init() {
    InputMultiplexer.getInput().addComponent(new InputComponent.InputKeyComponent() {

        @Override
        public boolean onKeyPress(int key, int action) {
            if(key == GLFW_KEY_ESCAPE && action == GLFW_REPEAT)
                Game.windowShouldClose();
            return true;
        }
    });

    ComponentPosition posPlayer = new ComponentPosition(1, 14, 15, 0);

    this.manager.addEntity(new SystemPosition(new ComponentPosition(0, 12, 13, 1)), new SystemRenderer(new ComponentPosition(0, 12, 13, 1)));
    this.manager.addEntity(new SystemMovement(posPlayer, new ComponentVelocity(1, 1, 1)),
            new SystemRenderer(posPlayer));

    this.manager.init();
    return true;
}

@Override
public void update() {
    this.manager.update();
}

@Override
public void render(double currentTime) {
    this.manager.render(currentTime);
}

@Override
public void dispose() {
    this.manager.dispose();
}
}

Note:

  • the Entity with the ID 0 could be for example a tree
  • the Entity with the ID 1 could be a player or a moving entity (thought AI is missing)
\$\endgroup\$
  • \$\begingroup\$ If you are looking for another review, please post a new question instead of an answer. \$\endgroup\$ – Simon Forsberg May 13 '17 at 16:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.