ECS in Java

Started by
7 comments, last by macmanmatty 4 years, 3 months ago

I was making a game (2D RPG) in java using libGDX. Started making classes Top Down aka Thing → Item → Animated Item → DirectionalAnimatedItem → Weapon → MeeleWeapon → MetalMeeleWeapon → Sword → FireSword → SuperFireSword → DeamonsSword. This quickely became a nightmare so I moved everything into nice packages and it became less of nightmare, but still a nightmare none the less. After much refractoring I realized that no class beyond MeeleWeapon was needed. So away they went. I replaced the damage methods with functional interfaces that one could extend and use to set the various methods for functionally of objects and what did when various things happened like collide or equip or swing. That seemed to good to me. But now after doing some good reading on ECS I am beginning to see some advantages to it. If I were create an ECS system in java I would have an entity with an id and hash maps of boolean , string, and numeric stats to hold the data and texture region to draw it. Corrrect? like this

public class Entity extends Actor  {
Map < String, DoubleStat>  numericStats= new HashMap<String, DoubleStat>();
Map < String, BooleanStat>  booleanStats= new HashMap<String , StringStat>();
Map < String, StringStat>  stringStats= new HashMap<String, StringStat>();
String id;
AtlasRegion region;
@Override
public void draw(Batch batch , float alpha){

}


}

then have base class called system that acts on entities like this and implements actual Systems by extending it.


public  abstract class System{
 abstract act(Entity entity);
}

then in the game loop method call and just loop through every system and entity having the system check whether or not the entity has any data to manipulate?

void gameLoop() {
for(int count=0; count<systems.size(); count++){
for(int count2=0; count2<entities.size(); count2++){
system.get(count).act(entities.get(count2);
}

This seems like you have to create a system for every action? Like move, explode on contact , kill enemies on contact , change to a tree on contact and it seems like you have the inverse problem of my first solution and you wind up with hundreds of systems/ components for every action you want to have in the game (but they are easily attached).

Is ECS really that great? or my current sorta mix of the two just as good? I like the fact any entity can be given any component but is seams now you're just creating as many Systems / components as my first solution though they can be resued. Although maybe I don't quite understand ECSs totally.


Advertisement

macmanmatty said:
MetalMeeleWeapon → Sword → FireSword → SuperFireSword → DeamonsSword

I am curious what unique gameplay features they offer.

macmanmatty said:
This seems like you have to create a system for every action?

Many similar actions can be grouped into a single System.

In general cases, looping should be done by a certain component type, not every entity.

class SystemWeaponGadget{
    void updateAllFire(){
        for(turret: getAllComponent<Fire>()){  … }
    }
    void updateAllDaemonLike(){
        for(radar: getAllComponent<Deamons_like>()){ … }
        for(radar: getAllComponent<Deamons_like>()){ … }  // sometimes, I need second pass
    }
}
//e.g. in game loop, just call SystemWeaponGadget.updateAllFire(), then SystemWeaponGadget.updateAllDaemonLike()

macmanmatty said:
… Like move, explode on contact , kill enemies on contact , change to a tree on contact

I registered callback to physics engine like below. This way, all enemy-related will be handled by only 1 system.

class System_Enemy extends PhysicCallback{
   void ini_register(){
      physicEngine→register(this);
   }
   //override
   void physic_callback(PHYSIC_COMMUNICATION ph){
       PhysicObject a=ph.a; …
       //do BOTH explode on contact &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp; kill enemies on contact <== if you think it is similar / should be grouped
   }
   //something else related to enemy (a lot of util/update methods)
};

macmanmatty said:
Is ECS really that great?

No. It is a tool that highly suitable in many situation though.

IMHO, in Java, even in the best implementation, some major ECS advantages like contiguous-memory will be missed.

hyyou said:

IMHO, in Java, even in the best implementation, some major ECS advantages like contiguous-memory will be missed.

I have the same thought. Class members might not sit to each others even if they are members of the same class, as they can be a reference to value stored somewhere else in the memory.

http://9tawan.net/en/

@hyyou

so with your method I am having to do a bunch of instanceof checks on marker interfaces or check if entity has the needed stats before adding it to the list? That seems inefficient.

Half dipping into an ECS is going to leave you with all the issues an ECS is supposed to solve with all the complexity that an ECS brings on. I'd say choose one and stick with the choice. I personally would never consider anything else, but it's up to you.

Here's how I set up an ECS. It doesn't multithread well, but outside of that it works [i]really[/i] well.

Entities are just a number. That's it. Not even a class. Just an integer. The ECS has a set of maps, one for each type of component, mapping the EntityID to it's corresponding component, if it has one. So there isn't an Actor class at all. Just an ECS class to hold all the maps.

Next, a component is POD - “plain old data." Just a bag of ints, floats, strings, ect. Absolutely no code, unless a constructor is absolutely necessary for some reason. Most implementations wont even require you to inherit from some Component class/interface at all, because there's nothing to inherit.

Next, an event is also POD, just like a component, with all the same considerations.

Last you have a System. A System registers a callback function to trigger any time a specific event is broadcast to the ECS. As a side note, this should actually be a queue sorted by priority. MANY systems might want to trigger on any given event. You should also have a SINGLE place outlined where each system listener callback's priority is defined so the order is known without magic constants floating around coupling your code everywhere. All of my System callbacks take only a reference to the event in question and the ECS itself. The event itself tells you which entity to fetch from the ECS. There's absolutely no coupling.

All of your code should be in a system, in response to some event. So there's no need for your draw function. There should be a system, perhaps DrawSpriteSystem, that listens on RenderEvent's. The DrawSpriteSystem then queries the ECS for all entities with a SpriteComponent (and I use an “ActiveEntityComponent” to turn on and off entities easily) and uses the data from the SpriteComponent to carry out the draw.

The beauty of this? Consider basic movement controls. The game fires off an ActorStepEvent every logic frame. The PlayerControlSystem listens for this, and checks the input. If the left key is pressed, it calculates the new position and fires off a BodyMoved event. After the BodyMoved event resolves, use the data in the event to move to the new position. Why? Because two yeas later, when I want to expand the gameplay, I can add a ConfusionSystem that listens to the BodyMoved event and reverses the direction of motion. The PlayerControlSystem doesn't have to know anything about the ConfusionSystem to work with it just fine. And how do I know where to draw the sprite? Sprites aren't part of the BodyComponent, that stores just physics information. The SpriteComponent stores the index, location and rotation of the sprite to draw. I have a SpriteSystem listens for BodyMoved events (usually one of the last on the priority list) and updates the SpriteComponent accordingly. The SpriteSystem also listens for RenderEvents and draws the sprite on the screen based on the data in the SpriteComponent. NONE of this is coupled.

Your Entity::draw function is already a point of coupling. Why should an entity handle it's own draw code? In fact, being just a number, it shouldn't handle anything.

You will get a large bag of systems, events and components. Even a basic game has something like a CollisionSystem (collision detection, though no response) a RenderSystem (drawing) a SpriteSystem (updating animations, positions, etc), PlayerControlSystem and an AIScriptSystem (this is where I put my collision response). You'll get events up the wazoo - RenderEvent, CollisionEvent, BodyMovedEvent, TakeDamageEvent, LandFromJumpEvent, ect. And you WANT lots of events… because you can flexibly inject responses to those events however you need to. What if you want to make the screen shake every time the player lands? Or take damage from some debuff? Or make the player bounce every time he lands? That's easy to tack on (or remove) - for instance, just create a PoisonSystem that listens for LandFromJumpEvents and fires off a TakeDamageEvent if the entity named in the event also has a JumpPoisoned component. You want a lot of systems also. Clearly defined systems with clear responses to specific events make for easy to follow code and code that can be applied to all sorts of things (the SpriteSystem flat out ignores any entity without a SpriteComponent, and I can plug ANY entity into it just by slapping on the correct component).

Halfway doing this eliminates all of the flexibility. It introduces nasty code coupling that's almost impossible to sort through when a game gets sufficiently large. You also have to try to figure out who is responsible for what, when.

I'd say, give a full bodied ECS a try. You might be shocked. I was.

How do I make this method into ECS public void eat(Human human);? Where eat is implemented by food objects , and each food object does something vastly different to the human eating it? ECS is seeming to make implementing something that very hard. ECS seems to be a design pattern created for poor initial design / planning of the games or those who do not want to design anything upfront. Everything I read says later on if you want to do something like this ECS makes it easier.

macmanmatty said:

How do I make this method into ECS public void eat(Human human);? Where eat is implemented by food objects , and each food object does something vastly different to the human eating it? ECS is seeming to make implementing something that very hard. ECS seems to be a design pattern created for poor initial design / planning of the games or those who do not want to design anything upfront. Everything I read says later on if you want to do something like this ECS makes it easier.

There are no food objects, but there might be an entity with a component like “EatibleComponent”

Just off the top of my head, I would guess that an EatibleComponent includes information on the effects of eating it. Let's use a classic example: On a given frame, Kirby walks into a maxtomato. Just like every other frame, the CollisionSystem will respond to the ActorStepEvent, but since kirby is overlaping the maxtomato it will broadcast a CollisionEvent (my games actually broadcast two, one with source = kirby and other = maxtomato, and a second with source = maxtomato and other = kirby). The EatingSystem will listen for CollisionEvents - if the collision source has a MouthComponent and the other has an EatibleComponent, it will do whatever the eatible component describes. That's not a FUNCTION, the eatible component has the DATA to carry out whatever the effect is. I would have a “hp-restored” member value and simply add that to the kirby entity's HealthComponent "current-health" member value. If you REALLY want to be flexible, I could see scripting powerups in python/lua/whatever and so the EatibleComponent would simply contain the name of the correct python script and whatever arguments it requires, and the EatingSystem would actually execute the python. But that's not necessary for, say, a Mario game with very limited numbers of power ups. Just keep in mind, you use this exact same pattern for really, really complicated games like a roguelike, where there might be 500 different potions… or sword effects… etc.

ECS actually makes implementation simpler. You are probably stuck in what I call “object oriented hell" paradigm. ECS is a VERY different, data driven paradigm. Yes, we're still using objects (a system is an object, a component is an object, an event is an object, the ECS itself is an object) but we're not defaulting to EVERYTHING being an object. There is no MaxTomato object. There is an entity (which is JUST a number!) that serves as an index to a table/map within the ECS that stores an EatibleComponent. That EatibleComponent contains the data for running a script that changes the “other” (in this case Kirby's) current health (which is going to be in ANOTHER component. Probably a ActorLifeComponent or something similar). But if you've played Kirby you know that not every food item restores max health. You can easily make a second entity (NUMBER!) that has an EatibleComponent who's hp-restored value is 50 instead of 100.

As for your “poor design” statement, that's pretty telling, because it has nothing to do with designing beforehand or after. Flexibility isn't “poor design” and games will change, often dramatically, as development progresses. Whole parts of the game will be added or removed based on playtesting and quality assurance alone. You still can, and should, know the gest of what is going into your game and have that written out in a well formed design document. ECS doesn't prevent you from doing anything you would have done without it, but it allows you to make changes without massive code refractors. Coupling, as your method is dependent upon, is actually horrible design. Minimal coupling is the ideal. Trust me, once you work on bigger projects with multiple people, you will PRAY there's no coupling to deal with.

I'm finally figuring ECS Out! The work flow is easier and I have gotten much more done the same amount of time than I ever did with OOP. To solve the flexibility problem each entity has stat component that has 3 hash maps of various stat kinds boolean, numeric, and string. Entities that change stats have stat changeable component that changes a stat if the other entity has it.

This topic is closed to new replies.

Advertisement