Best practices - communicate with game code

bbjones

Member
Scenario: Crafting with capacity limits, animations etc

For now I have a simple BT where the AI is assigned an external BT for whatever job they are assigned to, let's say foraging for food.

I setup a sharedVariable for the job location (gameobject I place in the world).
BT is simple at this point:
  • Seek - sharedVariable "job location"
  • Wait - (Simulates animation and AI checking if inventory is full)
  • FindNextForagePoint - custom action that finds the next "forage" tag as a gameObject, sets shared variable nextForageItem
  • Seek - sharedVariable "next forage item"
  • Wait - (again Simulates animation and AI checking if inventory is full)
Currently that's all in sequence on an endless repeater and works as expected.

What I am trying to do next is fill out the 2 Wait action placedholders with something like:
  • goes to job location, play animation checking inventory (I'm assuming I'll just use the Unity->Animation actions in BD)
  • check code - Is location full? yes/no
  • if no, find next forage target and seek
  • at next forage target, I need to interact with the resource and reduce it's inventory, which is all handled in code
  • if forage target becomes empty, move to next forage target
  • etc
There would be lots of new decisions based on how I interact with the game code. Question is, how would I interact with the game code?

My best guess is to use the send/receive events? Something like...
  • Get to forage loctation, send event "arrived at resource point", then BD waits for response, likely playing some animation
  • in code I receive that event, process resource inventory being depleted over time until zero, then raise event "resource depleted" in the BT
  • the BT receives that event and decides how to proceed.
Does that make sense or am I going about this all wrong?

Any examples that have this type of two-way communication with game code?

Thanks
 
Does that make sense or am I going about this all wrong?
That way will definitely work. The alternative way would be to just call method directly, which is similar to how the integration tasks work.

Any examples that have this type of two-way communication with game code?
As an example the UCC behavior tree can be used. When the weapon is used it starts the Use ability, and then that use ability will decrement the ammo amount. The Has Ammo task will check that ammo amount on the next tick to determine if the character is out of ammo.

In this case either option will work, so it's mostly how you have structured your code and if other objects will need to listen for that same event.
 
One thing I'm not clear on, I'm thinking of it like I want to use a fork action, a task that provides a yes or no response but only when I get to it.

For example, my rotation is to go out, find some food, pick it up and return to the drop off point. Only when I get back there do I want to check if that drop off point is full, or becomes full while I'm dropping off stuff. If yes, I want to engage a new branch, then when that is done, go back to my regular rotation. If no, I just continue with the regular rotation.

I was thinking about adding new custom decisions that bridge with my game code, that way I can build up decisions like "is drop off point full" and use them for other jobs. I think I can do that but only if I return failure if the drop off point isn't full. Is that how I should be creating a yes/no response from a decision?

I definitely don't want the drop off point being constantly re-evaluated when the AI isn't at the location.

And with the UCC integration demo, I don't see how the NolanAgent BT is setting up the 2 parameters for DemoAgent.Ammo and .Health. How are those being set in the BT? They look like regular floats in the BT variable list, but I don't seem able to change in them in the inspector.
 
. I think I can do that but only if I return failure if the drop off point isn't full. Is that how I should be creating a yes/no response from a decision?
Yes, a conditional task that returns failure if it's not full would work great. If you combine that with a Self conditional abort it will only be reevaluated when that local branch is being run.

How are those being set in the BT? They look like regular floats in the BT variable list, but I don't seem able to change in them in the inspector.
They are used using property mappings: https://opsive.com/support/documentation/behavior-designer/variables/
 
Having some issues trying to play a custom animation on the AI Agent.

I have a farming loop animation. I know it works on the rig from a simple test.
Add non-Opsive character to scene. Add new custom controller that has idle and my farming loop.
Map an input key to play the farming loop animation on the controller when pressed, eg:
Code:
        if (Input.GetKeyDown(this.PlayClipInput))
        {
            animator.CrossFadeInFixedTime(clipName, 0.1f);
        }

If I add a the new animation to the opsive animator, I can't get the animation to play with the same approach. I've tried several variations.
Looking at the docs I didn't find clear guidance on how to add new animations.

And I'm not even sure that is the direction I should take.
Should I instead be looking at adding state changes or new AI abilities and mange those from the BT?

Ideally I can keep a single animator controller for all activities instead of swapping them in/out since I'll always want things like enemy detection.

The initial goal is simple.
Have an Opsive AI Agent standing idle. WORKS
Move to some location in the world. WORKS
Once arrived, start playing some looping animation - NOT WORKING
 
The character controller takes control of the animator so you can't manually set an animation. You will either need to create a new ability or use the generic ability for a simple animation:

 
Ok got that working with multiple animations, simple enough it seems.

Question though, what's the best way to deal with a looping animation? Wait for a change in ability index or other input (like combat movement) to interrupt? For now I have one looping animation with no exit transition. It says looping until the ability index is changed. Seems to work so far.

Back to the BT, I should then add a new custom action that handles starting whichever generic action ability index for whatever stage in crafting the AI is in? I'm thinking something pretty simple like an action that links to the AI controller via sharedVar and provides the correct abilityIndex.
When the action plays in the BT is basically just does this:
Code:
this.aiAnimatorController.SetInteger("AbilityIndex", this.AbilityIndex2);
this.aiAnimatorController.SetBool("AbilityChange", true);

Are there any downsides to using the Generic ability?
 
Question though, what's the best way to deal with a looping animation? Wait for a change in ability index or other input (like combat movement) to interrupt? For now I have one looping animation with no exit transition. It says looping until the ability index is changed. Seems to work so far.
That'll work.

Are there any downsides to using the Generic ability?
You should not set the parameters on the animator controller directly. There's a chance that the character controller will overwrite them because it doesn't know of your changes. You should instead use the ability system to set the ability index. If the generic ability doesn't provide all of the functionality that you need you should subclass it. The ability index can be set with the generic ability, though.
 
I switched over to use the Generic ability to control start/stopping, and changing the index param, it does work but cuts short right after starting the animation.
I've read the abilities documentation, tried a bunch of changes but can't get the ability to play it's total duration like it does when I call the animation directly on the animator. Any ideas?

I've tried it in the custom action as well as just through some custom code triggered by keyboard input, same problem no matter how I trigger it.

It doesn't appear to be a priority problem since putting the Generic ability at the top of the list makes no difference.

Part of the code...
C#:
this.genericAbility = uccLocomotion.GetAbility<Generic>();
this.genericAbility.AbilityIndexParameter = abilityIndex;
bool abilityStarted = uccLocomotion.TryStartAbility(this.genericAbility);
 
If you place a breakpoint within Generic.AbilityStopped can you tell what is stopping the ability?
 
What does the call stack look like? From there I should be able to see what is stopping it.
 
There's no call stack because there is no error. Everything is working the animation just stops right after starting.

Here's a sample script I'm using to trigger 2 different animations by key input.
They both work correctly when NOT using the Generic ability.

Code:
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Character.Abilities;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class OpsiveAiTestInput : MonoBehaviour
{
    public bool useGenericAbility;

    public int AbilityIndex = 10100;
    public KeyCode AbilityChangeInput = KeyCode.Keypad6;

    public int AbilityIndex2 = 10200;
    public KeyCode AbilityChangeInput2 = KeyCode.Keypad7;

    public Animator aiAnimatorController;
    public UltimateCharacterLocomotion uccLoco;
    private Generic genericAbility;

    void Update()
    {
        bool doAbility = false;
        int abilityIndex = 0;

        if (Input.GetKeyDown(this.AbilityChangeInput))
        {
            doAbility = true;
            abilityIndex = this.AbilityIndex;

        }
        if (Input.GetKeyDown(this.AbilityChangeInput2))
        {
            doAbility = true;
            abilityIndex = this.AbilityIndex2;
        }

        if (doAbility && uccLoco != null)
        {
            if (useGenericAbility)
            {
                this.genericAbility = uccLoco.GetAbility<Generic>();

                if (this.genericAbility != null)
                {
                    this.genericAbility.AbilityIndexParameter = abilityIndex;
                    bool abilityStarted = uccLoco.TryStartAbility(this.genericAbility);
                    print("abilityStarted=" + abilityStarted.ToString());
                }
            }
            else
            {
                this.aiAnimatorController.SetInteger("AbilityIndex", abilityIndex);
                this.aiAnimatorController.SetBool("AbilityChange", true);
                print("manuallyStartAnimation.");
            }
        }
    }
}

Details from the long duration/looping animation:
1585925645821.png
1585925697033.png
 
If you place a breakpoint (or log) within Generic.AbilityStopped you can then get a call stack. This method does not exist within the generic ability but you can add it and just call the base method. In your script you are manually setting the animator parameters which you don't want to do - you can use the generic ability to set the index.

Code:
        protected override void AbilityStopped(bool force)
        {
            base.AbilityStopped(force);
        }
 
Note I am using the Generic ability in that code, but have an option to use the animator parameters since that plays the animation correctly for now. I'll remove that once the Generic ability is working correctly.

Here's the stack trace:
Code:
  at Opsive.UltimateCharacterController.Character.Abilities.Generic.AbilityStopped (System.Boolean force) [0x00001] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Character.Abilities.Ability.StopAbility (System.Boolean force, System.Boolean fromController) [0x00021] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Character.UltimateCharacterLocomotion.TryStopAbility (Opsive.UltimateCharacterController.Character.Abilities.Ability ability, System.Boolean force) [0x00174] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Character.Abilities.Ability.StopAbility (System.Boolean force, System.Boolean fromController) [0x0000a] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Character.Abilities.Ability.StopAbility () [0x00001] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Character.Abilities.Generic.OnComplete () [0x00001] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Game.ScheduledEvent.Invoke () [0x00001] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Game.Scheduler.Invoke (Opsive.UltimateCharacterController.Game.ScheduledEventBase scheduledEvent, System.Int32 index) [0x00025] in <99918ab4ca274c16aa56ede380e6682f>:0
  at Opsive.UltimateCharacterController.Game.Scheduler.FixedUpdate () [0x00030] in <99918ab4ca274c16aa56ede380e6682f>:0
Generic.AbilityStopped() at /Opsive/UltimateCharacterController/Scripts/Character/Abilities/Generic.cs:33
31:   System.Diagnostics.StackTrace t = new System.Diagnostics.StackTrace();
-->33:   Debug.Log(t);
35:   base.AbilityStopped(force);

Ability.StopAbility() at /Opsive/UltimateCharacterController/Scripts/Character/Abilities/Ability.cs:679
677:   m_ActiveIndex = -1;
-->679:   AbilityStopped(force);
681:   return true;

UltimateCharacterLocomotion.TryStopAbility() at /Opsive/UltimateCharacterController/Scripts/Character/UltimateCharacterLocomotion.cs:1425
1423:   m_ActiveAbilities[m_ActiveAbilityCount] = null;
-->1425:   ability.StopAbility(force, true);
1427:   // Let others know that the ability has stopped.

Ability.StopAbility() at /Opsive/UltimateCharacterController/Scripts/Character/Abilities/Ability.cs:674
672:   // If the ability wasn't stopped from the character controller then call the controller's stop ability method. The controller must be aware of the stopping.
673:   if (!fromController) {
-->674:       return m_CharacterLocomotion.TryStopAbility(this, force);
675:   }

Ability.StopAbility() at /Opsive/UltimateCharacterController/Scripts/Character/Abilities/Ability.cs:651
649:   public bool StopAbility()
650:   {
-->651:       return StopAbility(false, false);
652:   }

Generic.OnComplete() at /Opsive/UltimateCharacterController/Scripts/Character/Abilities/Generic.cs:65
63:   private void OnComplete()
64:   {
-->65:       StopAbility();
66:   }

ScheduledEvent.Invoke() at /Opsive/UltimateCharacterController/Scripts/Game/Scheduler.cs:80
78:   public override void Invoke()
79:   {
-->80:       m_Action();
81:   }

Scheduler.Invoke() at /Opsive/UltimateCharacterController/Scripts/Game/Scheduler.cs:661
659:       RemoveActiveEvent(index, scheduledEvent.Location);
660:   }
-->661:   scheduledEvent.Invoke();
662:   if (scheduledEvent.EndTime != -1) {
663:       ObjectPool.Return(scheduledEvent);

Scheduler.FixedUpdate() at /Opsive/UltimateCharacterController/Scripts/Game/Scheduler.cs:329
327:   if (m_ActiveFixedUpdateEvents[i].EndTime <= Time.time) {
328:       var prevCount = m_ActiveFixedUpdateEventCount;
-->329:       Invoke(m_ActiveFixedUpdateEvents[i], i);
330:       // An event may have been removed because of the invoke. When the element is removed the next element replace it. The scheduler shouldn't
331:       // skip over this element and should back up within the list by the number of elements removed.
 
Thanks -
at Opsive.UltimateCharacterController.Game.ScheduledEvent.Invoke () [0x00001] in <99918ab4ca274c16aa56ede380e6682f>:0 at Opsive.UltimateCharacterController.Game.Scheduler.Invoke (Opsive.UltimateCharacterController.Game.ScheduledEventBase scheduledEvent, System.Int32 index) [0x00025] in <99918ab4ca274c16aa56ede380e6682f>:0 at Opsive.UltimateCharacterController.Game.Scheduler.FixedUpdate () [0x00030] in <99918ab4ca274c16aa56ede380e6682f>:0
It looks like the Stop Event timer is ending the Generic ability.

1585947063952.png
 
Two things:

1) I didn't have Stop Event checked so that is why it was stopping right away.

2) However, I can't get the OnAnimatorGenericAbilityComplete event to work correctly.
I can handle other animation events (note they are method calls, not actual events) and can handle OnAnimatorGenericAbilityComplete if I make my own method. But the Generic ability never seems to pick it up as an event handler. I made my version of Generic.OnComplete() public and call that from my own animation event handler/method and everything seems to work as expected.

For now I can at least continue using Generic ability and my own animation events but I would like to know how to do this in a supported way.

Thanks!
 
Based off of your last post it looks like you are calling the event wrong. The function name should be ExecuteEvent, and then event string should be OnAnimatorGenericAbilityComplete. Take a look at this page for more info:

 
Ok that makes way more sense and is now working correctly.

Note the documentation for Generic is wrong then
1586022884678.png

And the link in 07 for "animation event" points to Unitys docs, maybe it should point to the Opsive Animation Event Trigger page which explains things correctly?

Thanks a ton for all the help!
 
Top