Too much abstraction?

So, I started working on abilities. By abilities, I mean magic and spells and whatnot. To be precise, an ability is any way the an entity can change the scene or the game world.

Unsurprisingly, my abilities mostly create other entities that do stuff. For example, an ability create a entity that does damage when it collides with another entity. Baby stuff, right?

My engine uses a Entity-Component System. I sacrifice type safety for flexibility. Here’s the problem. Any concrete ability details relies on certain components. Someone, me, can easily remove necessary components. My code crashes and burns.

I could just prevent access to the entity model and only allow changes through some mutators. The problem with this is that my ECS completely loses its purpose. I might as well created classes for every single entity and allow access.

I had the brilliant idea to remove all specifics (projectiles, aoe, etc) and define an ability as a series of events with a specific entry point for running (when the ability is casted and using an optional target). This way, I don’t need to worry about relying on anything because I do not use anything. All I do is start the event. This is an especially good idea because it more faithful to what it happening, i.e. an ability creates a projectile and not an ability is a projectile.

I just wonder if this is too much abstraction. It comes dangerously close to programming with objects, particularly when it comes to modifying entities. Creating an entity can be done with one event but modifying is trickier because there are hundreds of different ways to modify an entity. I am, for sure, not making a event for every mutator.

Sounds like typical over-engineering to me.
Same for entity-component systems.

When a spell is activated, I want to see one place of code where it’s done. There might be other actors involved in the game’s ecosystem and they might be notified by events for the benefit of loose coupling, but for the main part there should be one place.

Highly recommend:

It sounds like you are new to ECS / component architectures (CA)… You should probably use one that someone else created first; Artemis-odb being a good candidate. Learn how it works then move on from there as there are some tell-tale problems in what you’ve described in your own effort. First, it’s absolutely possible to create an ECS with Java that is dynamic and type safe. Regarding being able to lock down an Entity or component manager consider a specialized system which has callbacks referenced in the Entity / component manager implementation itself for mutating methods like add / remove / set of components. The internal implementation before completing each operation can callback pre & post actions for the this particular component type offering the ability for pre actions to veto add / remove / set operations. Create a FinalizedSystem component which vetos all mutation operations and once added to an Entity then no longer can components be added or removed. Also consider enum sets and depending on your game create one data component with one or more enum sets tracking relevant data for the game at hand. This is useful for state tracking. Extra credit to create an extensible enum set that can accept more than one extensible enum running at similar performance to EnumSet (limited to one enum as things go). Eventbus / message passing goes hand in hand with ECS / CA and is rather handy with communication between data / system components across module / logical boundaries. A bit of a dump of things that can be done, but it’s completely possible to do great things with ECS / CA and Java while keeping things manageable for the developer using this architecture.

So yeah… examine others work in this area before striking out on your own for sure. The lack of type safety in your efforts do indicate that things are going down the wrong path.

I’ve made my decision.

I am going ahead with events. If I find that some events are really common, I will add them in by default. To avoid the programming with objects problem, I will stay away from small events that rely on each. Instead I would create a single more powerful event, or, in layman terms, code.

I will ignore type safety. I cannot imagine a solution that does not introduces an annoying amount of complexity. Instead I probably code with the assumption that I was dumb enough to remove necessary components. Or let it crash and not try to fix a leaky pipe.

When you say events what are you really saying? The listener pattern or something else? I highly recommend that you do not adopt the old listener pattern and look at the eventbus. From Guava or elsewhere. Though don’t use Guava!

Indeed it can be tricky coding to make a type safe component architecture really flexible with Java as it takes an advanced knowledge of generics / particularly generic methods. I’ll give a basic example though.

import java.util.HashMap;

public class ComponentManager
    HashMap<Class, ?> componentMap;

    public ComponentManager()
        componentMap = new HashMap<Class, ?>();

    public <T> T getAs(Class<T> componentType)
        return (T)componentMap.get(componentType);

    public <T> void set(Class<T> componentType, T component)
        componentMap.put(componentType, component);

// Simple usage...

ComponentManager entity = new ComponentManager();

entity.set(PlayerState.class, new PlayerState()); // Let's say it has a public health field.

if (entity.getAs(PlayerState.class).health >= 100) { /* yeah!!!! */ };

The above is the simplest type safe example of implicit composition. From there the rabbit hole goes much deeper. The nice thing though with the component architecture approach is that you can have all data fields publicly scoped in the component as there is still type safety and a query from the component manager before things are accessible. IE no “getHealth” method necessary with PlayerState.

By events, I mean listeners, observers, scripts or whatever you call it. Its all the same thing; dynamically added code that runs when something happens.

By type safety, I meant my ECS returning null for objects. Any accessor can return null but that’s usually a bug and/or can be prevented. For ECS, you cannot easily prevent someone from removing a component because you cannot remove mutators w/o breaking your ECS.

In regard to your casting tip, I’ve had that since day 1. Casting at one place is much better at casting at 100 others.

Yes, look beyond the listener / observer pattern; it’s not the '90s anymore. ::slight_smile: Start here

In my efforts I have an eventbus implementation which is driven by extensible enums defining categories that also is a system component which can be added to any component manager. The events are also component managers where any data can be attached and recycled after the event propagates.

The benefit of an main / app wide eventbus is that there is a single place where all event bindings are stored and can be removed in one call. Depending on the design only the receiving end of events is registered explicitly with the eventbus as well. Having this intermediary between the triggering and receiving locations is a marked improvement over the classical listener / observer pattern.

For ECS, you cannot easily prevent someone from removing a component because you cannot remove mutators w/o breaking your ECS.

As mentioned if you are a little clever you can prevent this scenario by adding into the mutation methods of the component architecture invocation of an internally supported component which can have pre & post actions around the mutation itself. The pre action can cancel the mutation action among other use cases. The post action could be whatever you want based on the added component; IE fire an event based on a component being added or whatever.

  1. Who is “someone” removing objects from your code? Hackers? Plugin devs?
  2. Have an additional component map that does not allow removal or overwriting for components you want to keep in your entity.
  1. Me. I make mistakes especially with my already reasonable sized engine and editor.
  2. Very good idea. I do not think i will implement it now, but I will try it if I run into any more problems.