7
\$\begingroup\$

I'm trying to find the best design pattern for my AI code using an ECS.

Right now the entities that act as CPU-controlled have components like:

  • WeaponComponent
  • ChargeComponent
  • MovementComponent
  • AIControlledComponent

...

I have an AIControlSystem that goes over the entities that have the AIControlledComponent and during each frame switches the mental state of the entity based on some rules like "can I see the player?", "can I move somewhere safe?", etc...

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. This way, those methods tend to grow as the kind of enemies and actions grow and the code starts to become a mess as there are way too many checks and paths to follow.

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.

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 with the virtual methods that interpret the queue of actions and inherit a new component from that one for each enemy type. It looks a bit better for code organization but I'm not sure that making this kind of exception is a good idea.

Is there any other way to keep the code under control and at the same time have the flexibility needed?

\$\endgroup\$
  • 6
    \$\begingroup\$ AI is exactly one of the things that lead me to dislike the strict ECS nonsense that Reddit hobbyists peddle these days. Using components is good. Using an ECS-like approach for some things is good. Using a strict ECS for everything is asking for unnecessary hardship. \$\endgroup\$ – Sean Middleditch Feb 9 '16 at 18:29
  • \$\begingroup\$ @SeanMiddleditch I agree with your sentiment. I've been evaluating turning my AIControlledComponent into a sort of blackboard and move everything else AI related into a whole different system that doesn't actually use the ECS directly. If you have any hints to share for designing something like that it would be really appreciated. \$\endgroup\$ – Valerio Santinelli Feb 9 '16 at 19:25
  • \$\begingroup\$ None of your major modules should even be allowed to include the ECS headers in the first place. Only use Components to glue modules together into game objects. All of your core engine code should be usable with any object model. Graphics renders scene nodes, physics moves bodies, core AI interprets decision graphs, etc. None of those rely on actual game objects. Specific AI nodes of course are game-state-dependent so the high-level AI System might use Components to feed data or trees to the low-level AI module, but the module does not depend on the components at all. \$\endgroup\$ – Sean Middleditch Feb 9 '16 at 19:33
  • \$\begingroup\$ @SeanMiddleditch 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. How would you decouple that? \$\endgroup\$ – Valerio Santinelli Feb 10 '16 at 17:38
  • \$\begingroup\$ I maybe wasn't clear. The AI module itself doesn't need to know, because it doesn't have to know the concrete decision logic in the evaluation nodes. Those are "plugins" (interface implementations) that the higher-level gameplay code can supply. \$\endgroup\$ – Sean Middleditch Feb 10 '16 at 21:18
3
\$\begingroup\$

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.

  1. 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.)

  2. 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.

\$\endgroup\$
1
\$\begingroup\$

Use a behavior tree instead for sure. Each behavior tree, you make it specific to the "actor". Each actor has the components that it has and since the BT is attached to it, the nodes of the BT will know the components and can query for them. Now your BT knows exactly what the actor has at it's disposal and can work with it.

So for example you have a bird BT that you attach to bird entities that have the fly component. You have specific flying leaf/action node that you use in this BT when you want to get this bird AI around. Since the action node is fly, then it's expecting the entity it's attached to, to have the fly component. It will query to get it, and will work with it.

Now your entities that have components attached and a BT attached can have nodes in that BT work on it's components.

\$\endgroup\$
  • \$\begingroup\$ This answer seems to assume that the reader already knows what behavior trees are and how they work. You could improve it by giving a quick summary or at least post a link to one. \$\endgroup\$ – Philipp Jan 9 '18 at 16:45
  • \$\begingroup\$ @Philipp Good point. gamasutra.com/blogs/ChrisSimpson/20140717/221339/… \$\endgroup\$ – user441521 Jan 9 '18 at 17:11
  • \$\begingroup\$ I'd argue, you shouldn't necessarily mutate components within the BT, but add/remove "tags" or "marker" components, that the Systems (outside the tree) will work on the entity containing said markers/tags. \$\endgroup\$ – Seivan Jul 19 '18 at 11:27

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.