Greetings!
I’ve been looking for a GOAP solution for an upcoming Unity ECS project, and your AI Planner seems like it could be a nice fit for it!
In order to better understand the AI Planner, see how stable/polished it is, and if it’s a good match for the project, I’m trying to make a simulated “spider kills and then eats prey” scenario.
I used the Step-by-Step guide and video from this thread as a starting point, and I was able to make the spider eat food that it finds lying around, using these traits and an action:
Eatable Trait
Eater Trait
Eat Action
However, I also wanted the Spider to understand that it could obtain food by killing. So I created these Attacker and Damageable traits, as well as an Attack action:
Attacker Trait
Damageable Trait
Attack Action
I haven’t yet figured out how to make the target object only get destroyed and spawn food after its Health reaches 0. I suppose I could do it with an ICustomActionEffect<IStateData>
, but I’m still in the process of understanding those. The Match3 sample project has a CustomSwapEffect class that implements ICustomActionEffect<StateData>
, but then it starts doing some stuff with GetTraitBasedObjectId
, GetTraitBasedObject
and newState.GetTraitOnObject<Cell>
, and I don’t know what ID or object I should use (if any). And what is the #if PLANNER_DOMAINS_GENERATED
line for?
I would also like to use custom effects to add upper and lower bounds when increasing/decreasing values. For now I’m ignoring all of that and making every attack a 1-hit kill, but if anyone could point me in the right direction, that would be much appreciated!
I’m not sure if the way I set up my Attack action to create a deadBody object with Eatable and Location traits is enough, or if I also need a custom effect or callback that spawns a prefab that contains those traits (perhaps using deadBody as a parameter).
Another concern of mine is how the Attack agent’s position has to be exactly the same as its target. In most games, that wouldn’t really work, and you would need at least a nearby operator, and preferably a collision checker or line trace. The operators we can use now are very limited, and I think this would be a great place to use a visual scripting language similar to Unreal’s Blueprints, allowing for more complex conditions without leaving the Editor, but that’s a whole 'nother project. Hopefully Unity has something like that planned.
But going back to my setup… I added the Attack action to my SpiderAgentPlan. Does the order of the actions matter when setting up a plan? From what I understood, in this context a Plan contains a list of Actions that an agent can perform, so it kinda contradicts the usual definition of plan - a sequence of actions that an agent intends to perform. The real plan will only be determined while the game is running by the Planner, so it’s strange to call this list of available actions a “plan”, unless I’m misunderstanding something. I suggest changing the name to something like “Agent Profile”.
SpiderAgentPlan
I’ve been having some recurring problems with Unity and Visual Studio freezing when trying to run or reload the project, and then having to force close them. Yesterday I was getting a “Couldn’t create compiled assemblies folder” error that only stopped happening after closing Visual Studio, because VS had given itself exclusive access to that folder and I couldn’t even open it in Windows Explorer with my Admin account.
Today I’m having a different problem. In my simulation, I have one SpiderAgent, 3 pieces of food lying around, and a stationary Prey. The Spider eats all the food, but ignores the Prey. It’s probably related to this error I’m getting in the generated Attack.cs file:
I’ve tried disabling Burst compilation, and it makes the error go away, but the spider doesn’t change its behavior. I have a feeling that I’m missing something silly. I checked Attack.cs, and the error is happening in the ApplyEffects function. Line 95 only contains a “{”, so I imagine it’s actually referring to the line before it:
using (var deadBodyTypes = new NativeArray<ComponentType>(2, Allocator.Temp) {[0] = typeof(Eatable), [1] = typeof(Location), })
Full Attack.cs code
using System;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.AI.Planner;
using Unity.AI.Planner.DomainLanguage.TraitBased;
using Unity.Burst;
using AI.Planner.Domains;
using AI.Planner.Domains.Enums;
namespace AI.Planner.Actions.SpiderAgentPlan
{
[BurstCompile]
struct Attack : IJobParallelForDefer
{
public Guid ActionGuid;
const int k_agentIndex = 0;
const int k_targetIndex = 1;
const int k_MaxArguments = 2;
[ReadOnly] NativeArray<StateEntityKey> m_StatesToExpand;
StateDataContext m_StateDataContext;
internal Attack(Guid guid, NativeList<StateEntityKey> statesToExpand, StateDataContext stateDataContext)
{
ActionGuid = guid;
m_StatesToExpand = statesToExpand.AsDeferredJobArray();
m_StateDataContext = stateDataContext;
}
public static int GetIndexForParameterName(string parameterName)
{
if (string.Equals(parameterName, "agent", StringComparison.OrdinalIgnoreCase))
return k_agentIndex;
if (string.Equals(parameterName, "target", StringComparison.OrdinalIgnoreCase))
return k_targetIndex;
return -1;
}
void GenerateArgumentPermutations(StateData stateData, NativeList<ActionKey> argumentPermutations)
{
var agentFilter = new NativeArray<ComponentType>(3, Allocator.Temp){[0] = ComponentType.ReadWrite<Unity.AI.Planner.DomainLanguage.TraitBased.Location>(),[1] = ComponentType.ReadWrite<AI.Planner.Domains.Moveable>(),[2] = ComponentType.ReadWrite<AI.Planner.Domains.Attacker>(), };
var targetFilter = new NativeArray<ComponentType>(2, Allocator.Temp){[0] = ComponentType.ReadWrite<Unity.AI.Planner.DomainLanguage.TraitBased.Location>(),[1] = ComponentType.ReadWrite<AI.Planner.Domains.Damageable>(), };
var agentObjectIndices = new NativeList<int>(2, Allocator.Temp);
stateData.GetTraitBasedObjectIndices(agentObjectIndices, agentFilter);
var targetObjectIndices = new NativeList<int>(2, Allocator.Temp);
stateData.GetTraitBasedObjectIndices(targetObjectIndices, targetFilter);
var LocationBuffer = stateData.LocationBuffer;
for (int i0 = 0; i0 < agentObjectIndices.Length; i0++)
{
var agentIndex = agentObjectIndices[i0];
var agentObject = stateData.TraitBasedObjects[agentIndex];
for (int i1 = 0; i1 < targetObjectIndices.Length; i1++)
{
var targetIndex = targetObjectIndices[i1];
var targetObject = stateData.TraitBasedObjects[targetIndex];
if (!(LocationBuffer[agentObject.LocationIndex].Position == LocationBuffer[targetObject.LocationIndex].Position))
continue;
var actionKey = new ActionKey(k_MaxArguments) {
ActionGuid = ActionGuid,
[k_agentIndex] = agentIndex,
[k_targetIndex] = targetIndex,
};
argumentPermutations.Add(actionKey);
}
}
agentObjectIndices.Dispose();
targetObjectIndices.Dispose();
agentFilter.Dispose();
targetFilter.Dispose();
}
StateTransitionInfoPair<StateEntityKey, ActionKey, StateTransitionInfo> ApplyEffects(ActionKey action, StateEntityKey originalStateEntityKey)
{
var originalState = m_StateDataContext.GetStateData(originalStateEntityKey);
var originalStateObjectBuffer = originalState.TraitBasedObjects;
var originaltargetObject = originalStateObjectBuffer[action[k_targetIndex]];
var originalagentObject = originalStateObjectBuffer[action[k_agentIndex]];
var newState = m_StateDataContext.CopyStateData(originalState);
var newDamageableBuffer = newState.DamageableBuffer;
var newAttackerBuffer = newState.AttackerBuffer;
var newLocationBuffer = newState.LocationBuffer;
TraitBasedObject newdeadBodyObject;
TraitBasedObjectId newdeadBodyObjectId;
using (var deadBodyTypes = new NativeArray<ComponentType>(2, Allocator.Temp) {[0] = typeof(Eatable), [1] = typeof(Location), })
{
newState.AddObject(deadBodyTypes, out newdeadBodyObject, out newdeadBodyObjectId);
}
{
var @Damageable = newDamageableBuffer[originaltargetObject.DamageableIndex];
@Damageable.Health -= newAttackerBuffer[originalagentObject.AttackerIndex].AttackDamage;
newDamageableBuffer[originaltargetObject.DamageableIndex] = @Damageable;
}
{
newState.SetTraitOnObject<UnderAttack>(default(UnderAttack), ref originaltargetObject);
}
{
var @Location = newLocationBuffer[newdeadBodyObject.LocationIndex];
@Location.Position = newLocationBuffer[originaltargetObject.LocationIndex].Position;
newLocationBuffer[newdeadBodyObject.LocationIndex] = @Location;
}
newState.RemoveTraitBasedObjectAtIndex(action[k_targetIndex]);
var reward = Reward(originalState, action, newState);
var StateTransitionInfo = new StateTransitionInfo { Probability = 1f, TransitionUtilityValue = reward };
var resultingStateKey = m_StateDataContext.GetStateDataKey(newState);
return new StateTransitionInfoPair<StateEntityKey, ActionKey, StateTransitionInfo>(originalStateEntityKey, action, resultingStateKey, StateTransitionInfo);
}
float Reward(StateData originalState, ActionKey action, StateData newState)
{
var reward = -1f;
return reward;
}
public void Execute(int jobIndex)
{
m_StateDataContext.JobIndex = jobIndex; //todo check that all actions set the job index
var stateEntityKey = m_StatesToExpand[jobIndex];
var stateData = m_StateDataContext.GetStateData(stateEntityKey);
var argumentPermutations = new NativeList<ActionKey>(4, Allocator.Temp);
GenerateArgumentPermutations(stateData, argumentPermutations);
var transitionInfo = new NativeArray<AttackFixupReference>(argumentPermutations.Length, Allocator.Temp);
for (var i = 0; i < argumentPermutations.Length; i++)
{
transitionInfo[i] = new AttackFixupReference { TransitionInfo = ApplyEffects(argumentPermutations[i], stateEntityKey) };
}
// fixups
var stateEntity = stateEntityKey.Entity;
var fixupBuffer = m_StateDataContext.EntityCommandBuffer.AddBuffer<AttackFixupReference>(jobIndex, stateEntity);
fixupBuffer.CopyFrom(transitionInfo);
transitionInfo.Dispose();
argumentPermutations.Dispose();
}
public static T GetAgentTrait<T>(StateData state, ActionKey action) where T : struct, ITrait
{
return state.GetTraitOnObjectAtIndex<T>(action[k_agentIndex]);
}
public static T GetTargetTrait<T>(StateData state, ActionKey action) where T : struct, ITrait
{
return state.GetTraitOnObjectAtIndex<T>(action[k_targetIndex]);
}
}
public struct AttackFixupReference : IBufferElementData
{
internal StateTransitionInfoPair<StateEntityKey, ActionKey, StateTransitionInfo> TransitionInfo;
}
}
Does anyone have an idea how to fix this? I am stuck. =\
These are my GameObject setups for the Decision and Trait components, if it helps:
SpiderAgent
(The GoapAgent/Attack Callback Method is empty for now, and the GoapAgent/Eat callback destroys the eatable GameObject, as instructed by the Step-by-step guide)
Prey
Thanks in advance!