Exemple #1
0
        /* Convert Brainiac XML to JSON */
        private void ConvertStoryToJSON()
        {
            //Load XML from brainiac and setup our variables
            var           gameDoc     = XDocument.Load("elements/" + GetCurrentWorkspace().Attribute("folder").Value + "/main.xml");
            XElement      gameCoreXML = gameDoc.Element("Behavior").Element("Node").Element("Connector").Element("Node");
            string        finalOutput = "{";
            List <string> prefixes    = new List <string>();

            //Load language XML
            XDocument languageDoc  = XDocument.Load("elements/" + GetCurrentWorkspace().Attribute("folder").Value + "/" + CompileLanguage.SelectedItem + ".xml");
            XElement  languageData = languageDoc.Element("language");

            //Start traversing XML...
            int levelCount = 0;

            foreach (var levelNode in gameCoreXML.Element("Connector").Elements())
            {
                if (levelNode.FirstAttribute.Value == "TextAdventure.Nodes.GameLevel") //Level
                {
                    finalOutput += "\"" + levelCount.ToString() + "\":{\"level_name\":\"" + levelNode.Attribute("Name").Value + "\", ";
                    int zoneCount = 0;
                    foreach (var ZoneNode in levelNode.Element("Connector").Elements())
                    {
                        if (ZoneNode.FirstAttribute.Value == "TextAdventure.Nodes.LevelZone") //Zone in current level
                        {
                            finalOutput += "\"" + zoneCount.ToString() + "\":{\"zone_name\":\"" + ZoneNode.Attribute("Name").Value + "\", ";
                            int stateCount = 0;
                            foreach (var stateNode in ZoneNode.Element("Connector").Elements())
                            {
                                if (stateNode.FirstAttribute.Value == "TextAdventure.Nodes.ZoneState") //State in current zone
                                {
                                    finalOutput += "\"" + stateCount.ToString() + "\":{\"state_name\":\"" + stateNode.Attribute("Name").Value + "\", ";
                                    foreach (var inputActionNode in stateNode.Element("Connector").Elements())
                                    {
                                        if (inputActionNode.FirstAttribute.Value == "TextAdventure.Nodes.ZoneIntro") //Zone intro text for state
                                        {
                                            finalOutput += "\"zone_intro\":\"" + GetLocalisedString(inputActionNode.Attribute("IntroText").Value, languageData) + "\", ";
                                        }
                                        if (inputActionNode.FirstAttribute.Value == "TextAdventure.Nodes.InputAction") //Input action in current zone
                                        {
                                            string thisAction = inputActionNode.Attribute("Action").Value;
                                            if (!prefixes.Contains(thisAction))
                                            {
                                                prefixes.Add(thisAction);
                                            }
                                            finalOutput += "\"" + thisAction + "\":{";
                                            foreach (var inputSubjectNode in inputActionNode.Element("Connector").Elements())
                                            {
                                                if (inputSubjectNode.FirstAttribute.Value == "TextAdventure.Nodes.InputSubject") //Input subject for current action
                                                {
                                                    //Here our logic is handled...
                                                    string thisSubject = inputSubjectNode.Attribute("Subject").Value;
                                                    if (thisSubject == "")
                                                    {
                                                        thisSubject = "*"; //Can't have blank object names in Unity
                                                    }
                                                    finalOutput += "\"" + thisSubject + "\":{";
                                                    List <string> gameDataToAdd    = new List <string>();
                                                    List <string> gameDataToRemove = new List <string>();
                                                    foreach (var inputScriptNode in inputSubjectNode.Element("Connector").Elements())
                                                    {
                                                        if (inputScriptNode.FirstAttribute.Value == "TextAdventure.Nodes.ReferencedInput")
                                                        {
                                                            //Action is referenced from another node group.
                                                            finalOutput += "\"referenced\": [\"" + inputScriptNode.Attribute("InputAction").Value + "\",\"" + inputScriptNode.Attribute("InputSubject").Value + "\"]";
                                                        }
                                                        else
                                                        {
                                                            if (inputScriptNode.FirstAttribute.Value == "TextAdventure.Nodes.GameDataExists")
                                                            {
                                                                //Optional/Required GameData Condition(s)
                                                                if (inputScriptNode.Attribute("GameDataCondition").Value == "OPTIONAL:0")
                                                                {
                                                                    finalOutput += "\"optional_gamedata\": ";
                                                                }
                                                                else
                                                                {
                                                                    finalOutput += "\"required_gamedata\": ";
                                                                }
                                                                string[] conditionalGameData = inputScriptNode.Attribute("GameData").Value.Split(',');
                                                                finalOutput += "[\"";
                                                                foreach (string gameData in conditionalGameData)
                                                                {
                                                                    if (gameData.Substring(0, 1) == " ")
                                                                    {
                                                                        finalOutput += gameData.Substring(1) + "\", \"";
                                                                    }
                                                                    else
                                                                    {
                                                                        finalOutput += gameData + "\", \"";
                                                                    }
                                                                }
                                                                finalOutput = finalOutput.Substring(0, finalOutput.Length - 3) + "], ";

                                                                //Response of condition for true/false outcomes.
                                                                //OPTIONAL and REQUIRED use these outcomes, however REQUIRED will ignore all following logic if false.
                                                                foreach (var dataConditionResult in inputScriptNode.Elements())
                                                                {
                                                                    if (dataConditionResult.Attribute("Identifier").Value == "ConditionTrue")
                                                                    {
                                                                        if (dataConditionResult.Element("Node").LastAttribute.Name == "SystemResponse")
                                                                        {
                                                                            finalOutput += "\"system_reply_ok\": \"" + GetLocalisedString(dataConditionResult.Element("Node").LastAttribute.Value, languageData) + "\", ";
                                                                        }
                                                                    }
                                                                    else if (dataConditionResult.Attribute("Identifier").Value == "ConditionFalse")
                                                                    {
                                                                        if (dataConditionResult.Element("Node").LastAttribute.Name == "SystemResponse")
                                                                        {
                                                                            finalOutput += "\"system_reply_issue\": \"" + GetLocalisedString(dataConditionResult.Element("Node").LastAttribute.Value, languageData) + "\", ";
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                            if (inputScriptNode.FirstAttribute.Value == "TextAdventure.Nodes.ModifyGameData")
                                                            {
                                                                //Add/Remove game data
                                                                if (inputScriptNode.Attribute("Action").Value == "ADD:0")
                                                                {
                                                                    gameDataToAdd.Add(inputScriptNode.Attribute("ModifyGameDataLocationName").Value);
                                                                }
                                                                else
                                                                {
                                                                    gameDataToRemove.Add(inputScriptNode.Attribute("ModifyGameDataLocationName").Value);
                                                                }
                                                            }
                                                            if (inputScriptNode.FirstAttribute.Value == "TextAdventure.Nodes.Response")
                                                            {
                                                                //Basic response not in a context of condition
                                                                finalOutput += "\"system_reply_ok\": \"" + GetLocalisedString(inputScriptNode.Attribute("SystemResponse").Value, languageData) + "\", ";
                                                            }
                                                            if (inputScriptNode.FirstAttribute.Value == "TextAdventure.Nodes.MoveTo")
                                                            {
                                                                //Move to...
                                                                if (inputScriptNode.Attribute("MoveToLocationType").Value == "LEVEL:0")
                                                                {
                                                                    //Level
                                                                    finalOutput += "\"new_level\": \"";
                                                                }
                                                                else if (inputScriptNode.Attribute("MoveToLocationType").Value == "ZONE:1")
                                                                {
                                                                    //Zone
                                                                    finalOutput += "\"new_zone\": \"";
                                                                }
                                                                else
                                                                {
                                                                    //State
                                                                    finalOutput += "\"new_state\": \"";
                                                                }
                                                                finalOutput += inputScriptNode.Attribute("MoveToLocationName").Value + "\", ";
                                                            }
                                                            if (inputScriptNode.FirstAttribute.Value == "TextAdventure.Nodes.GameOver")
                                                            {
                                                                //Game over!
                                                                string winState = inputScriptNode.Attribute("EndCondition").Value;
                                                                finalOutput += "\"game_over\": \"" + winState.Substring(0, winState.Length - 2) + "\", ";
                                                            }
                                                        }
                                                    }
                                                    if (finalOutput.Substring(finalOutput.Length - 2) == ", ")
                                                    {
                                                        //Tidy up
                                                        finalOutput = finalOutput.Substring(0, finalOutput.Length - 2);
                                                    }
                                                    if (gameDataToAdd.Count() > 0)
                                                    {
                                                        //Concat gamedata to add (we can have multiple nodes of this)
                                                        finalOutput += ", \"add_gamedata\": [";
                                                        for (int i = 0; i < gameDataToAdd.Count(); i++)
                                                        {
                                                            finalOutput += "\"" + gameDataToAdd[i] + "\",";
                                                        }
                                                        finalOutput  = finalOutput.Substring(0, finalOutput.Length - 1);
                                                        finalOutput += "]";
                                                    }
                                                    if (gameDataToRemove.Count() > 0)
                                                    {
                                                        //Concat gamedata to remove (we can have multiple nodes of this)
                                                        finalOutput += ", \"remove_gamedata\": [";
                                                        for (int i = 0; i < gameDataToRemove.Count(); i++)
                                                        {
                                                            finalOutput += "\"" + gameDataToRemove[i] + "\",";
                                                        }
                                                        finalOutput  = finalOutput.Substring(0, finalOutput.Length - 1);
                                                        finalOutput += "]";
                                                    }
                                                    finalOutput += "}, ";
                                                }
                                            }
                                            //Tidy up
                                            finalOutput  = finalOutput.Substring(0, finalOutput.Length - 2);
                                            finalOutput += "}, ";
                                        }
                                    }
                                    //Tidy up & increment
                                    finalOutput  = finalOutput.Substring(0, finalOutput.Length - 2);
                                    finalOutput += "}, ";
                                    stateCount++;
                                }
                            }
                            //Tidy up & increment
                            finalOutput  = finalOutput.Substring(0, finalOutput.Length - 2);
                            finalOutput += "}, ";
                            zoneCount++;
                        }
                    }
                    //Tidy up & increment
                    finalOutput  = finalOutput.Substring(0, finalOutput.Length - 2);
                    finalOutput += "}, ";
                    levelCount++;
                }
            }

            //Work out all prefixes used in the game
            string prefixesString = "";

            for (int i = 0; i < prefixes.Count(); i++)
            {
                prefixesString += prefixes[i] + "\",\"";
            }
            prefixesString = prefixesString.Substring(0, prefixesString.Length - 3);

            //Json-ify core game data
            finalOutput += "\"game_core\":{\"title\": \"" +
                           gameCoreXML.Attribute("GameTitle").Value + "\", \"developer\": \"" +
                           gameCoreXML.Attribute("GameDeveloper").Value + "\", \"invalid_input\": \"" +
                           gameCoreXML.Element("Connector").Element("Node").Attribute("InvalidInput").Value + "\", \"fatal_error\": \"A FATAL ERROR OCCURRED!\\n\\nPLEASE CHECK YOUR SCRIPTING.\", \"input_disabled\": \"" +
                           gameCoreXML.Element("Connector").Element("Node").Attribute("DisabledInput").Value + "\", \"prefixes\": [\"" + prefixesString + "\"]}}";

            //Write out to file for use in game
            Directory.CreateDirectory("elements/games/2d_asge/data/");
            File.WriteAllText("elements/games/2d_asge/data/story.json", finalOutput);
            File.WriteAllText("elements/games/2d_unity/TextAdventureGame_Data/StreamingAssets/story.json", finalOutput);
            File.WriteAllText("elements/games/3d_unreal/TextAdventureGame/story.json", finalOutput);

            //Success
            MessageBox.Show("Successfully compiled selected game!\nPlay it to try out the logic.", "Operation completed.", MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    public string GetNextZoneInPath(string startZoneName, string targetZoneName)
    {
        if (startZoneName == targetZoneName)
        {
            return(targetZoneName);
        }
        if (nodeGraph == null)
        {
            ZonePathingData pathingData = Content.LoadImmediate(dataAssetKey);
            nodeGraph = GenerateNodeGraph(pathingData);
        }
        else
        {
            clearParentNodes();
        }
        ZoneNode         item  = nodeGraph[startZoneName];
        List <string>    list  = new List <string>();
        Queue <ZoneNode> queue = new Queue <ZoneNode>();

        queue.Enqueue(item);
        ZoneNode zoneNode = null;

        while (queue.Count != 0 && zoneNode == null)
        {
            ZoneNode zoneNode2 = queue.Dequeue();
            list.Add(zoneNode2.ZoneName);
            for (int i = 0; i < zoneNode2.ZoneTransitions.Length; i++)
            {
                ZoneNode zoneNode3 = zoneNode2.ZoneTransitions[i];
                if (zoneNode3 == null || list.Contains(zoneNode3.ZoneName))
                {
                    continue;
                }
                if (zoneNode3.ZoneName == targetZoneName)
                {
                    if (zoneNode2.ParentNode == null)
                    {
                        zoneNode = zoneNode3;
                        break;
                    }
                    zoneNode3.ParentNode = zoneNode2;
                    while (zoneNode2.ParentNode != null)
                    {
                        zoneNode2 = zoneNode2.ParentNode;
                    }
                    zoneNode = zoneNode2.ZoneTransitions[0];
                    break;
                }
                ZoneNode zoneNode4 = new ZoneNode();
                zoneNode4.ZoneName        = zoneNode2.ZoneName;
                zoneNode4.ZoneTransitions = new ZoneNode[1]
                {
                    zoneNode2.ZoneTransitions[i]
                };
                zoneNode4.ParentNode = zoneNode2.ParentNode;
                ZoneNode zoneNode5 = new ZoneNode();
                zoneNode5.ZoneName        = zoneNode2.ZoneTransitions[i].ZoneName;
                zoneNode5.ParentNode      = zoneNode4;
                zoneNode5.ZoneTransitions = zoneNode3.ZoneTransitions;
                queue.Enqueue(zoneNode5);
            }
        }
        string result = "";

        if (zoneNode != null)
        {
            result = zoneNode.ZoneName;
        }
        return(result);
    }
Exemple #3
0
    /// <summary>
    /// Finds all the waypoints in the scenes and creates nodes on the node graph for them.
    /// </summary>
    public void GenerateNodes()
    {
        // Find all the relevant game objects in the scene
        GameObject[] waypointGOs       = GameObject.FindGameObjectsWithTag(WaypointTag);
        GameObject[] zoneGOs           = GameObject.FindGameObjectsWithTag(ZonesTag);
        GameObject[] transitionZoneGOs = GameObject.FindGameObjectsWithTag(TransitionZonesTag);

        // Gotta have something to work with
        if (zoneGOs == null || waypointGOs == null)
        {
            Debug.LogWarning("Zones or waypoints not found. Zone graph not generated.");
            _nodes = new ZoneNode[0];
            return;
        }

        // We're going to start by keeping a list of all the nodes as we create them
        List <ZoneNode> waypointNodes = new List <ZoneNode>();

        // Go through each object designated as a waypoint, and create nodes as appropriate for them
        foreach (GameObject waypointGO in waypointGOs)
        {
            // If the waypoint is on the ground, use the points above it
            if ((CollisionMask.value & 1 << waypointGO.layer) != 0)
            {
                List <Vector3> waypoints = GetGameObjectWaypoints(waypointGO, 2);

                for (int i = 0; i < waypoints.Count; i++)
                {
                    Vector3  waypoint = waypoints [i];
                    ZoneNode newNode  = new ZoneNode(AstarPath.active);
                    newNode.position = (Int3)waypoint;
                    newNode.Walkable = !Physics.CheckSphere(waypoint, 0.0001f, CollisionMask.value) && CanFit(waypoint);
                    newNode.GO       = waypointGO;
                    newNode.Tag      = 0;
                    newNode.Tag     |= (1 << 0); // Set ground

                    // Note that we track of left and right ledges by ASSUMING that GetWaypoints returns the waypoint in order from left to right
                    if (waypointGO.name.ToLower().Contains("ledge"))
                    {
                        if (i == 0)
                        {
                            newNode.Tag |= (1 << 1);
                        } // Set Left Ledge
                        if (i == waypoints.Count - 1)
                        {
                            newNode.Tag |= (1 << 2);
                        } // Set Right Ledge
                    }

                    waypointNodes.Add(newNode);
                }
            }

            // Walls have extra waypoints to the sides
            else if (waypointGO.GetComponent <GrabbableObject>() != null)
            {
                List <Vector3> waypoints = GetGameObjectWaypoints(waypointGO, 1);
                waypoints.AddRange(GetGameObjectWaypoints(waypointGO, 3));
                for (int i = 0; i < waypoints.Count; i++)
                {
                    Vector3  waypoint = waypoints [i];
                    ZoneNode newNode  = new ZoneNode(AstarPath.active);
                    newNode.position = (Int3)waypoint;
                    newNode.Walkable = !Physics.CheckSphere(waypoint, 0.0001f, CollisionMask.value) && CanFit(waypoint);
                    newNode.GO       = waypointGO;
                    newNode.Tag      = 0;
                    newNode.Tag     |= (1 << 4); // Set Wall
                    waypointNodes.Add(newNode);
                }
            }

            // If it's a climbable object, just subdivide it and use those points
            else if (waypointGO.GetComponent <ClimbableObject>() != null)
            {
                List <Vector3> waypoints = GetGameObjectWaypoints(waypointGO, 4);
                for (int i = 0; i < waypoints.Count; i++)
                {
                    Vector3  waypoint = waypoints [i];
                    ZoneNode newNode  = new ZoneNode(AstarPath.active);
                    newNode.position = (Int3)waypoint;
                    newNode.Walkable = !Physics.CheckSphere(waypoint, 0.0001f, CollisionMask.value) && CanFit(waypoint);
                    newNode.GO       = waypointGO;
                    newNode.Tag      = 0;
                    newNode.Tag     |= (1 << 3); // Set Climbable
                    waypointNodes.Add(newNode);
                }
            }

            // If it's not one of our three types of game objects, then we did something wrong
            else
            {
                Debug.LogWarning("Astar node generation found a gameobject that it doesn't know how to use!");
            }
        }

        // Save all the nodes we found
        _nodes = waypointNodes.ToArray();

        // Set up the mappings of nodes to zones
        _zonesWithWaypoints = new Dictionary <Bounds, List <ZoneNode> >();
        foreach (GameObject zoneGO in zoneGOs)
        {
            if (!_zonesWithWaypoints.ContainsKey(zoneGO.GetComponent <Collider>().bounds))
            {
                _zonesWithWaypoints.Add(zoneGO.GetComponent <Collider>().bounds, new List <ZoneNode>());
            }
        }

        // Set up the mappings of nodes to transition zones
        _transitionZonesWithWaypoints = new Dictionary <Bounds, List <ZoneNode> >();
        foreach (GameObject transitionZoneGO in transitionZoneGOs)
        {
            if (!_transitionZonesWithWaypoints.ContainsKey(transitionZoneGO.GetComponent <Collider>().bounds))
            {
                _transitionZonesWithWaypoints.Add(transitionZoneGO.GetComponent <Collider>().bounds, new List <ZoneNode>());
            }
        }

        // Organize all the nodes into their respective zones and transition zones
        foreach (ZoneNode node in _nodes)
        {
            Vector3 nodePos = (Vector3)node.position;
            foreach (Bounds zoneBounds in _zonesWithWaypoints.Keys)
            {
                if (zoneBounds.Contains(nodePos))
                {
                    // TODO: ACCOUNT FOR THE CASE WHERE A NODE LIES IN MORE THAN 1 ZONE
                    nodePos.z     = zoneBounds.center.z;
                    node.position = (Int3)nodePos;
                    node.Walkable = !Physics.CheckSphere(nodePos, 0.0001f, CollisionMask.value) && CanFit(nodePos);
                    _zonesWithWaypoints [zoneBounds].Add(node);
                }
            }
            foreach (Bounds transitionZoneBounds in _transitionZonesWithWaypoints.Keys)
            {
                if (transitionZoneBounds.Contains(nodePos))
                {
                    _transitionZonesWithWaypoints [transitionZoneBounds].Add(node);
                }
            }
        }
    }
Exemple #4
0
    /// <summary>
    /// Checks if going from Node A to Node B is a valid movement for a character.
    /// </summary>
    /// <param name="A">The first node.</param>
    /// <param name="B">The second node.</param>
    /// <param name="dist">The distance between the two nodes.</param>
    /// <returns>Whether or not the connection between the nodes is valid.</returns>
    public bool IsValidConnection(ZoneNode A, ZoneNode B, out float dist)
    {
        dist = 0;

        // First check that both nodes are walkable
        if (!A.Walkable || !B.Walkable)
        {
            return(false);
        }

        // We'll be using these positions a lot, so cache them
        Vector3 posA = (Vector3)A.position;
        Vector3 posB = (Vector3)B.position;

        // Then check that the character is capable of jumping from the first node to the second
        if (!CanJump(posA, posB))
        {
            return(false);
        }

        // Then do a basic check to see if there's any ground objects in the way
        Vector3 dir = posB - posA;

        dist = dir.magnitude;

        Ray ray       = new Ray(posA, posB - posA);
        Ray invertRay = new Ray(posB, posA - posB);

        bool obstructedByGround = Physics.Raycast(ray, dist, CollisionMask) || Physics.Raycast(invertRay, dist, CollisionMask);

        if (obstructedByGround)
        {
            return(false);
        }

        // Let's avoid ridiculous situations where we have to jump in a circle to make the jump.
        bool bIsLeftLedge  = (B.Tag & (1 << 1)) != 0;
        bool bIsRightLedge = (B.Tag & (1 << 2)) != 0;

        if ((bIsLeftLedge || bIsRightLedge) && Mathf.Abs(posB.x - posA.x) < _olympusAnimator.Radius)
        {
            return(false);
        }

        // Find out some values we'll be using
        bool canFall      = CanFall(posA, posB) && FallClear(posA, posB);
        bool canJump      = JumpClear(posA, posB);
        bool samePlatForm = (A.GO == B.GO) || A.GO.GetComponent <Collider>().bounds.Intersects(B.GO.GetComponent <Collider>().bounds);

        // If the waypoint are on two different platforms, make sure we are either capable of jumping over or falling over
        if (!samePlatForm && !canFall && !canJump)
        {
            return(false);
        }

        // avoid situations where there is already an existing path.
        //        if (A.GO != null && B.GO != null) {
        //            RaycastHit[] hits = Physics.RaycastAll(ray, dist);
        //
        //            foreach (RaycastHit hit in hits) {
        //                // If there's already a path, return false
        //                if (hit.collider.CompareTag(WaypointTag) && hit.collider.gameObject != A.GO && hit.collider.gameObject != B.GO) {
        //                    return false;
        //                }
        //            }
        //
        //        }

        /*
         * // TODO: We penalize required jumps
         * if(!samePlatForm && !canFall && canJump)
         *  dist *= 2;
         */
        bool isAWall = ((A.Tag & (1 << 4)) != 0);
        bool isBWall = ((B.Tag & (1 << 4)) != 0);

        if ((isAWall && isBWall) && posA.x != posB.x)
        {
            return(false);
        }

        // If we pass all the tests, return true
        return(true);
    }
Exemple #5
0
    // Make the AI input to the Animator the values that will make it reach it's defined A* Target
    public virtual void NavigateToAstarTarget(float speedRatio)
    {
        // We find the difference between the nodes path and the vectorpath (in case they're different), to find the nodes
        int      nodeOffset = Path.vectorPath.Count - Path.path.Count;
        ZoneNode prevNode   = null;

        if (CurrentPathWaypoint - (1 + nodeOffset) >= 0)
        {
            prevNode = (ZoneNode)Path.path [CurrentPathWaypoint - (1 + nodeOffset)];
        }
        ZoneNode nextNode = null;

        if (CurrentPathWaypoint - nodeOffset < Path.path.Count)
        {
            nextNode = (ZoneNode)Path.path [CurrentPathWaypoint - nodeOffset];
        }

        // We need to know some tag information about the nodes
        bool isNextNodeLeftLedge  = nextNode != null && (nextNode.Tag & (1 << 1)) != 0;
        bool isNextNodeRightLedge = nextNode != null && (nextNode.Tag & (1 << 2)) != 0;
        bool isNextNodeLedge      = isNextNodeLeftLedge || isNextNodeRightLedge;
        bool isNextNodeWall       = nextNode != null && (nextNode.Tag & (1 << 4)) != 0;

        // Determine horizontal
        float horizontalDifference       = NextWaypoint.x - transform.position.x;
        bool  isNodeToRight              = horizontalDifference > 0;
        bool  isMidAir                   = !CharAnimator.IsGrounded;
        bool  atTarget                   = AtTarget(NextWaypoint);
        bool  shouldStayStillMidair      = atTarget && isMidAir;
        float timeToJump                 = TimeToJump(transform.position + Vector3.down * (CharAnimator.Height * 0.5f), NextWaypoint); // TODO: CONFIRM THIS
        float desiredHorizontalJumpSpeed = horizontalDifference / timeToJump;

        // Try to stay still while midair and at our target location
        if (shouldStayStillMidair)
        {
            // Make sure we grab onto things when we can
            if (CharAnimator.CanHangOffObject)
            {
                if (isNextNodeRightLedge)
                {
                    CharAnimator.CharInput.Horizontal = -speedRatio;
                }
                else if (isNextNodeLeftLedge)
                {
                    CharAnimator.CharInput.Horizontal = speedRatio;
                }
                else
                {
                    CharAnimator.CharInput.Horizontal = 0;
                }
            }
            else
            {
                // Get our speedratio down towards 0 (when it's less than 0.1, it's ignored)
                float currentSpeedRatio = CharAnimator.HorizontalSpeed / CharAnimator.Settings.MaxHorizontalSpeed;
                CharAnimator.CharInput.Horizontal = -currentSpeedRatio;
            }
        }
        // Basic horizontal movement while in the air
        else if (isMidAir)
        {
            // If it's in range, let's do a controlled jump
            if (timeToJump > 0)
            {
                float normalizedDesiredSpeed = desiredHorizontalJumpSpeed / _animator.Settings.MaxHorizontalSpeed;
                CharAnimator.CharInput.Horizontal = normalizedDesiredSpeed;
            }
            // But if it's too far, go all out
            else
            {
                CharAnimator.CharInput.Horizontal = isNodeToRight ? 1 : -1;
            }
        }
        // Basic horizontal movement while on the ground
        else
        {
            // Normally it's easy to just move left or right depending on location of next node
            CharAnimator.CharInput.Horizontal = isNodeToRight ? speedRatio : -speedRatio;

            // But it's important to stop moving while landing
            if (CharAnimator.IsLanding)
            {
                CharAnimator.CharInput.Horizontal = 0;
            }
        }

        // Determine vertical
        CharAnimator.CharInput.Vertical = NextWaypoint.y - transform.position.y;
        if (atTarget && nextNode != null && isNextNodeLedge)
        {
            CharAnimator.CharInput.Vertical = 1;
        } // Press up whenever we're on a ledge

        // Determine jump
        bool isClimbing            = _animator.IsClimbing;
        bool isTurningAround       = _animator.IsTurningAround;
        bool isNodeAbove           = NextWaypoint.y - _animator.transform.position.y > 0;
        bool isNodeOnOtherPlatform = false;

        if (prevNode != null && nextNode != null)
        {
            isNodeOnOtherPlatform = (prevNode.GO != nextNode.GO) && !prevNode.GO.GetComponent <Collider>().bounds.Intersects(nextNode.GO.GetComponent <Collider>().bounds);
        }
        bool canFall    = GameManager.AI.Graph.CanFall(transform.position, NextWaypoint);
        bool shouldJump = isNodeAbove || (isNodeOnOtherPlatform && !canFall);
        bool canJump    = !isTurningAround && !CharAnimator.IsLanding && (!isMidAir || isClimbing);
        bool jump       = canJump && shouldJump;

        // We need to determine how we jump on the one frame that we do decide to jump
        if (jump)
        {
            // Always clear out our horizontal input on the frame that we jump
            CharAnimator.CharInput.Horizontal = 0;

            // NOTE: WE HAVE THIS CHECK BECAUSE WE CAN GET A NEGATIVE TIME TO JUMP IF WE'RE GOING TO A LEDGE REALLY HIGH UP.
            // THIS THROWS OFF OUR HORIZONTAL SPEED CALCULATION.
            // WE ASSUME THAT THE LEDGE IS REALLY CLOSE. THIS MIGHT BE BAD.
            bool highJumpHasBadHorizontal = (timeToJump < 0) && isNextNodeLedge;

            // Do a normal jump if we can make the jump using our normal speedRatio
            if (!isNextNodeWall && (highJumpHasBadHorizontal || Mathf.Abs(desiredHorizontalJumpSpeed) < Mathf.Abs(speedRatio * CharAnimator.Settings.MaxHorizontalSpeed)))
            {
                CharAnimator.CharInput.Jump = Vector2.up;
            }

            // But sometimes we need to take big jumps
            else if (isNodeToRight)
            {
                CharAnimator.CharInput.Jump = new Vector2(1, 1);
            }
            else
            {
                CharAnimator.CharInput.Jump = new Vector2(-1, 1);
            }


            // Always make sure we face the direction of the ledge before we jump (since we can't change direction midair)
            if ((isNextNodeLeftLedge && CharAnimator.Direction.x < 0) || (isNextNodeRightLedge && CharAnimator.Direction.x > 0))
            {
                CharAnimator.CharInput.Pickup = true;
                CharAnimator.CharInput.Jump   = Vector2.zero;
            }
        }
        else
        {
            CharAnimator.CharInput.Jump = Vector2.zero;
        }


        // Account for things that might be in the way of our movement
        // This might include obstacles above our head and nodes that are below the ground under our feet
        Vector3 xExtension    = Vector3.right * CharAnimator.Radius;
        bool    isLeftClear   = CheckClear(FootPosition - xExtension, NextWaypoint);
        bool    isMiddleClear = CheckClear(FootPosition, NextWaypoint);
        bool    isRightClear  = CheckClear(FootPosition + xExtension, NextWaypoint);

        if (isLeftClear && (!isMiddleClear || !isRightClear))
        {
            CharAnimator.CharInput.Horizontal = -speedRatio;
        }
        else if (isRightClear && (!isMiddleClear || !isLeftClear))
        {
            CharAnimator.CharInput.Horizontal = speedRatio;
        }
    }