• Unity
  • IK Climbing (Ground Constraints)

Related Discussions
...

I am creating a climbing character which climbs various slopes and shapes with its arms. I am trying to modify the SkeletonUtilityGroundConstraints.cs script, so it works the other way around (with the roof/top instead of the ground).
I'm not the most professional coder, but I get the general idea of the original script: a ray is casting from above the ground, so I've reversed that etc.

Everything is working so far in Play Mode, but in Edit Mode I receive a lot of errors when leaving the climb (when the ray isn't hitting anything).

transform.position assign attempt for 'rear_foot_goal' is not valid. Input position is { -6.835991, Infinity, 0.000000 }.

I want to re-position the IK to the default/original animation position, when leaving the climb. I noticed the original script uses float.MaxValue (as some type of reset?), which only returns NaN in my scenario (don't really know why), so I'm using Mathf.Infinity, but that causes errors in Edit Mode. Is it a bug, or what am I doing wrong?

I am doing all of my testing in the SkeletonUtilityGroundConstraint Spine Example scene with the raptor, the "rear_foot_goal" and "front_foot_goal" has these scripts attached.

Thanks in advance for all suggestions

using Spine.Unity;
using UnityEngine;

[RequireComponent(typeof(SkeletonUtilityBone))]
public class SkeletonUtilityGroundConstraint : SkeletonUtilityConstraint
{
   [Tooltip("LayerMask for what objects to raycast against")]
   public LayerMask groundMask;

   [Tooltip("How high above the target bone to begin casting from")]
   public float castDistance = 4f;

   [Tooltip("How fast the target IK position adjusts to the ground. Use smaller values to prevent snapping")]
   public float adjustSpeed = 5;

   private Vector3 origin = new Vector3(0, 0, 0);
   public Vector3 rayDir = new(0, 1, 0); 
   float hitY;
   float lastHitY;
   private Vector3 newPosition;

   protected override void OnEnable()
   {
      base.OnEnable();
      lastHitY = transform.position.y;
   }

   public override void DoUpdate()
   {
      origin = transform.position + new Vector3(0, -castDistance/2, 0);
      var distance = castDistance;

  var positionScale = hierarchy.PositionScale;
  var adjustDistanceThisFrame = adjustSpeed * positionScale * Time.deltaTime;

  var hit = Physics2D.Raycast(origin, rayDir, distance, groundMask);

  if (hit)
  {
     hitY = hit.point.y;

     if (Application.isPlaying)
        hitY = Mathf.MoveTowards(lastHitY, hitY, adjustDistanceThisFrame);

     Debug.DrawRay(origin, rayDir * distance, Color.green);
  }
  else
  {
     hitY = Mathf.Infinity; // the problematic area (?)

     if (Application.isPlaying)
        hitY = Mathf.MoveTowards(lastHitY, transform.position.y, adjustDistanceThisFrame);

     Debug.DrawRay(origin, rayDir * distance, Color.red);
  }

  newPosition = transform.position;
  newPosition.y = hitY;
  transform.position = newPosition;

  bone.bone.X = transform.localPosition.x / positionScale;
  bone.bone.Y = transform.localPosition.y / positionScale;

  lastHitY = hitY;
   }
}

Hello,

I'm sorry that you're running into an error; our resident Unity expert, Harri, will be back in on Monday and can provide more guidance on what specifically is going wrong.

It is rather hard to say why your code fails just from looking at it. Nevertheless, your following line looks wrong, it should not be called at all when the raycast does not hit anything:

hitY = Mathf.Infinity; // the problematic area (?)

Simply removing that line might fix your problem.

While having a look at the diff between the reference SkeletonUtilityGroundConstraint.cs code and your code: why did you modify so much of it? It should be working if you just set rayDir in the opposite direction (or expose rayDir as an Inspector property) and leave the rest untouched.

Hi Harald!

I definitely would like to use the original code. I created the heavily modified/simplified version of the code to find the cause of my problem, and learn.

Going back to the original code:
If I expose rayDir, and set it's Y to 1 (instead of -1), like (0, 1, 0), I get some unwanted and unpredictable results: the white line/ray goes way of from where it should origin. And if I increase the castDistance it scales weirdly from both sides, instead of just increasing the length. It also increases in length when hitting the ground/constraint (when the line turns red). If the raycast was fixed, it would solve all my problems.

You can see in the picture below, I only changed the rayDir. (drew the gizmos thicker with arrows to make it more clear...)
Note, the raptor is a bad example, imagine my character hanging from the roof with it's arms grabbing onto the ceiling. With the hands having the constraint checks.


Maybe helpful:
A simple hacky fix would be to replace the first line:

rayOrigin = transform.position + new Vector3(castOffset, castDistance, 0);

with:

var positionOffset = new Vector3(0, -castDistance * 2, 0);
rayOrigin = transform.position + new Vector3(castOffset, castDistance, 0) + positionOffset;

But it doesn't work. The ray doesn't detect anything when casting from below the bone (rear_ or front_foot_goal). I suspect it is because of the clamp further down. This line:

v.y = Mathf.Clamp(v.y, Mathf.Min(lastHitY, hitY), float.MaxValue);

Oh, now I can see some other problematic points. Any code regarding the Y component that uses Min, Max, float.MinValue, float.MaxValue, and the like might need to be inverted or adjusted for the opposite direction.

So the initialization of hitY needs to be changed:

// hitY = float.MinValue; // old line
hitY = float.MaxValue;

And the clamp line could be inverted to this:

// v.y = Mathf.Clamp(v.y, Mathf.Min(lastHitY, hitY), float.MaxValue);  // old line
v.y = Mathf.Clamp(v.y, float.MinValue, Mathf.Max(lastHitY, hitY));

Note: The above clamp line is actually equivalent to the following, as clamping with float.min or float.max values is not necessary:

// v.y = Mathf.Max(v.y, Mathf.Min(lastHitY, hitY));  // old line
v.y = Mathf.Min(v.y, Mathf.Max(lastHitY, hitY));

Nevertheless, the clamp statement might be easier to read.

Thank you, it works! Just for clarification, if someone reads this in the future, I added these changes to the "hacky fix" that was already in place (the positionOffset I added at the top). Harald let me know if I wasn't supposed to do that.

The gizmos are acting a bit weird. This should be the fix for someone who needs it:

//Vector3 hitEnd = rayOrigin + (rayDir * Mathf.Min(castDistance, rayOrigin.y - hitY)); // old
Vector3 hitEnd = rayOrigin + (rayDir * Mathf.Min(castDistance, -rayOrigin.y + hitY));

Edit:
Harald I have one last issue: I'm trying to make the ray "overshoot/look ahead", to search beyond the IK. So it can detect the terrain ahead, and start to reach for it before it's there. It doesn't seem to be possible? I should be able to just move the whole rayOrigin, e.g. by adjusting the positionOffset, so the ray goes beyond the IK, but it still only registers up until the IK? Is it the clamp?

Edit: For example the right leg doesn't go to the ground below. I would want to search beyond the orange circkle/IK. added picture to show, notice the capsule collider. I could move up the collider, but that doesn't solve the problem in the long run.

Unfortunately I don't know the current status of your current script with all modifications, and how you implemented what you describe as "overshoot/look ahead", to search beyond the IK. Even if I knew your code, it's rather hard to debug that in my head from reading. I don't think that the clamp statement is problematic here, but it's hard to tell.

You will need to check whether your call to Physics2D.Raycast(rayOrigin, rayDir, distance) hits the right target. If this is the case, you need to use the debugger and step through your code and see where things are different than what you want them to be. Alternatively, you could use Debug.Log() statements for printf-style debugging, if you prefer that.

I want it to raycast further down away from the IK, so I can check ahead if there's a target, and start to reach for it in advance.
These simple steps should work, but don't:

  1. Open the scene "SkeletonUtility GroundConstraint" in "Spine Examples > Other Examples".
  2. On your original script, called "SkeletonUtilityGroundConstraint", I am adding an exposed:
    public Vector3 positionOffset;
  3. and then replaced the first line in DoUpdate() with:
    // rayOrigin = transform.position + new Vector3(castOffset, castDistance, 0) // old
    rayOrigin = transform.position + new Vector3(castOffset, castDistance, 0) + positionOffset;

If you then change the positionOffset Y value, to be for example -2, like this (0, -2, 0). Visually everything works, the gizmos show the white line moving down, and the red line shows when are hitting a target. But the leg doesn't move down to where we are hitting.

If you didn't modify it any more than that, it's easy to test.

In this case the problem is indeed the clamp line, I noticed that v.y contains the original animation location. So you would need some adjustment to get below this original animation location. An easy fix is this:

// v.y = Mathf.Clamp(v.y, Mathf.Min(lastHitY, hitY), float.MaxValue);
v.y = Mathf.Clamp(v.y + offsetBelowAnimation.y, Mathf.Min(lastHitY, hitY), float.MaxValue);

Works, thanks for the help! Cheers :beer: :fiesta:

Glad to hear! 🙂