In this lecture, we're going to implement a GOAP system in Unity.
Now you can download the starter file to follow along
and it's called GOAP.zip.
There's 2 starter files with this lecture...there's the GOAP.zip
and there's also a goap-master.zip.
The goap-master is the complete version of this GOAP library that I've taken from the URL that you can see
on the screen.
So it's got other examples and things in it for you to look at later
and what I've done with just the GOAP one is I've stripped it back as much as I can and I've also modified
it to allow for a Nav Mesh Agent because we'll use a NavMesh to push our agents around on the screen
between their different destinations.
Now, if you bring the GOAP.zip package into Unity, then you'll find THIS when you open up the
village scene.
I've got an agent and he's going to be a Baker.
This is his Bakery...
this is the Windmill supplying the flour...
and then over here, we've got some Markets. Now, with each of these buildings I've included a cube
(which I've called 'Door')
and you can see that in the Hierarchy. The agent needs a location to go to, to perform the task
and you could have them go to the actual building to perform the task
but I thought it would be nice to have just a little marker where they could go to,
so that's why I've added them here and you'll see them in use later.
Now this agent is on a plane, which is a NavMesh.
If you open up your Navigation tab, you'll see that the NavMesh is already applied.
The agent (which is called Baker because we're going to get him to bake some bread) in the Hierarchy
has got a few things already set up on it.
So, first of all, it has the Nav Mesh Agent on there, which is what we all use to navigate around the
scene.
You also need to add to any agent (that's a GOAP agent) the 'Goap Agent' script and that Goap Agent script
is in the Assets in the GOAP folder.
We'll get to that in a moment, that's already written for you. Then the things that you have to write
that sit over the top of the GOAP system are the scripts that run the agent itself and we're calling
him a Worker and the agent also has an Inventory and I'll come back and look inside of those in a second.
Let's go over to the Assets and have a look in the AI folder.
You're going to find that there's a Finite State Machine (FSM) folder.
This is the little state machine that just drives the agent from one location to the other to perform
the tasks.
It doesn't use the State Machine System that's built into Unity, it's all done by code.
It's such a simple thing that it doesn't really need the Animation System to set up
all of the different states and transitions between it because it's essentially only 2 states
that you have to deal with.
Then there's a GOAP folder. Go into the GOAP folder and this is where you'll find all of the classes
that meet the different parts of the GOAP architecture. First there's GOAP agent, which takes care of the basic
agent actions and makes sure that it's going to run the Planner, that it's looking for Goals, that it's
keeping track of the World State as it changes.
We then have a GoapAction.
Now each action that you add will inherit from this particular class and that will allow you to create a
whole bunch of actions, even if they don't get used.
So remember: an action in the GOAP system can be added.
It's not connected to anything at all, it's just sitting there as itself and it has Preconditions
and it has Effects
and then what happens when there's a Goal that comes in
is that the Planner gathers up all of these Actions and tries to piece them all together, to see where the
Preconditions and the Effects join together to make a chain, which becomes your Plan. So you can just
add as many of these Actions onto your agent as possible
but if they don't have Preconditions and Effects that will join with other actions, then they will never
get used.
So that's one of the things you really need to look out for when you're designing your Actions
but we'll get around to that when we start making our own Actions and how important it is, so that they do
chain together.
The GoapPlanner itself gathers together all of the available Actions and checks them against the conditions,
the state of the world and any particular Goals and comes up with your chained plan of actions, then reports
that back to the agent that then uses the Finite State Machine to perform the actions along the chain.
Now the code that we're interested in and the code that we write to interact with that GOAP system is
currently on our Baker and that's the Worker script and the Inventory script. The Inventory script, if you
drop it down in the Inspector, you'll see that it has levels for Flour and Bread.
So these are the things that the agent has, it's like their 'backpack' or 'inventory'. If you have a look in
that code, it's really quite simple.
All it is, is some integers holding the values
and we're starting off our agent as having 5 bags of flour.
There's a really simple OnGUI there, just to print it on the screen, so we can see these values change
as we run the simulation.
Now the Worker code that's also on our agent is going to inherit from our GOAP interface here (IGoap), which is essentially
our GOAP agent.
It's got on it, obviously, our NavMeshAgent which we need to push it around.
There's also a previousDestination Vector3 that's being stored for the reason of navigation, which you'll
see in a moment
and then we've also got a link to our inventory system that is on this agent.
Those 2 things (the NavMeshAgent and the Inventory) are set up inside of the Start.
So we grab hold of those 2 components,
so we don't have to keep doing this throughout the code.
Now, moving down through this code, we get to some of the most important parts of this agent and
that is these 2 next methods: GetWorldState and CreateGoalState. The GetWorldState will inject into
the GOAP system, the state of the world.
Now for this, we're going to be using 'true' and 'false'
states as we looked at originally in the previous lecture and we're going to set them in here.
So this will keep getting run and updated for your agent so that the planning system can check if certain
states have been achieved or not.
Now in this case, initially, we're setting our World State 'hasFlour' to either True or False based on the
inv.flourLevel. Before we continue, I'd like to explain the structure of the Preconditions and
Effects, as they are essentially the glue that holds all of the Actions together in the chain.
They are all stored in a HashSet in the format 'string, object'.
In this case, we're representing them with a string that explains the State and then a True or a False.
For example, ("hasFood", true) ("hasBread", false). You'll see these set for actions later on and they are truly
the conditions that will bind each action together, so that for each actions Precondition, you need to
make sure that you have another action that has an effect that will essentially magnetize them and match
them together, so that they can form a chain.
If you don't have those anywhere in your action set, then that action will not get used and you might find
that the Planner cannot find a plan.
So, back to our code. Where we put these Preconditions and Effects is inside of these 2 methods that we
just talked about, between the initial declaration for the 'worldData' set and also for the 'goal' set.
So this is all the states of the world, as I said. It gets updated all of the time, so this function gets
called all of the time to keep checking that this 'hasFlour', for example, is either True or False
and that's why we can put these variables in here, that we can change.
This is the flour level in the inventory and it's not going to say that you've got any flour unless
the flourLevel is greater than 1.
So, in this case, it doesn't consider you've got enough flour to do anything with until you have 2 bags
of flour.
So that's setting the limit of how many bags of flour is considered 'having enough flour to bake
with'.
Now if you've got other conditions in World States that you want to do, that have a variable value in it,
then you can add them inside of here.
You cannot put the variable-type values like this inside of your Actions because it won't work.
The Actions have to match up exactly when it goes to plan
and if they don't, then you will get the whole system stalling.
So, we'll see that in a moment anyway.
So that's your worldData.
Now the Goals themselves go in-between
where you declare the set of goals and where you return them.
So inside here, I want you to add a new goal for our Baker and it will be like this.
So here's our little string here and here's our True or False value.
Now this is a global goal for our agent and it's just going to be called
"doJob", so "doJob"
is True and it's going to keep forcing that on the agent and it'll want to keep making sure that it
is addressing this particular goal.
So we'll just have the 1 goal in here.
So add that in and then further down let's have a look at the rest of the code.
We've got the code for moving the agent.
This is just typical NavMeshAgent-moving type code
and what it does is a couple of things.
It first of all checks for our previousDestination (remember that Vector3 at the top of this code), checks
if that is not the same as where we need to go to next because if a NavMeshAgent is at its destination when
it's given a new goal, then it won't plan a path and that becomes a bit of an issue for determining if
we've reached our next destination to do our next task.
So if the task is in the same position as the last task, then this will take care of that because the
agent doesn't need to physically move around the environment, at which point we set
nextAction.setInRange to true.
Now each action is going to not run UNLESS your agent is within range of where that needs to happen.
So that's what this line here is doing.
Then, if we're not required to stay at the same destination (we need to go somewhere else), then we will
use our NavMesh with SetDestination using the nextAction.target.transform.position to push us
to the next location and then we're going to run checks like we have in the past with NavMeshes to
check if we've reached our destination.
So if we have a path and we're within a distance of 2, then we've reached our destination.
This is basically your accuracy of how close you want to get
and then we do the same thing.
So we're in range
and then we update our previousDestination, so that this just records where we are and we return True
so that the system knows we're at our destination and we're ready to do the next action.
All right.
So moving down to the Update, you don't need anything inside the Update
but what I've done here is just add a little bit of code which will help your agent slow down and
speed up better into corners and you can actually apply this whole if-block to any Nav Mesh Agent code that
you've got,
if you want your agents to take tight corners.
So I developed this for something else that I was working on and just thought I'd throw it in here.
This works really nicely if you're trying to get your agents to move along streets and roads and you
want them to stay on there and not veer-off onto the sidewalk, which is what they'll do if they're going
quite fast.
This will slow them down into corners for you,
so take note of that
and then further down we've got some methods that we aren't actually implementing whatsoever
but we just need them in there
otherwise it's going to complain and later on when you're more comfortable with this system, you could
come back and put your own code in here.
So, first of all, there's one that gets called when the plan fails...one when the plan is found...when your
action has finished...
and when your plan has aborted. So if you want to explore these, you could put Debug statements in here
to see when they actually run
but we're not going to be using them.
Ok.
At this point, if you press Play, you'll see in the console (if you go there) the messages that come out
of the system.
So these messages are in Debug statements throughout the GOAP library that you're using and you can find
them if you have a look around but they're going to give you an idea of what's going on.
So, the ones we're getting at the moment is that it's found actions.
Now it hasn't actually found any actions because we haven't put any actions in there
but it's still found an empty action set and then trying to do a plan that's going to
satisfy that "doJob" goal and it can't find one, obviously, so it just does that (NO PLAN) and then it gives you this
'Failed Plan' message.
So you will know as you go along and program this system, when things aren't working. Ok, we'll leave this
lecture here and in the next lecture we'll start creating actions for our agent.