Grapple Hook

nantoaqui

New member
Hello there!

I've been trying to create a Grapple Hook ability for the past few weeks but i don't know how to make it work on UCC.

I followed these articles and manage to implement the pendulum using a normal rigidbody. For some reason the same logic results in weird behavior when applied to the player.



Here's the code for the pendulum force calculation:

Code:
using UnityEngine;

namespace ChromaSuit.Locomotion
{
    public class WebSwingingPendulum
    {
        public float ArcForce = 2f;
        public float VelocityClampMin = 20f;
        public float VelocityClampMax = 30f;
        public float StretchWebFactor = 20f;
        public float SwingingMinAngle = -60f;
        public float SwingingMaxAngle = 60f;
        public float StretchWebAngle = 90f;
        public float BottomSpeed = 50f;


        private Vector3 playerLocation = Vector3.zero;
        private Vector3 playerVelocity = Vector3.zero;
        private Transform player;

        public void SetValues(Transform Player, Vector3 velocity)
        {
            player = Player;
            playerLocation = player.position;
            playerVelocity = velocity;
        }

        public Vector3 CalculateWebSwinForce(Vector3 anchorPoint)
        {
            var playerPosition = playerLocation + playerVelocity * Time.deltaTime;
            var dir = playerPosition - anchorPoint;
            var tetherLength = dir.magnitude;

            //var force = SwingArcForce(anchorPoint);
            var force = SwingArcForce(anchorPoint) + SpeedsUpSwingAtBottom(anchorPoint);
            // force = force.normalized * _pendulum.Radius;

            return force;
        }

        private Vector3 SwingArcForce(Vector3 anchorPosition)
        {
            var playerPosition = playerLocation;
            var dir = playerPosition - anchorPosition;

            var force = ArcForce / dir.magnitude;
            force = Mathf.Clamp(force, 1f, 4f);
            return SwingArcForceFormula(anchorPosition, VelocityClampMin, VelocityClampMax, force);
        }

        public Vector3 ClampMagnitude(Vector3 v, float max, float min)
        {
            double sm = v.sqrMagnitude;
            if (sm > (double) max * (double) max) return v.normalized * max;
            else if (sm < (double) min * (double) min) return v.normalized * min;
            return v;
        }


        private Vector3 SwingArcForceFormula(Vector3 anchorPosition, float velocityClampMin, float velocityClampMax,
            float reduceForceByFactorX)
        {
            var clampedVelocity = ClampMagnitude(playerVelocity, velocityClampMin, velocityClampMax);
            //var clampedVelocity = Vector3.ClampMagnitude(playerVelocity, velocityClampMax);

            var playerPosition = playerLocation;
            var dir = anchorPosition - playerPosition;

            var projection = Vector3.Dot(clampedVelocity, dir);
            var x = projection * dir.normalized;
            x = x * -2;
            x = x / reduceForceByFactorX;

            var stretchedWeb = StretchWeb(anchorPosition) * dir.normalized;
            return x + stretchedWeb;
        }

        private float StretchWeb(Vector3 anchorPosition)
        {
            var swingingAngle = CalculateSwingingAngle(anchorPosition);
            return (swingingAngle / StretchWebAngle) * StretchWebFactor;
        }

        private Vector3 SpeedsUpSwingAtBottom(Vector3 anchorPosition)
        {
            var velocity = playerVelocity.normalized * BottomSpeed;
            var swingingAngle = CalculateSwingingAngle(anchorPosition);

            //Debug.Log($"swingingAngle: {swingingAngle}");

            if (swingingAngle > SwingingMinAngle && swingingAngle < SwingingMaxAngle)
            {
                return velocity;
            }

            return Vector3.zero;
        }

        private const float RAD_TO_DEG = 57.2957795130823f;

        public float AngleOffAroundAxis(Vector3 v, Vector3 forward, Vector3 axis, bool clockwise = false)
        {
            Vector3 right;
            if (clockwise)
            {
                right = Vector3.Cross(forward, axis);
                forward = Vector3.Cross(axis, right);
            }
            else
            {
                right = Vector3.Cross(axis, forward);
                forward = Vector3.Cross(right, axis);
            }

            return Mathf.Atan2(Vector3.Dot(v, right), Vector3.Dot(v, forward)) * RAD_TO_DEG;
        }


        private float CalculateSwingingAngle(Vector3 anchorPosition)
        {
            Vector3 targetDir = anchorPosition - playerLocation;
            Vector3 forward = player.up;
            float angle = Vector3.SignedAngle(targetDir, forward, player.right);

            return angle;
        }
    }
}

Here's the code applied to a rigidbody:
Code:
using ChromaSuit.Locomotion;
using UnityEngine;

public class Test22 : MonoBehaviour
{
    public Rigidbody Bob;
    public Transform Anchor;

    public float ArcForce = 2f;
    public float VelocityClampMin = 20f;
    public float VelocityClampMax = 30f;
    public float StretchWebFactor = 20f;
    public float SwingingMinAngle = -60f;
    public float SwingingMaxAngle = 60f;
    public float StretchWebAngle = 90f;
    public float BottomSpeed = 50f;

    private WebSwingingPendulum webSwingingPendulum;

    private bool m_Tethered = false;
    private float m_TetherLength;
    private Vector3 m_TetherPoint;

    void Start()
    {
        m_Tethered = true;
        m_TetherPoint = Anchor.position;
        m_TetherLength = Vector3.Distance(m_TetherPoint, Bob.position);

        webSwingingPendulum = new WebSwingingPendulum();
    }

    void FixedUpdate()
    {
        webSwingingPendulum.ArcForce = ArcForce;
        webSwingingPendulum.VelocityClampMin = VelocityClampMin;
        webSwingingPendulum.VelocityClampMax = VelocityClampMax;
        webSwingingPendulum.StretchWebFactor = StretchWebFactor;
        webSwingingPendulum.SwingingMinAngle = SwingingMinAngle;
        webSwingingPendulum.SwingingMaxAngle = SwingingMaxAngle;
        webSwingingPendulum.StretchWebAngle = StretchWebAngle;
        webSwingingPendulum.BottomSpeed = BottomSpeed;
        webSwingingPendulum.SetValues(Bob.transform, Bob.velocity);

        var force = webSwingingPendulum.CalculateWebSwinForce(Anchor.transform.position);
        Bob.AddForce(force);

        Vector3 directionToGrapple = Vector3.Normalize(m_TetherPoint - Bob.position);
        float currentDistanceToGrapple = Vector3.Distance(m_TetherPoint, Bob.position);


        if (currentDistanceToGrapple > m_TetherLength)
        {
            Bob.position = m_TetherPoint - directionToGrapple * m_TetherLength;
        }
    }
}


As you can see it works perfectly fine.

Now the similar logic applied to UCC:

Code:
using Opsive.UltimateCharacterController.Character.Abilities;
using UnityEngine;

namespace ChromaSuit.Locomotion
{
    public class GrappleHook : Ability
    {
        public Transform Anchor;

        public float ArcForce = 2f;
        public float VelocityClampMin = 20f;
        public float VelocityClampMax = 30f;
        public float StretchWebFactor = 20f;
        public float SwingingMinAngle = -60f;
        public float SwingingMaxAngle = 60f;
        public float StretchWebAngle = 90f;
        public float BottomSpeed = 50f;

        private WebSwingingPendulum webSwingingPendulum;

        private bool m_Tethered = false;
        private float m_TetherLength;
        private Vector3 m_TetherPoint;

        protected override void AbilityStarted()
        {
            m_Tethered = true;
            m_TetherPoint = Anchor.position;
            m_TetherLength = Vector3.Distance(m_TetherPoint, m_Transform.position);

            webSwingingPendulum = new WebSwingingPendulum();
        }

        public override void Update()
        {
            base.Update();

            webSwingingPendulum.ArcForce = ArcForce;
            webSwingingPendulum.VelocityClampMin = VelocityClampMin;
            webSwingingPendulum.VelocityClampMax = VelocityClampMax;
            webSwingingPendulum.StretchWebFactor = StretchWebFactor;
            webSwingingPendulum.SwingingMinAngle = SwingingMinAngle;
            webSwingingPendulum.SwingingMaxAngle = SwingingMaxAngle;
            webSwingingPendulum.StretchWebAngle = StretchWebAngle;
            webSwingingPendulum.BottomSpeed = BottomSpeed;
            webSwingingPendulum.SetValues(m_Transform, m_CharacterLocomotion.Velocity);
            
            var force = webSwingingPendulum.CalculateWebSwinForce(Anchor.transform.position);
            AddForce(force, 0, true, true);
        }

        public override void LateUpdate()
        {
            Vector3 directionToGrapple = Vector3.Normalize(m_TetherPoint - m_Transform.position);
            float currentDistanceToGrapple = Vector3.Distance(m_TetherPoint, m_Transform.position);
            
            if (currentDistanceToGrapple > m_TetherLength)
            {
                m_Transform.position = m_TetherPoint - directionToGrapple * m_TetherLength;
            }
        }
    }
}

The output:


I would expect the character to have a similar behavior as the rigidbody.

Could you help me solve this problem?

Thank you!
 
UCC is a kinematic character controller, so you can't compare it to a non-kinematic rigidbody. You can use the Move with Object ability:
Set the target to a transform that moves with your swing.
 
Top