Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice

Question AI Planner - Initialize a DesicionController with a plan chosen during runtime

Discussion in 'AI & Navigation Previews' started by Noxalus, Oct 9, 2020.

  1. Noxalus

    Noxalus

    Joined:
    Jan 9, 2018
    Posts:
    80
    Hello!

    I already saw multiple messages here saying that it's not possible to change the plan of a DecisionController during runtime and that to do something like that now, the best way is to have multiple a DecisionController for every different plan.

    So first, I create 2 differents plans designed to be used by the same kind of agent:

    upload_2020-10-9_10-27-41.png

    The first one use multiple action with terminations and the second one has no termination and can only use "Move".

    Then, I created 2 prefabs, Plan1 and Plan2 that only contains a DecisionController with that uses that proper plan.

    But the DecisionController need to now which method call for every action available in the plan, that's why I created a small behaviour that wrap all action and call the appropriate method from my Agent class, here it is:

    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3. using UnityEngine.AI.Planner.Controller;
    4.  
    5. public class AgentActionWrapper : MonoBehaviour
    6. {
    7.     [SerializeField]
    8.     private DecisionController _decisionController = null;
    9.  
    10.     private Agent _agent;
    11.  
    12.     public DecisionController DecisionController => _decisionController;
    13.  
    14.     public void Initialize(Agent agent)
    15.     {
    16.         _agent = agent;
    17.     }
    18.  
    19.     public void Wait()
    20.     {
    21.         _agent.Wait();
    22.     }
    23.  
    24.     public IEnumerator Move(GameObject target)
    25.     {
    26.         return _agent.Move(target);
    27.     }
    28.  
    29.     public void UseTotem(GameObject totemGameObject)
    30.     {
    31.         _agent.UseTotem(totemGameObject);
    32.     }
    33.  
    34.     public void ExitNoMoney()
    35.     {
    36.         _agent.ExitNoMoney();
    37.     }
    38.  
    39.     public void ExitNoFun()
    40.     {
    41.         _agent.ExitNoMoney();
    42.     }
    43.  
    44.     public void ExitNoEnergy()
    45.     {
    46.         _agent.ExitNoMoney();
    47.     }
    48.  
    49.     public void Die()
    50.     {
    51.         _agent.ExitNoMoney();
    52.     }
    53.  
    54.     public void Soil()
    55.     {
    56.         _agent.ExitNoMoney();
    57.     }
    58. }
    Then I can link all actions in my plan prefabs, example for Plan1:

    upload_2020-10-9_10-34-24.png

    Then in my Agent class, I do that:

    Code (CSharp):
    1. private void ChooseRandomPlan()
    2. {
    3.     List<string> availablePlans = new List<string>() { "Plan1", "Plan2" };
    4.     string randomPlanAddress = availablePlans[Random.Range(0, availablePlans.Count)];
    5.  
    6.     var async = Addressables.InstantiateAsync(randomPlanAddress);
    7.     async.Completed += (AsyncOperationHandle<GameObject> obj) =>
    8.     {
    9.         GameObject plan = obj.Result;
    10.         plan.transform.SetParent(transform);
    11.  
    12.         AgentActionWrapper actionWrapper = plan.GetComponent<AgentActionWrapper>();
    13.         actionWrapper.Initialize(this);
    14.  
    15.         _decisionController = actionWrapper.DecisionController;
    16.         _decisionController.Initialize();
    17.     };
    18. }
    I load a random prefab between Plan1 and Plan2 (using Addressables), I initialize the wrapper giving him the current agent instance, retrieve the DecisionController from the instanciated prefab to use it inside of the Agent class.

    And it works, except if you use custom code (precondition/effect/reward) in the action you share between plans. In my example, the second plan has only the "Move" action, but this action is the same than the one used in the first (which make sense), and it uses custom code:

    upload_2020-10-9_10-40-14.png

    The problem here, is that custom code always uses StateData type that depends on the generated code for a specific plan!

    Code (CSharp):
    1. using Unity.AI.Planner.DomainLanguage.TraitBased;
    2. using Generated.AI.Planner.StateRepresentation;
    3. using Generated.AI.Planner.StateRepresentation.AgentPlan;
    4.  
    5. public struct MoveEffect : ICustomActionEffect<StateData>
    6. {
    7.     public void ApplyCustomActionEffectsToState(StateData originalState, ActionKey action, StateData newState)
    8.     {
    9.         // Code
    10.     }
    11. }
    So my question is: how can I use the same action, that uses custom code, in different plan? (since these plan will be applied to the same kind of entity)

    Thank you in advance for your help!
     
  2. TrevorUnity

    TrevorUnity

    Unity Technologies

    Joined:
    Jun 28, 2017
    Posts:
    118
    Have you tried creating a separate action definition but with the custom code? That does require you to duplicate the action info, which is undesirable. This is a use case we haven't encountered before. I'll raise the issue with the team.
     
  3. Noxalus

    Noxalus

    Joined:
    Jan 9, 2018
    Posts:
    80
    Thank you for your answer @TrevorUnity.

    Yes, I did try that with a MovePlan2 action that uses the MoveEffect (a custom action effect).

    upload_2020-10-10_11-53-23.png

    But as expected, it doesn't compile and I get these errors:

    It's a big problem for us because it implies we need to multiply all actions and all custom code by the number of plan we want to use these actions :/ And it will be a use case we might encounter a lot in a near future.

    For me, custom code shouldn't actually depends on any plan, since what is important for custom code is the action, not the plan. But I suppose it's not as easy as it sounds.

    Thank you for raising the issue to the team! ;)
     
  4. TrevorUnity

    TrevorUnity

    Unity Technologies

    Joined:
    Jun 28, 2017
    Posts:
    118
    Ah. I understand the issue. That's tricky. Can you tell us a little more about what the custom code does that requires separate logic for the actions?
     
  5. Noxalus

    Noxalus

    Joined:
    Jan 9, 2018
    Posts:
    80
    The custom code will compute the distance between the mover and his target and decrease the mover's needs according to the time he takes to reach the target. Here is the code:

    MoveEffect.cs

    Code (CSharp):
    1. using Unity.AI.Planner.DomainLanguage.TraitBased;
    2. using Generated.AI.Planner.StateRepresentation;
    3. using Generated.AI.Planner.StateRepresentation.AgentPlan;
    4. using UnityEngine;
    5.  
    6. public struct MoveEffect : ICustomActionEffect<StateData>
    7. {
    8.     public void ApplyCustomActionEffectsToState(StateData originalState, ActionKey action, StateData newState)
    9.     {
    10.         TraitBasedObjectId moverObjectId = newState.GetTraitBasedObjectId(action[0]);
    11.         TraitBasedObject moverObject = newState.GetTraitBasedObject(moverObjectId);
    12.  
    13.         TraitBasedObjectId targetObjectId = newState.GetTraitBasedObjectId(action[1]);
    14.         TraitBasedObject targetObject = newState.GetTraitBasedObject(targetObjectId);
    15.  
    16.         CustomMoveable mover = newState.GetTraitOnObject<CustomMoveable>(moverObject);
    17.         Location moverLocation = newState.GetTraitOnObject<Location>(moverObject);
    18.         Location targetLocation = newState.GetTraitOnObject<Location>(targetObject);
    19.  
    20.         float distance = Vector3.Distance(moverLocation.Position, targetLocation.Position);
    21.         float timeToReachTarget = distance / mover.Speed;
    22.  
    23.         mover.TimeToReachTarget = timeToReachTarget;
    24.         newState.SetTraitOnObject(mover, ref moverObject);
    25.  
    26.         new DecreaseNeedsEffect().ApplyCustomActionEffectsToState(originalState, action, newState);
    27.  
    28.         // Reset the time to reach target
    29.         mover.TimeToReachTarget = 1f;
    30.         newState.SetTraitOnObject(mover, ref moverObject);
    31.     }
    32. }
    33.  
    DecreaseNeedsEffect.cs

    Code (CSharp):
    1. using Unity.AI.Planner.DomainLanguage.TraitBased;
    2. using Generated.AI.Planner.StateRepresentation;
    3. using Generated.AI.Planner.StateRepresentation.AgentPlan;
    4. using UnityEngine;
    5.  
    6. public struct DecreaseNeedsEffect : ICustomActionEffect<StateData>
    7. {
    8.     public void ApplyCustomActionEffectsToState(StateData originalState, ActionKey action, StateData newState)
    9.     {
    10.         TraitBasedObjectId moverObjectId = newState.GetTraitBasedObjectId(action[0]);
    11.         TraitBasedObject moverObject = newState.GetTraitBasedObject(moverObjectId);
    12.    
    13.         Needs needs = newState.GetTraitOnObject<Needs>(moverObject);
    14.         CustomMoveable mover = newState.GetTraitOnObject<CustomMoveable>(moverObject);
    15.  
    16.         int tickToReachTarget = Mathf.CeilToInt(mover.TimeToReachTarget);
    17.  
    18.         needs.Energy = Mathf.Max(0, needs.Energy - tickToReachTarget);
    19.         needs.Fun = Mathf.Max(0, needs.Fun - tickToReachTarget);
    20.         needs.Restroom = Mathf.Max(0, needs.Restroom - tickToReachTarget);
    21.  
    22.         newState.SetTraitOnObject(needs, ref moverObject);
    23.     }
    24. }
    CustomMoveable trait looks like that:
    upload_2020-10-12_22-11-23.png

    With this custom code, when the planner plans to use the Move action, he knows exactly what will be the consequences for future actions. It's really similar to what Otto does in your project.
     
    Last edited: Oct 13, 2020
  6. Gnarf

    Gnarf

    Joined:
    Jul 29, 2015
    Posts:
    6
    Hi :)

    I'm attempting the same thing as @Noxalus on my end. I think what it boils down to is that the StateRepresentation generated for each plan is unique and un-castable to another (which makes sense).

    But it would be nice to be able to do something between different generated StateData classes, such as accessing the same traits. Or have an abstract StateData class.

    Thank you in advance !
     
    Last edited: Nov 4, 2020