In this lecture, we will look at the concept of 'Fleeing'.
It's a common technique used in A.I. for getting an NPC to move away from a location.
The technique can be used on any type of NPC. In this case, we will apply it to our crowd
we've been working on. Mathematically, fleeing works like this...an object or location is introduced into
an environment.
The NPC determines where the object is in relation to their own position. They then calculate a new goal
location that is away from the object.
In this case, we will use the exact opposite direction.
This can be calculated using the vector towards the object and taking that away from the NPCs position.
With the new goal location,
the agent can turn around and start heading for it, thus being taken away from the object in question
Other factors we can consider are the 'radius of influence of the object' and the 'cooldown period'. The radius
determines how close an NPC must be to an object to be affected by it and the cooldown is how long the
NPC flees for, before returning to normal behavior. To implement fleeing in your current city crowd and
to get to the point that I'm at now, you'll need to add in a cylinder and make a prefab out of it.
So what I've done is go to the Hierarchy and 'right-click > 3D Object > Cylinder' and I didn't change
the size of it but what I did do was made a green material and attached it to that cylinder.
I then took that cylinder from the Hierarchy and drag-and-dropped that into the Assets and that will create
a prefab of that cylinder for you. I'll just delete my second version of it.
Now this cylinder is doing nothing but marking the location where we're going to click.
So we need some code to position the cylinder onto the NavMesh where we click with the mouse.
So let's create some new C# code and let's call that 'DropCylinder', then open that up and we'll
add in this code.
So first of all we've got a public exposed game object that we're calling 'obstacle'.
This is where we'll pass through our prefab of the cylinder.
We're then going to keep an array of all of the agents that are in the scene
and so therefore in the Start, we will collect all those agents by finding the game objects that have
the tag 'agent'.
This means you'll have to create the tag 'agent' and add it to your characters.
So I've got my characters. I've put them under 'Crowd' and duplicated them. I created a fair few of them. Don't
have too many because the more you have, the slower this is going to run
but you want just enough.
Now under this 'Crowd', if you just click on all of them then you can add a tag of 'agent'.
So I created a new tag called 'agent', with a lower case 'a' and just set it to all of the characters that
are in the crowd.
So back to the code and those agents will be sitting in this 'agents' array
and that means that when we drop the object, we can send a message out to all of them saying that that
object is there. It's like
the Agent Manager that we've had in previous lectures. Now in the Update is where the mouse button will
be tested for being down and then we'll do a RaycastHit into the world from the screen position
using the mouse position on the main camera.
That means make sure that your camera is tagged as
'MainCamera'.
So just remember where that is...go over to
camera and the tag for it should be 'MainCamera' here so that it picks that up.
Now, we're doing our physics Raycast into the world, finding out where it hits...where it hits, we're
going to instantiate a copy of our cylinder.
So that's the obstacle, we're going to put it at the hit location and then we'll use the prefab's default
rotation for it because it's really not that important.
Then we've got a 'foreach' statement which is just going to loop through all of the agents in the scene
and call a method on them (which we haven't written yet) but it will be called 'DetectNewObstacle' and we'll
pass it the hitInfo.point, which is the location of where we're placing the cylinder.
So this is essentially telling our agent that there's an obstacle that's being placed and now it's your
job to flee from it. So save that...it will give you a little error because we haven't written this
DetectNewObstacle yet
but that's what we're going to do next.
Open up the AIControl script and this is the script you should have attached to every single agent
in the scene,
so it's always a good idea to create one and then duplicate it so that you don't have to go through adding
this on. My AIControl code is a little bit different from what I left off with in the last lecture
so I'll quickly go through it and show you the changes.
First of all, I've created this 'speedMult' up the top here. It was embedded in the code further down, as a local
variable
but now we're going to use it in a couple of different places.
So the goal now is to have 2 different types of animations and 2 different types of speed. We have
the walking
and we'll also have the running, so that when the obstacle is dropped and the character is fleeing then
we want them to be running away.
So put that at the top. Next,
I've created a function called 'ResetAgent' because I was going to be resetting the agent's speed
here-and-there, depending on whether you're fleeing or whether you're just wandering around and therefore it's
good to have all of this in the one spot so that it's easy to change and easy to update.
Now here you can see in ResetAgent, when it happens it's going to set the speed multiplier to a random value.
Before, it was doing this and the Start function so that the agent always had the same speed.
Now it's going to change the speed of the agent
every time there is a reset.
So that's a nice way to make your characters a little bit more randomized in their movements
so that they're not always just at the same single speed for the same character.
Now next we have the agent's speed being set here.
Remember that it's 2 as a default on the characters, multiplied by the speedMult value.
So that's just making sure that it's set back to 2
and I'm also setting the angularSpeed.
If you have a look at the default angularSpeed in the NavMesh Agent on one of your characters, you'll see
that it is 120.
The angular speed is how quickly the character will turn around.
Now what I want is to ramp that up when the character has to flee.
So you want them to see something you have to flee from and turn around really quickly, so that
means you've got to turn up the angular speed and that also means when they go back to walking that
you have to turn it back down again.
So I'm putting it back down here to 120. In the ResetAgent,
when it does reset, it will set the speedMult value that's in the animation system to speedMult and
it will set
the walking animation back again and then I'm doing a ResetPath here which clears out the current path
that the agent has calculated and basically sets it back to 0 again so it can be ready to generate
another path in the NavMesh system.
Now, the Start function...what it does is pretty much what we had before.
So it grabs all of the goal locations in the world,
the agents NavMesh, sets the destination to a random value
(this code is exactly the same code as before, I'll just make it bigger so you can see it), random
location out of the goalLocations array, grabs hold of the animator that's on the character and
then also sets the walking offset (so that they're not all walking in time with each other)
to a value between here and then we run the ResetAgent (which will go ahead and set all these values
that are up here for us).
Now in the Update, it's the same thing as we had before except we're doing a ResetAgent before we set the
next destination.
What that will mean is that if we're running to a location because we're fleeing from it, then once we've
reached it, we're going to reset back to walking and go and grab another goal location and just keep
wandering around. The new things that we'll add for this code to facilitate the fleeing...
first of all, we need a float, which will be the 'detectionRadius' and that's the distance from 'where the
object is dropped out' to 'how far that any agents nearby have to be in order to hear it'.
Now if you have that quite large, then all of the agents in the area will respond to it. If it's smaller,
then only the ones that are in the 'within 5 of it' will respond, which is nice because you can have
a small fleeing distraction or you can have something that's quite large as a fleeing distraction.
Then we also need a float for the 'fleeRadius'
and this is how far you want the characters to run away from the object before they forget about it
and start going back and looking for a new goal location.
So let's set that to 10, in this case.
Right, so now with that what we're going to do is create that method that I talked about before
(DetectNewObstacle) that is being called from the dropping of the objects in the other clicking script.
So the new method will look like this.
So it's going to be public because it's being called from elsewhere and we're passing through the position
of the obstacle that's being dropped into the scene.
We're then going to do a distance test of the 'position of the object' and the 'agent's position'.
Now, if this is inside the detection radius, then the agent will react to it.
If the agent is going to flee, it needs to determine in which direction it's going to flee which is
away from the position of the obstacle and to determine that, it's the position of the agent minus the
position of the obstacle. Notice I'm then normalizing it so that I have a length vector of 1 and I'm
going to use that as a 1 to multiply it by the fleeRadius, which you can see here.
So it's the fleeDirection for a length of fleeRadius, added to the current position of the agent
and that's going to be where the new goal is.
Now with that new goal information, we can set the destination of the agent
but beforehand, we're going to see if we can make it to that location.
So you could just have agent.setDestination newgoal
but in this case, it's possible that you can calculate a location that is off of the agent's NavMesh
altogether or it's not quite accessible and that means that you still want the agent to try and get
to it, if possible.
So this is a different way of working out the path.
What we do is we create a NavMesh path object and this path object can be calculated with
CalculatePath.
So we create this value for path and we pass it through to calculate path, given the new goal position.
Now this path will hold the path that the agent's going to follow to get to the goal
but if it can only make it part of the way along or it can't make it at all, you can test the
path.status.
In this case, if the path.status is not equal to an invalid path, then it will set the destination
to the end of the path that it's calculated. So 'path' itself ends up with an array called 'corners' and that
is an array of all the waypoints, essentially, that the character's going to follow as it gets from
where it is to its goal location.
Now if it can't get all the way to the goal location, you'll still have this corners array but it
will only take you part of the way, which means if you get the last value in the corners array
(which is this one here) then you'll have made it to the end of the path that was calculated, even if you haven't
made it to the exact goal. So you'll get the agent to move at least some way away from where you've placed
the obstacle.
At that point, we set the animation to running and then we speed up the agent with agent.speed and we turn
up the angularSpeed quite high because we want the agent to turn around very quickly and then head
off in the complete opposite direction.
All right, so save that and then we'll just go back over into Unity. What you'll need to do to test this
out
(you can see I've removed all the buildings, duplicated my agents)
I've got four goals and I've just placed them in a square around the area, so the agents when I press
Play are just going to wander around at random between those goals.
Now to set up the clicking for the cylinder, we've got our DropCylinder script here. I'm going to just
add it to the camera (drag-and-drop it onto the camera).
It just needs to be in the scene so that it runs and then that DropCylinder script requires our prefab
Cylinder that we created before.
So grab that and drop it into there. Now when we press Play, if we drop a cylinder
you'll see that the agents flee from it and then they come back because they only flee out as far as
they need to go to find their goal location and then they will just start wandering back and looking for
other goals. So they're still randomly moving around those 4 different goals which are in these 4
corners where they tend to be grouping.
Right, so if you want to change this up so that they all react, if we go back into our AIControl, go
to the top here and if we change this detectionRadius to 20, that should probably cover
everybody in that scene. Save it.
Go back and press Play. When we drop this in here, everybody will flee.
Now they're still not fleeing very far because you've still only got that radius of 10 (or whatever we had
in there) and you can change that if you want to push them right out to the edges of the NavMesh.
Now chances are, when you're doing this, there will be the odd agent who will seemingly be ignoring the
obstacle and that will be because in your AIControl script just down here, where you're calculating the path,
it can be possible if you've got a very, very large fleeRadius to be trying to calculate a new goal that
is so far away from the current NavMesh that it's just going to return an invalid path
and there's really nothing you can do about it.
So you have to live with it.
The other alternative is you'd have to manually program as an 'else' part to this here,
the agent turning around and going off in the opposite direction, which means you then lose control
of the agent and the agent would no longer be navigating via the NavMesh
and that could cause you issues.
So it's up to you whether you want to do that or not or
you're just happy to put up with the fact that that doesn't work.
Otherwise, you really do need to make sure that your fleeRadius is going to be inside the NavMesh or at
least not ridiculously far outside the NavMesh that it won't calculate a valid path for you.
So that is how to get your NPCs to flee from a space.
This is using the NavMesh method but the calculation of the vector to flee along and the new goal location
is exactly the same if you want to implement this in your own manual waypoint system.
You would just create a new waypoint on the fly and give it that new goal position, with that vector math
that's in this particular program.