The problem I see with this approach is that the AI module cannot be easily decoupled from the components attached to the entities. To evaluate a tree or run a state machine, it needs to know information from the entity being used so it actually needs to pull information like the inventory, the weapons available, the position, etc from the actual components.
Observe any animal: its nervous system is tailored to its tasks. Birds have sharp eyesight to seek food and threats, and the ability to rectify their posture and attain lift when falling, instinctively. And a human mind is wired to be very aware of the capabilities of its own body, its level of health, hunger, cold, not to mention presence or absence of limbs... you get the drift. If we use RL as a basis for object modelling, there is no fault in having the mind / AI be aware of such aspects as location, posture, health, held items, capabilities etc. What you could do is abstract missile weapons to be treated as a generic type. This leads to nice abstract AI logic like if holding missile weapon in hand, attempt to fire
. A brain in a jar, OTOH, is exactly that - provided it knows it is disembodied, it also knows that attempting to climb out of the jar is futile. That is: have AI check for null
components and proceed accordingly.
I thought about diverging from the pure ECS pattern of keeping components as data containers only and actually implement those methods in the components themselves so that I can have a base AIControlledComponent
I thought I could refactor the code splitting it up based on the kind of enemy (aka an enemy that has a Weapon won't be able to Charge and vice versa) but then I'd have to have a base AIControlSystem and inherit from that and specialize it based on the components attached to the entities. This would result in pretty much one new system per enemy kind and it doesn't really look like a good solution.
Keeping them as pure data items or not isn't the crucial issue; but your hierarchy of control is, and in that regard, the "pure" approach is often pushed as "sound practice". In truth it makes little difference whether the logic is on this
or on something else (like a higher level controller). Just don't let the non-pure method turn your code to spaghetti. (In my experience, the pure approach has more monolithic code and makes it harder to make mistakes in the local object that don't agree with global control mechanisms - I always find clear, top-down approaches best.)
Some will disagree, but generally speaking, the entire idea behind ECS is object composition vs. inheritance. I'm not saying you couldn't use inheritance on some of your component types, but I would avoid it inasmuch as possible.
Based on the mental state, this systems adds a set of actions to a queue. Then the queue is being consumed and evaluated. An action like "Attack" is actually calling a method of the system that checks whether the entity has a WeaponComponent attached and fires, or if there's a ChargeComponent it charges at the player.
I think this is central to your crisis. Your AI mind component should, knowing the full state of its own body and relevant parts of the external environment, never have queued "Attack" unless circumstances were ideal in the first place. For example, imagine you suddenly realised you didn't have a rifle in your hands after all, but a pen-knife. Would you still charge and attack, especially if the enemy were all armed with guns? I doubt it. It is tactically very different and in your present code, that distinction is not being made by your AI component before queueing the action in question.
whether the entity has a WeaponComponent attached and fires, or if there's a ChargeComponent it charges at the player
This is also totally wrong to my mind. Why is Charge
a Component
? Surely it's just a verb, a type of action that any AI can choose to take, even if it has a missile weapon in hand? Surely the only thing that would prevent a charge is speed / encumberment, and those should be factors handled in the AI mind's decision as to what actions to take, based on entity's own physical status? But... OK.
The last 2 paragraphs give you the gist of your problems: you're leaving what should be crucial, first class AI(!) decisions made before queueing occurs, to become last minute, second class decisions made at the moment when your entity should either be pulling the trigger or running for its life. Doesn't sound very decisive to me, and I can imagine what happens to real soldiers who fight this way. Further to that, I'm not even sure if the AI is involved at all in your "second-guessing" phase, or whether those are just other components making those final decisions as to whether to act or not... if the latter, you need to change that to single-phase AI decision-handling, and queue actions based on certainties.
Suggest you consolidate your AI processing to happen entirely before that queueing begins. The problems here are all in your AI strategy, and not in your ECS usage, insofar as I can see.