FlowReactor Custom ...
 
Notifications
Clear all

FlowReactor Custom Node to Evaluate Expressions with IKT Formula Parser  

  RSS

JFalcon
(@jfalcon)
New Member
Joined: 1 year ago
Posts: 2
22/08/2020 12:55 am  

Hello all!

This is my first node submission.  The code for the node is pretty much self-contained, so I didn't bother creating a .unitypackage file for it and will just put it in-line with this post.  Feel free to use it as you see fit, there is absolutely no warranty provided.

There is a pre-requisite that you have the IKT Formula Parser asset (it is $15 USD) from the Unity Asset Store.  Sorry, but it is required for this to be functional. The project also needs to be configured to use the .NET 4.0 Framework, a requirement for this component.

The node uses two variables: the "formula" (an FRString) which is the expression that will be parsed, and the "result" (an FRFloat) which will be the result of the expression.  You can create variables on your Blackboard(s) which are either FRFloat, FRInt, or FRString values that can be resolved as part of the expression (a conversion will be done if necessary and I would avoid FRString values for performance reasons).

Variable names, when resolved, are converted using ToUppercaseInvariant().  This is due to the fact that the formula parser does the same with the variable names it tries to resolve.  The custom node will do this with Blackboard variables internally, so if you use an uppercase or lowercase (i.e. 'x' or 'X') then it will resolve.

Variables are looked up by name, not by their internal Guid value which is not known from the formula string.  So if you have more than one variable with the same name in a Blackboard or the same name across multiple Blackboards in the same graph, then the first one found wins.  Once a variable is found after walking the hierarchy, a reference to its FRVariable is saved in a Dictionary for resolution in subsequent lookups (such as if the node is executed multiple times in a loop).

Without further ado, here is the code (Formula.cs):

using System;
using System.Collections.Generic;
 
using UnityEngine;
 
using IkTools.FormulaParser;
 
#if UNITY_EDITOR
#endif
 
namespace FlowReactor.Nodes
{
    [NodeAttributes("Math""Evaluates a formula using IKT Formula Parser.""actionNodeColor"1NodeAttributes.NodeType.Normal)]
    public class Formula : Node
    {
        // This uses the IKT Formula Parser asset from the Unity Asset Store.
        // link:  https://assetstore.unity.com/packages/tools/input-management/ikt-formula-parser-174653 
        private FormulaParser formulaParser = new FormulaParser();
 
        // Node variable provider to resolve node variables from the blackboard(s)
        private NodeVariableProvider variableProvider = new NodeVariableProvider();
 
        // Current formula that has been parsed.
        private Color savedNodeColor;
 
        // Parsed formula to be cached and used when needed.
        private FormulaTemplate parsedFormula = null;
 
        // Cached formula delegate to perform actual calculation.
        private Func<NodeVariableProviderdouble> formulaDelegate;
 
        // This represents the formula that will be parsed.
        [Title("Formula")]
        public FRString formula;
 
        // This is the float result that will be returned 
        [Title("Result")]
        public FRFloat result;
 
        // Editor node methods
#if UNITY_EDITOR
        // Node initialization called upon node creation
        public override void Init(Graph _graph, Node _node)
        {
            base.Init(_graph, _node);
 
            icon = EditorHelpers.LoadIcon("mathIcon.png");
 
            disableDefaultInspector = true;
            disableVariableInspector = false;
            disableDrawCustomInspector = true;
 
            nodeRect = new Rect(nodeRect.x, nodeRect.y, 15060);
        }
 
        public override void DrawGUI(string _title, int _id, Graph _graph, GUISkin _editorSkin)
        {
            base.DrawGUI("Formula", _id, _graph, _editorSkin);
        }
#endif
 
        public override void OnInitialize(FlowReactorComponent _flowReactor)
        {
            // Save the node color in case we set it to 'Red' to indicate an error.
            this.savedNodeColor = node.color;
 
            // Set the root graph (used for accessing the blackboards) in our variable resolver.
            this.variableProvider.SetGraph(_flowReactor.graph);
 
            // Watch for the formula to be changed so we can re-parse if necessary.
            this.formula.OnValueChanged += ValueChanged;
 
            // Perform initial parsing attempt.
            AttemptParseFormula(this.formula.Value);
        }
 
        private void ValueChanged(FRVariable formula)
        {
            // Attempt to get the FRString formula value that changed.
            var newFormula = (formula as FRString);
 
            // If the formula is a string type, attempt to parse its changed value;
            if (newFormula != null)
            {
                AttemptParseFormula(newFormula.Value);
            }
        }
 
        private void AttemptParseFormula(string formula)
        {
            // If null, empty or whitespace, don't bother.
            if (string.IsNullOrWhiteSpace(formula))
            {
                return;
            }
 
            try
            {
                // Attempt to parse the formula and save the parsed template.
                var parsedFormula = this.formulaParser.Parse(formula);
 
                if (parsedFormula != null)
                {
                    // Save the parsed formula for future use (until it changes again).
                    this.parsedFormula = parsedFormula;
                }
 
                // Restore the node's color (just in case we changed it on an Exception).
                this.node.color = this.savedNodeColor;
            }
            catch (Exception e)
            {
                // Note: We don't want an exception to halt running in the event that the user is typing the formula at runtime.
                this.parsedFormula = null;
                this.node.color = Color.red;
 
                Debug.LogError(e.Message);
            }
        }
 
        // Execute node
        public override void OnExecute(FlowReactorComponent _flowReactor)
        {
            // If we don't have a parsed formula template, nothing to do, move along.
            if (this.parsedFormula == null)
            {
                ExecuteNext(0, _flowReactor);
                return;
            }
 
            try
            {
                if (this.result != null)
                {
                    // Resolve the current variables for the formula template with our variable provider.
                    var formulaResult = this.parsedFormula.Resolve(variableProvider);
 
                    // Call the returned formula result to evaluate the expression.
                    this.result.Value = (float)(formulaResult(variableProvider));
 
                    // Restore the node color if everything went well.
                    this.node.color = this.savedNodeColor;
                }
            }
            catch (ArgumentException e)
            {
                // If we cannot resolve a variable, set the node red and log an error.
                this.node.color = Color.red;
 
                Debug.LogError(e.Message);
            }
 
            // Execute the next node.
            ExecuteNext(0, _flowReactor);
        }
    }
    
    // This is the variable provider from the FlowReactor blackboards in the graph.
    public class NodeVariableProvider : IVariableProvider<NodeVariableProvider>
    {
        // This will have a reference to the collection of Blackboards in the graph.
        private ICollection<Graph.Blackboards> graphBlackboards;
 
        // This is the variables name and its FRVariable derived value cached for faster lookup again.
        private readonly Dictionary<stringFRVariable> variableCache = new Dictionary<stringFRVariable>();
 
        public void SetGraph(Graph graph)
        {
            // Must be called from the node's OnInitialize() set a reference to the Blackboards.
            this.graphBlackboards = graph.blackboards.Values;
        }
 
        public Func<NodeVariableProviderdouble> Get(string name)
        {
            // No Blackboards, no play.
            if (this.graphBlackboards == null)
            {
                throw new Exception("Blackboards do not exist for this node graph.");
            }
 
            // Attempt to find the variable name being resolved in the Blackboards (provided string is upper culture invariant).
            FRVariable foundVariable = FindBlackboardVariable(name);
 
            // If we can't find the variable, it can't be resolved
            if (foundVariable == null)
            {
                throw new ArgumentException("Variable was not found in the blackboard(s)."nameof(name));
            }
 
            // This will be the reult we return after processing the formula.
            float result = default(float);
 
            if (foundVariable is FRFloat)
            {
                // If the variable is a float, take it at face value.
                result = (foundVariable as FRFloat).Value;
            }
            else if (foundVariable is FRInt)
            {
                // If the variable is an int, we can cast to a float.
                result = (float)((foundVariable as FRInt).Value);
            }
            else if (foundVariable is FRString)
            {
                // If the variable is a string, we can convert to a float (yuck!)
                if (float.TryParse((foundVariable as FRString).Value, out float tempFloat))
                {
                    result = tempFloat;
                }
            }
            else
            {
                // We give up and throw an exception here, none of these variables can be assigned to a float.
                throw new ArgumentException("Variable is not the correct or coercable type for this formula.");
            }
 
            // Return the delegate that will be called to evaluate the expression.
            return v => result;
        }
 
        private FRVariable FindBlackboardVariable(string name)
        {
            // Check to see if we've already cached this FRVariable, if so, return it.
            if (this.variableCache.ContainsKey(name))
            {
                return this.variableCache[name];
            }
 
            // See if we can find the variable in the Blackboard list (first one wins!).
            FRVariable foundVariable = null;
 
            // Walk each of the graph Blackboards.
            foreach (var graphBlackboard in this.graphBlackboards)
            {
                // Get a reference to teh variables for the Blackboard.
                var blackboardVariables = graphBlackboard.blackboard.variables.Values;
 
                // Walk through each of the variables and try to find it.
                foreach (var variable in blackboardVariables)
                {
                    // Using ToUpperInvariant() to check with the provided name from resolver (using same casing).
                    if (variable.name.ToUpperInvariant().Equals(name))
                    {
                        // If we found the variable, let's add it to the cache for next time.
                        foundVariable = variable;
                        this.variableCache.Add(name, variable);
 
                        break;
                    }
                }
 
                // If we already found a variable, no need to keep looking.
                if (foundVariable != null)
                {
                    break;
                }
            }
 
            // Return the found variable (if any).
            return foundVariable;
        }
    }
}

Any feedback is welcome, please be gentle!

- J

 

This topic was modified 1 year ago 3 times by JFalcon

Quote
Share:

Privacy Settings
We use cookies to enhance your experience while using our website. If you are using our Services via a browser you can restrict, block or remove cookies through your web browser settings. We also use content and scripts from third parties that may use tracking technologies. You can selectively provide your consent below to allow such third party embeds. For complete information about the cookies we use, data we collect and how we process them, please check our Privacy Policy
Youtube
Consent to display content from Youtube
Vimeo
Consent to display content from Vimeo
Google Maps
Consent to display content from Google