acdamiani / schema

Visual intelligence for the Unity game engine
https://schema-ai.com
MIT License
53 stars 4 forks source link

Stop Idle Node Execution #6

Closed ijisthee closed 8 months ago

ijisthee commented 8 months ago

Hello again :)

The Idle Node is nice but it's useless for me since there is no way to abort it conditionally.

In the description is stated, that it can be pulled of from the flow by a decorator.

namespace Schema.Builtin.Nodes
{
    [Description("Will stop execution of the tree perpetually, until flow is pulled from it by a Decorator."),
     Category("Miscellaneous")]
    public class Idle : Action
    {
        public override NodeStatus Tick(object nodeMemory, SchemaAgent agent)
        {
            return NodeStatus.Running;
        }
    }
}

There are no concrete decorators in the code. The only classes that could be a node decorator are the conditionals and the modifiers.

The only modifier that could possibly used is the LoopUntil modifier. But for the public BlackboardEntrySelector<bool> condition; I'm getting an error in editor from BlackboardEntrySelectorDrawer: image

in

public static string DoSelectorDrawer(Rect position, SerializedProperty property, GUIContent label,
    Type fieldType, FieldInfo fieldInfo)
{
    Type parentType = property.serializedObject.targetObject.GetType();

    if (!valid.Any(t => t.IsAssignableFrom(parentType)))
    {
        GUI.Label(position,
            new GUIContent("Cannot use a BlackboardEntrySelector in a non tree type",
                Icons.GetEditor("console.warnicon")), EditorStyles.miniLabel);
        return "";
    }
 /// [...]
}

The Conditionals are not executed on a node that has returned a Running status in the last tick.

The only way to abort a node that is Running (e.g. MoveTo) is to write own custom logic into a new node.

Imagine this situation:

An enemy moves to a position 50 meters away and I walk to him. I'm right in front of him. He just ignores me until he has reached his location. There is no way to handle that problem without coding.

Or did I do misunderstand something ?

Thanks in advance for you help and hopefully explanations. :)

acdamiani commented 8 months ago

Hey there! I appreciate the in depth explanation of your problem.

The problem you are describing is solved by Conditional Aborts. Conditional nodes should run on NodeStatus.Running nodes if the "Aborts Type" property on the conditional is set to "Self" or "Both". Basically, setting the conditional to abort itself means that any node in the subtree of the node attached to the conditional will be exited when the conditional evaluates to a value described by the "Aborts When" property. I can explain this more in depth should you need it, but for the sake of brevity I'll skip it for now. The engine marks these conditionals as those needed to be evaluated every Tick regardless of the node currently running. (Of course, this is how it should work. It's not unlikely that I've screwed up somewhere; I haven't tested this before writing out this explanation)

You can see an example of this in the Example tree included in the project. The enemy AI is set to "wander" throughout the map using a NavMesh. Upon seeing the player, the tree immediately pulls execution to the subtree designed to "chase" the player throughout the map. Note that this conditional will trigger even when the enemy is en route to a destination, thereby demonstrating the interrupt behavior you are describing. It's on my list to improve the documentation and provide a more in depth explanation of this at some point as soon as I have more free time.

The error on the LoopUntil modifier is a bug. I'll patch it out when I get the chance.

Let me know if you have any other questions!

ijisthee commented 8 months ago

Okay, it works. And it works well. I've found my mistake and it does not seem to be possible with the actual logic to handle that.

EDIT: It does not work. It is called but it does not have an impact.

Look at this:

image image

It only works, when the distance is updated before the conditional is checked. Since the DistanceToPlayer Node does not run as long as the Wander Node is in status Running, the distance is not updated and the check fails. It has the last distance before the Wander Node has started.

image

I guess the only way around that is a custom CompareDistance Conditional that takes two transforms and checks the distance and decides to fail or succeed based on the given parameters.

Or maybe you have another solution on that?

Thank you very much.

ijisthee commented 8 months ago

The CompareDistance Evaluate method is called every Tick. Great. But the result does not seem to have any impact.

Look at this picture: image

And please have a look at the code:

see CompareDistance class using System.Text; using System.Text.RegularExpressions; using UnityEngine; namespace Schema.Builtin.Conditionals { [DarkIcon("Conditionals/d_Compare")] [LightIcon("Conditionals/Compare")] public class CompareDistance : Conditional { public enum ComparisonType { Equal, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual } [Tooltip("LHS Vector 1 for the Distance check")] public BlackboardEntrySelector firstVector; [Tooltip("LHS Vector 2 for the distance check")] public BlackboardEntrySelector secondVector; [Tooltip("RHS of the comparison")] public BlackboardEntrySelector distanceToCheck; [Tooltip("The comparison type for this operation")] public ComparisonType comparisonType; public override bool Evaluate(object decoratorMemory, SchemaAgent agent) { float distance = Vector3.Distance(firstVector.value, secondVector.value); switch (comparisonType) { case ComparisonType.Equal: Debug.Log("distance: " + distance + " " + comparisonType + " distanceToCheck:" + distanceToCheck.value + "; result: " + (distance == distanceToCheck.value)); return distance == distanceToCheck.value; case ComparisonType.GreaterThan: Debug.Log("distance: " + distance + " " + comparisonType + " distanceToCheck:" + distanceToCheck.value + "; result: " + (distance > distanceToCheck.value)); return distance > distanceToCheck.value; case ComparisonType.GreaterThanOrEqual: Debug.Log("distance: " + distance + " " + comparisonType + " distanceToCheck:" + distanceToCheck.value + "; result: " + (distance >= distanceToCheck.value)); return distance >= distanceToCheck.value; case ComparisonType.LessThan: Debug.Log("distance: " + distance + " " + comparisonType + " distanceToCheck:" + distanceToCheck.value + "; result: " + (distance < distanceToCheck.value)); return distance < distanceToCheck.value; case ComparisonType.LessThanOrEqual: Debug.Log("distance: " + distance + " " + comparisonType + " distanceToCheck:" + distanceToCheck.value + "; result: " + (distance <= distanceToCheck.value)); return distance <= distanceToCheck.value; } return false; } public override GUIContent GetConditionalContent() { StringBuilder sb = new StringBuilder(); sb.AppendFormat("If distance ({0},{1}) is ", firstVector.name, secondVector.name); string cName = Regex.Replace(comparisonType.ToString(), "(\\B[A-Z])", " $1").ToLower(); switch (comparisonType) { case ComparisonType.Equal: case ComparisonType.LessThanOrEqual: case ComparisonType.GreaterThanOrEqual: sb.AppendFormat("{0} to ", cName); break; case ComparisonType.GreaterThan: case ComparisonType.LessThan: sb.AppendFormat("{0} ", cName); break; } sb.AppendFormat("{0}", distanceToCheck.name); return new GUIContent(sb.ToString()); } } }

As you can see, it works like your CompareTo code with some small adaptions.

I have digged a bit deeper and found some possible reasons for that. However I don't understand the algorithm completely.

In ExecutableNode.RunDynamicConditionals the status from bool status = c.Evaluate(conditionalMemory[id][j], context.agent); (line 187) is never the same status like the last status that is checked here (lines 198-202):

if (status != last
    && ((status && abortOnSuccess) || (!status && abortOnFailure))
    && ((isSubAbort && isSub) || (isPriorityAbort && isPriority))
   )
    return true;

Same with ExecutableNode.DoAction (lines 335-345):

if (context.last.index != index)
    for (int j = 0; j < action.conditionals.Length; j++)
    {
        run = action.conditionals[j].Evaluate(conditionalMemory[id][j], context.agent);
        run = action.conditionals[j].invert ? !run : run;

        lastConditionalStatus[j] = run;

        if (!run)
            break;
    }

This code also does not run since last context index is same like index.

I hope it helps to provide a fix or at least an explanation. :)

Thx again

acdamiani commented 8 months ago

Thanks again for looking into this! Yes, this seems like a bug; I've managed to reproduce it on my system. I'll work on a fix this weekend. To make sure this thread doesn't get too crowded, I've separated each concern detailed here into other issues: see #7, #8, #9, and #10.

To your question about custom conditional logic: yes, creating custom conditionals is the best solution right now since the engine does not allow for concurrent execution. It's something I could look into for a future release. Feel free to reach out if you notice anything else. I'll work on getting these problems fixed in the coming days.

ijisthee commented 8 months ago

Thank you very much. I appreciate it a lot!