3. Pears
Pears
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 theBehaviours
folder and add the following code:
using UnityEngine;
namespace CrashKonijn.Docs.GettingStarted.Behaviours
{
public class PearBehaviour : MonoBehaviour
{
}
}
In our scene, create a new GameObject using
GameObject > 3D Object > Sphere
. Rename the object toPear
and add thePearBehaviour
component to it. Pears are generally smaller than agents, so let's adjust the scale to0.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 selectCreate > Material
. Rename the material toPearMaterial
and change the color to a nice yellow/green. Drag the material onto thePear
object.In the
GettingStarted
folder let's create a new folder calledPrefabs
. Drag thePear
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. Please make sure al your pears are on 0 on the y-axis of their positions.
For these actions we need data that represents the
PearCount
andHunger
values. The source of truth for these values must be our ownMonoBehaviours
. Let's create a script calledDataBehaviour
in theBehaviours
folder and add the following code:
using System;
using UnityEngine;
namespace CrashKonijn.Docs.GettingStarted.Behaviours
{
public class DataBehaviour : MonoBehaviour
{
public int pearCount = 0;
public float hunger = 0f;
private void Update()
{
// For simplicity, we will increase the hunger over time in this class.
this.hunger += Time.deltaTime * 5f;
}
}
}
Add the new
DataBehaviour
to theAgent
object in the scene.Create the sensors required for the
PearCount
andClosestPear
keys. This time we'll use something called aMultiSensor
. This sensor can provide multiple keys at once. Create a new class calledPearSensor
in theSensors
folder and inherit fromMultiSensorBase
.
using System.Collections.Generic;
using CrashKonijn.Docs.GettingStarted.Behaviours;
using CrashKonijn.Goap.Runtime;
using UnityEngine;
namespace CrashKonijn.Docs.GettingStarted.Sensors
{
// Defining a GoapId is only necessary when using the ScriptableObject configuration method.
[GoapId("PearSensor-d68c875d-29c0-43f3-9d79-054d4cc6505d")]
public class PearSensor : MultiSensorBase
{
// A cache of all the pears in the world
private PearBehaviour[] pears;
// You must use the constructor to register all the sensors
// This can also be called outside of the gameplay loop to validate the configuration
public PearSensor()
{
this.AddLocalWorldSensor<PearCount>((agent, references) =>
{
// Get a cached reference to the DataBehaviour on the agent
var data = references.GetCachedComponent<DataBehaviour>();
return data.pearCount;
});
this.AddLocalWorldSensor<Hunger>((agent, references) =>
{
// Get a cached reference to the DataBehaviour on the agent
var 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 example
return (int) data.hunger;
});
this.AddLocalTargetSensor<ClosestPear>((agent, references, target) =>
{
// Use the cashed pears list to find the closest pear
var closestPear = this.Closest(this.pears, agent.Transform.position);
if (closestPear == null)
return null;
// If the target is a transform target, set the target to the closest pear
if (target is TransformTarget transformTarget)
return transformTarget.SetTransform(closestPear.transform);
return new TransformTarget(closestPear.transform);
});
}
// The Created method is called when the sensor is created
// This can be used to gather references to objects in the scene
public override void Created() { }
// 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.
public override void Update()
{
this.pears = Object.FindObjectsOfType<PearBehaviour>();
}
// Returns the closest item in a list
private T Closest<T>(IEnumerable<T> list, Vector3 position)
where T : MonoBehaviour
{
T closest = null;
var closestDistance = float.MaxValue; // Start with the largest possible distance
foreach (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.
using CrashKonijn.Agent.Core;
using CrashKonijn.Agent.Runtime;
using CrashKonijn.Docs.GettingStarted.Behaviours;
using CrashKonijn.Goap.Runtime;
using UnityEngine;
namespace CrashKonijn.Docs.GettingStarted.Actions
{
[GoapId("PickupPear-06ef21a4-059b-4314-800a-e7c2622637fb")]
public class PickupPearAction : GoapActionBase<PickupPearAction.Data>
{
// This method is called every frame while the action is running
public override IActionRunState Perform(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 again
return ActionRunState.WaitThenComplete(0.5f);
}
// This method is called when the action is completed
public override void Complete(IMonoAgent agent, Data data)
{
if (data.Target is not TransformTarget 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 class
public class Data : IActionData
{
public ITarget Target { get; set; }
// When using the GetComponent attribute, the system will automatically inject the reference
[GetComponent]
public DataBehaviour DataBehaviour { get; set; }
}
}
}
Configuring the PickupPearGoal
In the
Capabilities
folder let's create thePearCapability
class and add the following code:
using CrashKonijn.Docs.GettingStarted.Actions;
using CrashKonijn.Goap.Core;
using CrashKonijn.Goap.Runtime;
namespace CrashKonijn.Docs.GettingStarted.Capabilities
{
public class PearCapability : CapabilityFactoryBase
{
public override ICapabilityConfig Create()
{
var builder = new CapabilityBuilder("PearCapability");
builder.AddGoal<PickupPearGoal>()
.AddCondition<PearCount>(Comparison.GreaterThanOrEqual, 3);
builder.AddAction<PickupPearAction>()
.AddEffect<PearCount>(EffectType.Increase)
.SetTarget<ClosestPear>();
builder.AddMultiSensor<PearSensor>();
return builder.Build();
}
}
}
Edit the
DemoAgentTypeFactory
and add thePearCapability
to the list of capabilities.
factory.AddCapability<PearCapability>();
Adding the PickupPearGoal to the brain.
Adjust the
RequestGoal
call in ourBrainBehaviour
to include thePickupPearGoal
.
this.provider.RequestGoal<IdleGoal, PickupPearGoal>();
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.
using CrashKonijn.Agent.Core;
using CrashKonijn.Agent.Runtime;
using CrashKonijn.Docs.GettingStarted.Behaviours;
using CrashKonijn.Goap.Runtime;
namespace CrashKonijn.Docs.GettingStarted.Actions
{
[GoapId("Eat-b235695c-727b-41a5-aa66-4757ce65719d")]
public class EatAction : GoapActionBase<EatAction.Data>
{
// This method is called every frame while the action is running
public override IActionRunState Perform(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 again
return ActionRunState.WaitThenComplete(5f);
}
// This method is called when the action is completed
public override void Complete(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 class
public class Data : IActionData
{
public ITarget Target { get; set; }
// When using the GetComponent attribute, the system will automatically inject the reference
[GetComponent]
public DataBehaviour DataBehaviour { get; set; }
}
}
}
Creating the EatCapability
This is your time to shine, create a new capability called
EatCapability
that uses theEatGoal
andEatAction
.
EatGoal
Conditions
- Hunger <= 0
EatAction
Effects
- Hunger--
Conditions
- PearCount >= 1
Target: none
RequiresTarget: false
Add the capability to you agent type.
Adjusting the brain
Let's adjust the BrainBehaviour
to include the EatGoal
when the hunger is higher than 50!
using System;
using CrashKonijn.Agent.Core;
using CrashKonijn.Agent.Runtime;
using CrashKonijn.Goap.Runtime;
using UnityEngine;
namespace CrashKonijn.Docs.GettingStarted.Behaviours
{
public class BrainBehaviour : MonoBehaviour
{
private AgentBehaviour agent;
private GoapActionProvider provider;
private GoapBehaviour goap;
private DataBehaviour data;
private void Awake()
{
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 demo
if (this.provider.AgentTypeBehaviour == null)
this.provider.AgentType = this.goap.GetAgentType("DemoAgent");
}
private void Start()
{
this.provider.RequestGoal<IdleGoal, PickupPearGoal>();
}
private void OnEnable()
{
this.agent.Events.OnActionEnd += this.OnActionEnd;
}
private void OnDisable()
{
this.agent.Events.OnActionEnd -= this.OnActionEnd;
}
private void OnActionEnd(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.

Last updated