Getting the placement of the hands correct is one of the hardest parts of doing a ledge grab system. For roof tops like in the video it is a little easier as you have an over hang for ledge so when the player is underneath it the hand placement will be correct as it is easy to get the colliders to line up. The issue comes in when you have a ledge that the player can not be directly underneath.
For these scenarios Inverse Kinematics will be a help.
I have a 2.5D 2.5D Platformer that I used this technique on, I used Scriptable Objects as variables to pass values in this project, but the principles are the same.
Along with an Article that I wrote up.
https://blog.devgenius.io/completing-the-ledge-grabbing-system-9a4fef94be3b
The first step is to enable IK Pass on the layer with the animation. In our case it is the Base Layer on the Player’s Animator Controller.
Now we need an On Animator IK Method to do our work.
LedgeDetector.cs
[RequireComponent(typeof(Collider), typeof(Rigidbody))]
public class LedgeDetector : MonoBehaviour
{
[SerializeField] private Transform leftHandPosition;
[SerializeField] private Transform rightHandPosition;
private bool _hasLeftHandPosition;
private bool _hasRightHandPosition;
private bool _useIK;
public event Action<Vector3> OnLedgeDetect;
private void Awake()
{
_hasRightHandPosition = rightHandPosition;
_hasLeftHandPosition = leftHandPosition;
}
private void OnTriggerEnter(Collider other)
{
OnLedgeDetect?.Invoke(other.transform.forward);
_useIK = true;
}
private void OnTriggerExit(Collider other)
{
_useIK = false;
}
//private void OnAnimatorIK(int layerIndex)
public void OnLedgeAnimatorIK([NotNull] Animator animator)
{
float weight = _useIK ? 1 : 0;
Debug.Log($"{_useIK}: {weight}, {_hasLeftHandPosition}, {_hasRightHandPosition}");
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, _hasRightHandPosition ? weight : 0);
animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, _hasLeftHandPosition ? weight : 0);
if (_hasRightHandPosition)
animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandPosition.position);
if (_hasLeftHandPosition)
animator.SetIKPosition(AvatarIKGoal.LeftHand, leftHandPosition.position);
}
}
When Play Testing I Realized that for some reason the On Animator IK was not getting called from on the Ledge Detector to fix this I made the method public and added the On Animator IK Method to the Player State Machine.
In Player state machine
private void OnAnimatorIK(int layerIndex)
{
if (LedgeDetector) LedgeDetector.OnLedgeAnimatorIK(Animator);
}
Set Everything up in Unity, to set where the positions of the hands should go I lined them up with the hands on the player and just moved them forward on the Z axis a bit. When entering Play Mode I got found that I picked a correct position.
Play Testing
This can be approved upon. Like add a Ledge script to the Ledge and set the Use IK if you want to use IK for that ledge. So for over hang type ledges you would set it to false and for wall like edges set it to true.
public class Ledge : MonoBehaviour
{
[field: SerializeField] public bool UseIK { get; private set; }
}
In LedgeDetector
private void OnTriggerEnter(Collider other)
{
OnLedgeDetect?.Invoke(other.transform.forward);
if (other.TryGetComponent(out Ledge ledge))
_useIK = ledge.UseIK;
}
Or you can use a raycast to set the position of the hands to the nearest object surface.