In this lecture, we're going to continue writing some code that will allow our agents to execute a plan
and then we're going to put a simple plan onto our agent and before you start,
if you want EXACTLY the same project file that I've just started with here, it's attached to this lecture
as a Starter Project. Now, to do this we're going to go into our GAgent script, so open that up.
Now, if you remember from earlier on, there was a LateUpdate and we didn't actually fill this out.
So now is the time to add the code.
Ok, so the first thing we're going to do in here is add in the Planner.
So I'm just going to come down a little bit
because I want to come back and put other things above this a little later on. First of all, we're going to say
'if (planner == null || actionQueue == null)'
that means that the agent currently has NO plans to work on.
So, what are we going to do in here?
Well we're going to create a new plan.
So we create a new Planner, first of all. So 'planner = new Gplanner();' like that and then we're going
to sort through all of the Goals that we have. Now,
do you remember when we created our agent and we allowed it to have multiple sub-goals?
Ok, so if we go back up the top here.
A Goal is not just a single Goal but there's multiple Goals and then we also have the name of the Goal
and we're storing with it an integer value, which is going to be how important this particular Goal is,
in order to satisfy. So we're going to sort through all of our Goals and put them in order, from most
important to least important, so that we can then loop through them one at a time until we find one that
we can do.
So I'm going to do a 'var sortedGoals' which is going to equal our sorted dictionary, which will be using
a Linq command.
So it's going to be 'from entry in goals orderby entry.value descending select entry;'
Ok
and that's going to take our 'goals' array and order it and stick it into 'sortedGoals'.
Ok, now once we've got that, we're actually going to loop through each one at a time.
So 'foreach(KeyValuePair' of SubGoal and int.
So this is SubGoal in sortedGoals. Then in here, we will say that
'actionQueue = planner.plan'
so this is where we're calling plan.
So we're taking THE most important goal that we know about and trying to create a plan for it.
So 'planner.plan' needs the set of 'actions' that we've got, the goal
(that's 'sg.key.sgoals - so that's all of those sub-goals that make up that goal) and then we need to pass
through the state (not the World States but this is the Beliefs of the agent) and we actually haven't
put those in yet.
So at this time we're just going to pass through
'null'.
Ok, so we're not using the belief system, it's just going to use those World Goals we've got.
So then we go 'if(actionQueue != null);' then we must have a plan that we
can follow
and in that case, we're going to set the 'currentGoal' that we're working on to equal the sub-goal:
'sg.Key;' value and then we're going to break out because we don't need to keep looking through those
goals for another plan because we've already got one.
So that IS basically our Planner.
Now remember LateUpdate, it's a loop that's going to loop and loop and loop.
Ok.
So when it loops the first time and we don't have a plan or an actionQueue,
then we will get a Planner.
Now if we do have an action, then just above this (I want to test for it first) so if we're in the middle
of performing an action, then I'm going to return out of this LateUpdate before we get down into the
planning stage.
So up here, we'll go 'if(currentAction != null && currentAction.running)'
so there's a bool that we can set, to say that we're actually working on our action.
Now if we are working on our action, what we're going to do is put 'if(currentAction.agent.hasPath
&& and currentAction.agent.remainingDistance
Now this, as you will recognize, is NavMesh code and so we need to have this in here in order to move the
agent towards the goal.
Now as soon as the agent has a goal and it has actually reached that goal and we're saying, in this case,
less than a distance of 1, then we're going to run the end of our action, which is to run basically
the completion of it.
Ok, now remember that we put in each action a 'duration', so when we've got to this point, we've reached our
goal location...we then need to wait for the duration of it.
So let's say that you've rocked up to the Registry Desk and you're registering. So you've arrived there
physically and then it's going to take 10 seconds to actually complete the task that the agent is doing
there.
This is the code that will do that.
So we're going to say 'if(!invoked)' (and I'll explain this in a moment)
and if not invoked, then we're going to invoke an external method called 'CompleteAction' and that's going
to be sent through the currentAction.duration (or actually, it's going to be invoked in that, 10 seconds
later on and then 'invoked = true;'.
Now when we start, 'invoked' is going to be false and I'm just going to put it up here, ok?
So 'bool invoked = false', so we know that the first time we come into here, this will be false. It'll
allow us to go in here, invoke this CompleteAction method in however many seconds and then it sets invoked
to true, so we're never going to come back into here again at this point...we can't, it won't let us but
the CompleteAction method that we're going to write just up here is going to set invoked back to false
for us.
So we want 'void CompleteAction' and then inside of this method: 'currentAction.running = false;'.
So this is where we finally say we're finished running our current action and it will force our planning
system to go and plan again
and so we want 'currentAction.PostPerform()' which is the method that we run right at the end and
then we'll set invoked back to false. Ok.
So that's what happens when we've finished a current action and then after how many duration it is, we're
going to have 'running set to false', which means that this will then fail.
[Excuse me for one moment,
while I butt into my own video, just to say that I've missed a return-statement that you'll need to
put inside of that if-statement.]
Now at that point you've either run out of actions or you still have more actions in your queue.
So if that's the case, we now need to consider that.
So underneath our if-statement for our Planner, we're going to consider 'if(actionQueue != null)'.
so we actually have an Action Queue '&& actionQueue.Count == 0)' which means that we have
an empty Action Queue and we've actually run out of things to do,
then we can go 'if(currentGoal.remove'
so if the Goal that we've just been working on is one of those removable ones, then we can say
'goals.Remove(currentGoal);' from there and then we're going to set 'planner = null'
which is going to trigger getting another Planner the next time this loop goes through.
Now finally, there's one more if-statement condition we need to consider in here that might occur and
that is: 'if(actionQueue != null
&& actionQueue.Count 0)'
it means we've still got Actions left sitting in our queue that came back from the Planner. In that case,
we can start running them.
So we're now going to go: 'currentAction = actionQueue.Dequeue()' and this will take
the action that's sitting at the top of our queue OFF the queue and put it into currentAction. Now, we then perform
the pre-performance method that we've got for our action, so it's going to be 'if(currentAction.PrePerform())'
and in there we can do any checks that we need to do, like for physical attributes that need
to be set, if the number of, say, cubicles is available and all of those sorts of things and then if that
is true, we want to go 'if(currentAction.target == null && currentAction.targetTag != "")'
then what we're going to do is go 'currentAction.target =
gameObject.FindWithTag(currentAction.targetTag);' so we can set the target for the Nav Mesh Agent
to move to, by setting it in the Inspector or we can find it using a 'tag'. So if you've made a prefab of
your character that's going to be much easier for you just to set the tag, so then it does this search when
it gets into the environment to look for it. Now as you can see, I'm getting errors with our targetTag
here. If I just quickly save that and switch back into my GAction (and you might want to have a look here)
you can see that we set targetTag to be a 'GameObject' when, in fact, it's meant to be a 'string', so that
was just a little mistake I made before and will still be in your code.
So just save that and then we can switch back to our agent.
The other thing you can see here is that the GameObject.FindWithTag needs to be a capital 'G' in
there.
Now let's finish this if-statement.
We've got our target object, then we're going to go: 'if(currentAction.target != null' so
it still could be that we end up having nowhere to go, as far as moving our agent around and if that's
the case, we're going to check that it's not equal to null.
So if it's not equal to null, the 'target' (not the tag, I should say): 'currentAction.running = true;'
and this is where the action actually starts to take place.
Let's get rid of that 'T'
and currentAction.agent.SetDestination' and that's our Nav Mesh Agent setting the destination
to the '(currentAction.target.transform.position);'
Ok, so that is the starting point for setting the destination for our agent and then up higher is where
we're actually testing if we've made it to the destination.
Now remember: the Nav Mesh Agent code is going to go and do its thing automatically in the background
as far as moving the agent around.
Ok, so we're just going to end that if-statement. Now
else if we can't do our PrePerform method, we're going to say 'actionQueue = null;'.
and that will then force us to get a new plan.
Go back to the goals, get a new plan.
Now if any one of your actions that you're working through (or trying to work through) fails, then it's
going to replan the whole thing which may, in fact, replan the exact same sequence of actions but send
you back to the beginning, so that it makes sure that you do that
and it also makes sure that your agent doesn't get stuck somewhere through the middle of a plan.
Ok.
So now that we've got that, let's just save it. That is our main code for our LateUpdate.
Let's go back into Unity.
We're now going to create our basic class for our Patient.
So right-click 'Create C# Script'.
We're going to call this 'Patient' and then open this up (it's going to be very short code). So the Patient
is going to inherit from GAgent and it's not going to have an Update,
it's going to have a Start. Now in the Start,
we want to call 'base.Start();' that's going on in the GAgent script and then we're going to create
a sub-goal. So 'SubGoal s1 = new SubGoal("isWaiting,' and we'll give
it a priority of 1 and we will say 'true', which will remove it from being a Goal and then we have to
add it to our 'goals'.
So 'goals.Add(s1,' and it's priority, in this case, is 3.
Now this particular Start and our GAgent...let's just save this, let's go back to GAgent...I just want to point out
something that you need to fix in here (that I just did before) is to add a 'public' to the Start, otherwise
you're going to get an error when you try and call that 'base.Start' but basically base.Start is
going to set up all your Actions that you've attached to your Patient and then you can go ahead and
run the rest of this Patient code.
So we're going to save that, go back into Unity and grab hold of that Patient script and drag-and-drop it
onto your Patient.
Yep
and there it is.
Now we actually need an Action to follow.
So let's make a quick Action and in here, let's go 'Create C# Script' and let's call this 'GoToHospital'.
Open that up to edit it and this time it's not going to be a MonoBehaviour,
this is actually going to be a 'GAction' and inside of here we're going to get rid of our Start and Update
and all we need to do is put a 'public override bool PrePerform' which we will just put 'return true;'
in there and then a copy of that down here, for our PostPerform. So that is a basic stock standard
action.
So just save that.
Now switch back to Unity.
We're going to attach this GoToHospital to our Patient, which will give it access to be able to perform
that particular action.
Now this action, remember we've got pre-conditions and we've got post-conditions that we can put in here.
Let's say, just for now, there's NO Precondition.
The AfterEffect is going to have to match whatever our Goal is.
Now the Goal for our Patient (if we go back into the script here) is looking for 'isWaiting'.
Ok.
So back in Unity, on our Patient, we're going to add 1 After Effect and it's going to be 'isWaiting'
and this is where it needs to be spelled correctly.
Now the value in this case doesn't matter because we're not really using it for anything.
The other thing we need to know is where the Target for this action is going to occur. Now because the
agent is in the Scene in the Hierarchy, let's say we're going to go to the hospital in the
'Waiting Area'.
So get the 'Waiting Area' and drag-and-drop that into your Target.
It's going to be ok.
It's not a prefab, so we don't actually have to look for it using the 'tag' of the Waiting Area.
So now if we press Play, our agent is going to formulate a plan to make sure that 'isWaiting'
Goal occurs and as you can see, it has done that because all it needed to do was run the GoToHospital
script in order to satisfy its Goal.
So there we go. There's our very, very basic, very, very simple plan system working in our G.O.A.P. system
that we've just put together.
So now onwards and upwards to more complex actions!