In this part of the tutorial we will add a PickupPearGoal and let the agent pickup pears if it comes close to them.
Creating the classes
Let's start by boiler plating the files that we need. Use the generator to create the following files:
Goals: PickupPearGoal, EatGoal
Actions: PickupPearAction, EatAction
WorldKeys: PearCount, Hunger
TargetKeys: ClosestPear
We will implement the classes in a bit, first let's make sure that we know what pears are. Create a new class called PearBehaviour in the Behaviours folder and add the following code:
In our scene, create a new GameObject using GameObject > 3D Object > Sphere. Rename the object to Pear and add the PearBehaviour component to it. Pears are generally smaller than agents, so let's adjust the scale to 0.5 on all axes. You can remove the collider as we won't need it.
Let's create a new material for the pear. Right-click in the Assets folder and select Create > Material. Rename the material to PearMaterial and change the color to a nice yellow/green. Drag the material onto the Pear object.
In the GettingStarted folder let's create a new folder called Prefabs. Drag the Pear object into this folder to create a prefab.
Let's duplicate the pear a couple of times in the scene and place them in different locations.
For these actions we need data that represents the PearCount and Hunger values. The source of truth for these values must be our own MonoBehaviours. Let's create a script called DataBehaviour in the Behaviours folder and add the following code:
DataBehaviour.cs
usingSystem;usingUnityEngine;namespaceCrashKonijn.Docs.GettingStarted.Behaviours{publicclassDataBehaviour:MonoBehaviour {publicint pearCount =0;publicfloat hunger =0f;privatevoidUpdate() { // For simplicity, we will increase the hunger over time in this class.this.hunger+=Time.deltaTime*5f; } }}
Add the new DataBehaviour to the Agent object in the scene.
Create the sensors required for the PearCount and ClosestPear keys. This time we'll use something called a MultiSensor. This sensor can provide multiple keys at once. Create a new class called PearSensor in the Sensors folder and inherit from MultiSensorBase.
PearSensor.cs
usingSystem;usingSystem.Collections.Generic;usingCrashKonijn.Docs.GettingStarted.Behaviours;usingCrashKonijn.Goap.Runtime;usingUnityEngine;namespaceCrashKonijn.Docs.GettingStarted.Sensors{publicclassPearSensor:MultiSensorBase { // A cache of all the pears in the worldprivatePearBehaviour[] pears; // You must use the constructor to register all the sensors // This can also be called outside of the gameplay loop to validate the configurationpublicPearSensor() {this.AddLocalWorldSensor<PearCount>((agent, references) => { // Get a cached reference to the DataBehaviour on the agentvar data =references.GetCachedComponent<DataBehaviour>();returndata.pearCount; });this.AddLocalWorldSensor<Hunger>((agent, references) => { // Get a cached reference to the DataBehaviour on the agentvar data =references.GetCachedComponent<DataBehaviour>(); // We need to cast the float to an int, because the hunger is an int // We will lose the decimal values, but we don't need them for this examplereturn (int) data.hunger; });this.AddLocalTargetSensor<ClosestPear>((agent, references, target) => { // Use the cashed pears list to find the closest pearvar closestPear =this.Closest(this.pears,agent.Transform.position);if (closestPear ==null)returnnull; // If the target is a transform target, set the target to the closest pearif (target isTransformTarget transformTarget)returntransformTarget.SetTransform(closestPear.transform);returnnewTransformTarget(closestPear.transform); }); } // The Created method is called when the sensor is created // This can be used to gather references to objects in the scenepublicoverridevoidCreated() { } // This method is equal to the Update method of a local sensor. // It can be used to cache data, like gathering a list of all pears in the scene.publicoverridevoidUpdate() {this.pears=GameObject.FindObjectsOfType<PearBehaviour>(); } // Returns the closest item in a listprivateTClosest<T>(IEnumerable<T> list,Vector3 position)whereT:MonoBehaviour {T closest =null;var closestDistance =float.MaxValue; // Start with the largest possible distanceforeach (var item in list) {var distance =Vector3.Distance(item.gameObject.transform.position, position);if (!(distance < closestDistance))continue; closest = item; closestDistance = distance; }return closest; } }}
Editing the PickupPearAction
Let's implement the PickupPearAction so it actually performs the action of picking up a pear.
PickupPearAction.cs
usingCrashKonijn.Agent.Core;usingCrashKonijn.Agent.Runtime;usingCrashKonijn.Docs.GettingStarted.Behaviours;usingCrashKonijn.Goap.Runtime;usingUnityEngine;namespaceCrashKonijn.Docs.GettingStarted.Actions{ [GoapId("PickupPear-06ef21a4-059b-4314-800a-e7c2622637fb")]publicclassPickupPearAction:GoapActionBase<PickupPearAction.Data> { // This method is called every frame while the action is runningpublicoverrideIActionRunStatePerform(IMonoAgent agent,Data data,IActionContext context) { // Instead of using a timer, we can use the Wait ActionRunState. // The system will wait for the specified time before completing the action // Whilst waiting, the Perform method won't be called againreturnActionRunState.WaitThenComplete(0.5f); } // This method is called when the action is completedpublicoverridevoidComplete(IMonoAgent agent,Data data) {if (data.TargetisnotTransformTarget transformTarget)return;data.DataBehaviour.pearCount++;GameObject.Destroy(transformTarget.Transform.gameObject); } // The action class itself must be stateless! // All data should be stored in the data classpublicclassData:IActionData {publicITarget Target { get; set; } // When using the GetComponent attribute, the system will automatically inject the reference [GetComponent]publicDataBehaviour DataBehaviour { get; set; } } }}
Configuring the PickupPearGoal
In the Capabilities folder let's create the PearCapability class and add the following code:
Start the scene and watch the agent pickup pears when it comes close enough!
You graph should now look like this:
Adding the EatGoal and EatAction
Adjusting the EatAction
Let's adjust the EatAction to consume the pears that the agent has picked up.
EatAction.cs
usingCrashKonijn.Agent.Core;usingCrashKonijn.Agent.Runtime;usingCrashKonijn.Docs.GettingStarted.Behaviours;usingCrashKonijn.Goap.Runtime;namespaceCrashKonijn.Docs.GettingStarted.Actions{ [GoapId("Eat-b235695c-727b-41a5-aa66-4757ce65719d")]publicclassEatAction:GoapActionBase<EatAction.Data> { // This method is called every frame while the action is runningpublicoverrideIActionRunStatePerform(IMonoAgent agent,Data data,IActionContext context) { // Instead of using a timer, we can use the Wait ActionRunState. // The system will wait for the specified time before completing the action // Whilst waiting, the Perform method won't be called againreturnActionRunState.WaitThenComplete(5f); } // This method is called when the action is completedpublicoverridevoidComplete(IMonoAgent agent,Data data) {data.DataBehaviour.pearCount--;data.DataBehaviour.hunger=0f; } // The action class itself must be stateless! // All data should be stored in the data classpublicclassData:IActionData {publicITarget Target { get; set; } // When using the GetComponent attribute, the system will automatically inject the reference [GetComponent]publicDataBehaviour DataBehaviour { get; set; } } }}
Creating the EatCapability
This is your time to shine, create a new capability called EatCapability that uses the EatGoal and EatAction.
Let's adjust the BrainBehaviour to include the EatGoal when the hunger is higher than 50!
BrainBehaviour.cs
usingSystem;usingCrashKonijn.Agent.Core;usingCrashKonijn.Agent.Runtime;usingCrashKonijn.Goap.Runtime;usingUnityEngine;namespaceCrashKonijn.Docs.GettingStarted.Behaviours{publicclassBrainBehaviour:MonoBehaviour {privateAgentBehaviour agent;privateGoapActionProvider provider;privateGoapBehaviour goap;privateDataBehaviour data;privatevoidAwake() {this.goap=FindObjectOfType<GoapBehaviour>();this.agent=this.GetComponent<AgentBehaviour>();this.provider=this.GetComponent<GoapActionProvider>();this.data=this.GetComponent<DataBehaviour>(); // This only applies sto the code demoif (this.provider.AgentTypeBehaviour==null)this.provider.AgentType=this.goap.GetAgentType("DemoAgent"); }privatevoidStart() {this.provider.RequestGoal<IdleGoal,PickupPearGoal>(); }privatevoidOnEnable() {this.agent.Events.OnActionEnd+=this.OnActionEnd; }privatevoidOnDisable() {this.agent.Events.OnActionEnd-=this.OnActionEnd; }privatevoidOnActionEnd(IAction action) {if (this.data.hunger>50) {this.provider.RequestGoal<EatGoal>();return; }this.provider.RequestGoal<IdleGoal,PickupPearGoal>(); } }}
A mixed graph
Play the scene and watch the agent eat the pears when it's hungry!
Your graph should now look like this. As you can see the actions of the different capabilities are mixed into a single graph. The PickupPearAction can now also be performed when the EatGoal is active.