Hello people!
While I do know how to script an A.I shooting and sort of, I need a help on how to script the following A.I behaviors:
Keep distance from the player and shoot.
Run/Walk and shoot in the same time.
Any ideas? Thanks!
Hello people!
While I do know how to script an A.I shooting and sort of, I need a help on how to script the following A.I behaviors:
Keep distance from the player and shoot.
Run/Walk and shoot in the same time.
Any ideas? Thanks!
Have you ever heard of a finite state machine? This is a pretty good place to start with simple AI.
Your request is too broad to cover all of because I’m not sure what you know and what you don’t know, but generally speaking, just work on one behavior at a time. In your Update loop, tell your enemy to move away from the player. Once you have that working, add another function to make it periodically fire.
Do you have a more specific question about these behaviors? Have you gotten any of it to work at all?
Hello @thekillergreece , and welcome on these forums.
Behaviour Tree (BT) is more appropriate to implement your AI. Using this tool, you break down your AI into small tasks which are then assemble into an hierarchical structure (a tree) which defines how and when these tasks are executed. The advantages of BT over FSM are mainly:
But let’s get concrete and make our hands dirty by implementing your AI using a behaviour tree.
I’m the author of Panda BT, it’s a script-based behaviour tree engine:
http://www.pandabehaviour.com/
This package contains the PandaBehaviour component, which you attach to a GameObject you want to drive with a behaviour tree.
This is a screenshot of this component, running a BT script:
Naturally, I’m going to demonstrate how to implement your AI using this tool.
If I understand correctly, the AI is trying, at the same time, to keep the distance from the player and try to shoot the player. So the root of your tree is going to be:
tree "root"
parallel
tree "KeepTheDistance"
tree "Attack"
This is a BT script, which is the mean to define a behaviour tree in Panda BT. A BT script has a root node, which is the starting point of a tree (similar the “main” function in common programming language). The child of the root is a parallel node, which runs its children in parallel (at the same time). Then the parallel node has two sub-trees: “KeepTheDistance” and “Attack”. So, we have our top behaviour defined. Please note how similar this script is to the description of the behaviour in plain English.
Now, we have to define these two sub-trees. What to we mean exactly by keeping the distance from the player?
We mean that if we are too close, we move away. Otherwise, If we are too far, we get closer. Here we can define two other sub-trees: “GetCloser” and “MoveAway”. So the definition of “KeepTheDistance” is:
tree "KeepTheDistance"
fallback
tree "GetCloser"
tree "MoveAway"
Succeed
Here we have a new node: fallback, which is a mean to try out some actions. if an action is not successful, try the next one, …and so on. Here we want, first, to try get closer, if that fails (for whatever reason, in fact if we are too close to the player), then try the next action, which is to move away. If that one also fails, it means we are at the right distance and we have nothing more to do. Therefore, we just Succeed (the Succeed task is there, otherwise the fallback would fail if it does not execute any successful node).
Alright. So, we have our “KeepTheDistance” sub-tree defined. Let’s define the tree even further. What do we mean by “GetCloser”?
We need a way to test whether we are too close or too far from the player. We know how to do that, we just test if the distance to the player is within some range. In Panda BT, we can define custom tasks by implementing a function on a MonoBehaviour. So the tasks would be:
using UnityEngine;
using Panda;
public class EnemyAI : MonoBehaviour
{
public Transform player;
// Distance range
public float minDistance = 10.0f;
public float maxDistance = 15.0f;
[Task]
bool IsTooClose()
{
return Vector3.Distance(player.position, this.transform.position) < minDistance;
}
[Task]
bool IsTooFar()
{
return Vector3.Distance(player.position, this.transform.position) > maxDistance;
}
}
Now, we need as well to define the DoMoveAway and DoMoveCloser tasks. As you are guessing, this task can take some time to complete, because we are not teleporting to some position instantaneously, rather, we need to move the unit progressively over several frames, therefore the C# implementation will be called on each frame. Then, at some point in time, we just indicate that the task is completed by calling Task.current.Succeed() (if we want to indicate that a task has failed, we would use Task.current.Fail() instead):
[Task]
void DoMoveAway()
{
Vector3 direction = (player.position - this.transform.position).normalized;
Vector3 velocity = -direction * moveSpeed;
this.transform.Translate(velocity * Time.deltaTime);
if ( !IsTooClose() )
Task.current.Succeed();
}
The DoMoveCloser task is similar, excepted that we move in the opposite direction, and we succeed when not too far. You know how to do that, so no need to show its implementation here.
Now that we have theses tasks defined we can define our “GetCloser” and “MoveAway” trees:
tree "GetCloser"
while IsTooFar
DoMoveCloser
tree "MoveAway"
while IsTooClose
DoMoveAway
Here is a new node again. The “while” node execute a node while a condition is true: While we are too close, we move away. (please note that “while” is different from the c# keyword, here we have no repetion, in fact, there is a “repeat” node for doing that).
And here we are, we have hit the bottom: we have nothing do define further in this part of the tree.
I think by now you’ve got the process by which a behaviour is defined and how tasks are implemented in C#. So, here is the complete behaviour tree with the “Attack” sub-tree defined:
tree "root"
parallel
tree "KeepTheDistance"
tree "Attack"
tree "KeepTheDistance"
fallback
tree "GetCloser"
tree "MoveAway"
Succeed
tree "GetCloser"
while IsTooFar
DoMoveCloser
tree "MoveAway"
while IsTooClose
DoMoveAway
tree "Attack"
fallback
sequence // Shoot at the player, if we have a line of sight.
HasLineOfSight
AimAtPlayer
Shoot
Succeed // Otherwise do nothing.
Notice that we have a new node: sequence. This node is the most straight forward node: it executes its children one after the other as long as they succeed. The sequence stops and fails as soon as one child fails. Here, if HasLineOfSight fails, (meaning there is an obstacle between us and the player), the sequence stops there and we won’t shoot. So the sequence node can be use to implement if-then logic as well.
This is it, for implementing your AI.
Also, with Panda BT you can visualize the execution of the tree at runtime, which is valuable for inspecting what the AI is currently doing. This online demo shows what an executing BT looks like:
http://www.pandabehaviour.com/?page_id=217
If you have any question about this tool, please ask. You’re also welcome on this thread:
http://forum.unity3d.com/threads/ai-scripting-panda-bt-behaviour-tree-scripting.388429
I would like to add something about using FSM to implement this behaviour.
FSM does not implement parallelism, that is an an FSM runs one state at a time. So, if you want to implement Run and Shoot at the same time, you would need the following states:
Now, we need to specify Run further, because we need to keep the distance. Therefore Run split into RunAway and RunCloser, which increases the number of states:
See how the number of states is growing? And we have not yet talked about the transitions…
That’s a very small number of states, and is generally easy to handle. An FSM model stops states from overlapping which makes logical debugging simpler. Also, when you “split” states, you can actually just use a substate; if the behavior is still just running, simply changing the direction based on another input keeps it from becoming too much more complex. By that and OP’s description, I’d argue you could get away with only two or three states.
Mecanim is a state machine, and it’s common to have dozens of states used for both animation and behavior; this isn’t really much different. It’s not omnipotent, but that’s why I said it’s a good place to start for simple AI.
It is indeed a very small number of states: 6 states. Yet it is already problematic. The difficulty with FSM is not really the number of states, but handling the transitions. With the current behaviour, the AI should transitions from any state to any other states. Which means that each state has 5 outgoing transitions. Therefore the total number of transitions is 30. If you add an extra state, that number goes to 42, two extra states would be 56 transitions. So the number of transitions grows quadratically (~n^2). Of course this is an extreme example, since a state is not necessarily connected to every other states, but it is a good approximation of the general complexity of an FSM.
Furthermore, if at some point in your project you want to change the behaviour of your AI, you would probably modify a large number of transitions. Due to the complexity, it is very likely that this change would introduce bugs.
That won’t work neither. A sub state is just another FSM, therefore it still execute one state at a time; we still have no parallelism.
I know what you mean here. Instead of having two states, RunAway and RunCloser, you could have only one state, Run, and manage the direction inside the code that handle the state. That will work and will simplify the FSM a lot, but the FSM does not capture the logic of the AI anymore, since now a part of the logic is embedded into the implementation of the state. So, a part of the responsibility of implementing the AI has been transferred from the FSM to somewhere else.
FSM is a nice tool; it is easy to understand and to master. And it helps to handle a lot of situations. Any serious game developer should have this tool around. However, despite of being easy to understand and to master, it can become overwhelming for implementing complex AI practically, because of the increasing number of explicit transitions. This is where BT becomes a good alternative.
We’re more or less in agreement @ericbegue , but I want to point out a few things:
Transitions absolutely can become a bit of a web, especially when it’s visualized in some way (like in the Animator), but a transition from any given state to every other existing state isn’t needed very often. Even if there are a lot of transitions, it doesn’t cause too many workflow problems unless it’s taken to the extreme. Most of the time, some default or idle state will take you to most of the other states or state branches, which will then eventually return to the default state. Some special-case transitions are added where it’s necessary to go to a new state immediately.
When I pointed out that sub-state machines can be used instead of adding more states, I wasn’t trying to say that this introduces a parallel state mechanism. As you pointed out, FSM’s can’t do that, and indeed the characteristic of an FSM is that it removes the concept of layered behaviors from behavior design altogether. I was just saying that it’s yet another way that the state transition count is reduced; transitions between the substates are minimal, and all substates are dependent on the superior state’s transitions.
I’m not 100% sure what you mean about a run left/run right state not capturing AI logic, but I think the question is whether that’s significant. As an abstraction, couldn’t you say that the output of the “run” state is to set “running” to true? The “running” signal activates some other module with its own combinational logic to determine “left” or “right”. It’s true that the behavior isn’t fully realized within the FSM, but if it’s simpler to do it that way, then why not? This is common if not ubiquitous. We don’t technically have the standard limitations of a traditional state machine (one state, one output set), so we can just insert a conditional into our output block for the “run” state and call it good.
I’m not endorsing FSM’s for complex AI’s, of course. I wouldn’t do it for an AI opponent in a multiplayer game, as one example.
Sorry about that, I’ve somehow misinterpreted you. Reading you again, you were not saying that indeed.
What I mean by the FSM does not capture the AI logic in this situation anymore, is that the logic is no more described by the FSM. If you read the description of the behaviour it is “Keep the distance from the player”, which is different from just “Run”. So by embedding the logic somewhere else, the specification of the behaviour is lost in the process. This can be a problem in term of software maintenance. Since the FSM does not match the specification anymore, which can cause misunderstanding or confusion about what the FSM is actually doing.
Please keep in mind that what I’m doing all along here is to compare FSM to BT. So I am not criticizing FSM only. I am poundering FSM and BT in term of usability to define AI behaviours. In this comparaison, while the AI complexity increases, FSM becomes far less manageable than BT.