Climbing an Obi Rope

I've been puzzling over this and I'm at the point of working on this ability with the new version of the controller.

I want my third person character to be able to climb on and interact with Obi Rope actors.

I have collisions and finding particle positions figured out for Obi, but I have the task of figuring out movement for my character when he climbs.

He can find the nearest particle, and he can start climbing but I'm curious what would be the suggested way to move between particles, especially given that they continue to move in reaction to the character's movement.

I considered using character motor, but I'm not sure it's ideal. Just curious to get any pointers.

See this video for a clearer explanation:


I am guessing I may need to make creative use of the drive ability to make the two way interaction work since UCC is a kinematic solution by default.
Would it end up being better to deactivate the Locomotion component and make a standard physics standin for the character appear and then replace it with the UCC character on dismount?
 
Last edited:
This will be really neat once you get it working. I'm guessing that the rope is non-kinematic? If so you should be able to use a similar technique as the drive ability. You'll want to parent the character to the rope, and let the rope do the movement. In the drive ability this is done by setting the AbilityMotor within UpdatePosition.
 
The rope is non-kinematic, absolutely. The tricky bit I can't seem to wrap my head around is that the rope is made of particles controlled by a DLL and they aren't GameObjects so I can query for positions and velocities, but otherwise it's a different animal to work with. I will try parenting the object containing their script and see if I can figure out how to benefit from that.

So far, my code to update movement between particles looks like this as a starting point, but obviously doesn't take the rope's movement into account and miscalculates a lot of factors:


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

[DefaultStartType(AbilityStartType.ButtonDown)]
[DefaultStopType(AbilityStopType.ButtonToggle)]
[DefaultInputName("Action")]
[DefaultAbilityIndex(1337)]
[DefaultAllowPositionalInput(false)]
[DefaultAllowRotationalInput(true)]
[DefaultUseGravity(AbilityBoolOverride.False)]
[DefaultUseRootMotionPosition(AbilityBoolOverride.True)]


public class RopeClimb : DetectObjectAbilityBase
{
    [Tooltip("A reference to the Ultimate Character Controller character.")]
    [SerializeField] private GameObject m_Character;
    private RopeClimb climbAbility;
    private ObiActor ropeComp;


    //Climbing Variables
    private float m_ClimbingSpeed=0.1f;

    private int m_MountParticle = 0;
    private int m_CurrentParticle = 0;
    private int m_ActorParticles=0;
    private Vector3 footPos;
    private Vector3 handPos;


    /// <summary>
    /// RopeClimb ability can start when the character is inside a rope collider on an object with an obi rope actor component.
    /// </summary>

    protected override void AbilityStarted()
    {
        base.AbilityStarted();


        m_Character.transform.SetParent(m_DetectedObject.transform);

        ropeComp = m_DetectedObject.GetComponent<ObiActor>();

        ToggleObiColliders();

        int nearestParticle = FindClosestRopeParticle();
        m_CurrentParticle = nearestParticle;
   
        //ToDo: Mount the nearest particle with Move Towards

    }


    private void LoopThroughParticles()
    {
        foreach (int solverIndex in ropeComp.solverIndices)
        {
            Debug.Log(ropeComp.solver.positions[solverIndex]);
        }
    }

    /// <summary>
    /// Find the nearest particle in the interacted rope's particle index and return it.
    /// </summary>
    public int FindClosestRopeParticle()
    {
        //Get the full length of the rope.
        m_ActorParticles = ropeComp.solverIndices.Length;

        var closestIndexDistance = 100f;
        int closestIndex=0;

        //Find nearest particle to players hand
        foreach (int solverIndex in ropeComp.solverIndices)
        {
            //Increase actor particles by one.
            m_ActorParticles++;
            Vector3 pos = ropeComp.solver.positions[solverIndex];

            float distance = Vector3.Distance(handPos, pos);
            if (distance < closestIndexDistance)
            {
                closestIndexDistance = distance;
                closestIndex = solverIndex;
            }
        }
        return closestIndex;
    }

    //ToDo: Make a method that deactivates or activates all Obi Colliders.

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


        if (Input.GetButtonDown("Jump"))
        {
            var characterLocomotion = m_Character.GetComponent<UltimateCharacterLocomotion>();
            var jumpAbility = characterLocomotion.GetAbility<Jump>();
            // Tries to start the jump ability. There are many cases where the ability may not start,
            // such as if it doesn't have a high enough priority or if CanStartAbility returns false.
            characterLocomotion.TryStartAbility(jumpAbility);
        }
   
    }


    private void ToggleObiColliders()
    {
        //Toggled enabled state of ObiColliders
        ObiCollider[] colliders = m_Character.GetComponentsInChildren<ObiCollider>();
        foreach (ObiCollider c in colliders)
        {
            c.enabled = !c.enabled;
        }
    }



    protected override void AbilityStopped(bool force)
    {
        base.AbilityStopped(force);

        ToggleObiColliders();
        m_Character.transform.SetParent(null);
        Debug.Log("Rope Climb Ability Stopped");
    }

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

        //Get a reference to the character locomotion component
        var characterLocomotion = m_Character.GetComponent<UltimateCharacterLocomotion>();
        //Set an empty vector to move to
        Vector3 destPart = new Vector3(0, 0, 0);
        //Get a variable for current player position
        Vector3 currentPos = characterLocomotion.transform.position;

        var vInp = Input.GetAxis("Vertical");
        if (vInp != 0)
        {
            //if the vertical input is positive, set the destination position to the particle upwards
            if (vInp > 0.2f && m_CurrentParticle <= m_ActorParticles-1)
            { m_CurrentParticle++; }
            else if (vInp < 0.2f && m_CurrentParticle > 0)//Otherwise if it's downward set to the particle below on the rope.
            { m_CurrentParticle--; }
            destPart = ropeComp.solver.positions[m_CurrentParticle];
        }
        else
        {
            destPart = ropeComp.solver.positions[m_CurrentParticle];
        }
        //Calculate distance between current particle.
        var dist = Vector3.Distance(currentPos, destPart);

        //Make a vector between the current position and the particle being moved to.
        Vector3 Dir = (destPart - currentPos);


        //Move in the direction vector.
        m_CharacterLocomotion.MotorThrottle = Dir * Time.deltaTime; //Not sure exactly what kind of units/magnitude motor throttle is looking for just yet.
    }
}

Here is a video of current progress, and flaws (LOL):

Watching a few times, I think the problem with the drifting is due to the m_CurrentParticle changing and perhaps not referencing the particle I want. I will return to the Obi docs etc to check on that.


I'm posting the full code because I'm happy to share this ability with the community once it's done, though it definitely isn't working so far.
I'm also going to implement an ObiRope grappling hook ability I can share here.
 
Last edited:
I looked back through my personal threads you have answered in the past Justin, and found this code you sent to help me understand and I modified it to point the character to the current ladder particle (If I can get this working, then I am a huge lead towards finishing the ability):


C#:
var deltaPosition = destPart - m_Transform.position;
        var movement = deltaPosition.normalized * m_ClimbingSpeed;
        m_CharacterLocomotion.AbilityMotor = movement / (m_CharacterLocomotion.TimeScaleSquared * Time.timeScale * m_CharacterLocomotion.FixedDeltaTime);

The only issue is that m_CharacterLocomotion doesn't contain a definition for "FixedDeltaTime" so I'm not sure what to replace that with. I'm going to look around the UCC script to see if I can figure it out though.
 
@Justin I appreciate your response!

Here's how I used it:


Code:
    public override void UpdatePosition()
    {

        base.UpdatePosition();

        //Get a reference to the character locomotion component
        var characterLocomotion = m_Character.GetComponent<UltimateCharacterLocomotion>();
        //Set an empty vector to move to
        Vector3 destPart = new Vector3(0, 0, 0);
        //Get a variable for current player position
        Vector3 currentPos = m_Character.transform.position;

       //ToDo: Implement Opsive input here
        var vInp = Input.GetAxis("Vertical");
        if (vInp != 0)
        {
            //if the vertical input is positive, set the destination position to the particle upwards
            if (vInp > 0.2f && m_CurrentParticle <= m_ActorParticles-1)
            { m_CurrentParticle++; }
            else if (vInp < 0.2f && m_CurrentParticle > 0)//Otherwise if it's downward set to the particle below on the rope.
            { m_CurrentParticle--; }
            destPart = ropeComp.solver.positions[m_CurrentParticle];
        }
        else
        {
            destPart = ropeComp.solver.positions[m_CurrentParticle];
        }


        var deltaPosition = destPart - m_Transform.position;
        var movement = deltaPosition.normalized * m_ClimbingSpeed;
        m_CharacterLocomotion.AbilityMotor = movement / (m_CharacterLocomotion.TimeScaleSquared * Time.timeScale * Time.deltaTime);

    }

Did I use the right DeltaTime? m_CharacterLocomotion doesn't seem to contain a definition for it.

I tried it with this code and the result in this video followed.

 
Last edited:
Here's a small update on the motion and animation:


I'm unsure if I need to rework the animation to remove the abrupt dip in height when the right hand lets go and the left hand reaches up.
I will check the code once more, then post it for comments and suggestions. Looking at it, I think that using root motion as a multiplier for his movement is a good idea.

Update:
I got the animation as stationary from mixamo, and fixed a few small things. It's still very jittery, and you can see that I added an animation event so that the individual hands are targeting the rope now. anyway, have a look:


I want to make the movement more precise and cut down on the sliding effect as much as possible though,
Root motion would be ideal, I'm just not too sure how to convert the ability just yet.
 
Last edited:
Now for the current code: the heavy comments and regions are to help keep the code organized and readable while I write it.

C#:
using UnityEngine;
using Opsive.UltimateCharacterController.Character.Abilities;
using Obi;
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Utility;
using Opsive.Shared.Events;

[DefaultStartType(AbilityStartType.ButtonDown)]
[DefaultStopType(AbilityStopType.ButtonToggle)]
[DefaultInputName("Action")]
[DefaultAbilityIndex(1337)]
[DefaultAllowPositionalInput(false)]
[DefaultAllowRotationalInput(true)]
[DefaultUseGravity(AbilityBoolOverride.False)]
[DefaultUseRootMotionPosition(AbilityBoolOverride.True)]


public class RopeClimb : DetectObjectAbilityBase
{
    [Tooltip("A reference to the Ultimate Character Controller character.")]
    [SerializeField] private GameObject m_Character;
    private RopeClimb climbAbility;
    private ObiActor ropeComp;


    //Climbing Variables
    [SerializeField] private float m_ClimbingSpeed=.5f;

    private int m_MountParticle = 0;
    private int m_CurrentParticleOnRopeLength = 0;
    private int m_ActorParticles=0;
    //True means the right hand is pulling the rope primarily, false means the left hand is.
    private bool ClimbingWithRightHand=true;

    [SerializeField] private Transform handRPos;
    [SerializeField] private Transform handLPos;
    [SerializeField] private Transform footRPos;
    [SerializeField] private Transform footLPos;
    private Animator m_Animator;

    private float m_RopeProgress;

    #region Awake
    /// <summary>
    /// Initialization including animation event registering.
    /// </summary>
    public override void Awake()
    {
        base.Awake();
        EventHandler.RegisterEvent(m_GameObject, "OnAnimatorLeftHand", LeftHand);
        EventHandler.RegisterEvent(m_GameObject, "OnAnimatorRightHand", RightHand);
    }
    #endregion

    #region Ability Started
    /// <summary>
    /// RopeClimb ability can start when the character is inside a rope collider on an object with an obi rope actor component.
    /// </summary>

    protected override void AbilityStarted()
    {
        base.AbilityStarted();


        m_Character.transform.SetParent(m_DetectedObject.transform);

        ropeComp = m_DetectedObject.GetComponent<ObiActor>();

        int nearestParticle = FindClosestRopeParticle();
        m_CurrentParticleOnRopeLength = nearestParticle;

        m_Animator = m_Character.GetComponent<Animator>();

        //ToDo: Mount the nearest particle with Move Towards
        //Set the nearest particle as the ability start location

    }
    #endregion



    #region Animation Events

    /// <summary>
    /// These are called by animation events on the climbing ability when his corresponding hand grabs the rope.
    /// </summary>
    ///
    private void RightHand()
    {
        ClimbingWithRightHand = true;
        Debug.Log("using the right hand to climb.");
    }

    private void LeftHand()
    {

       ClimbingWithRightHand = false;
        Debug.Log("using the left hand to climb.");
    }
    #endregion


    #region Find Nearest Rope Particle
    /// <summary>
    /// Find the nearest particle in the interacted rope's particle index and return it.
    /// </summary>
    public int FindClosestRopeParticle()
    {
        //Get the full length of the rope.
        m_ActorParticles = ropeComp.solverIndices.Length;

        var closestIndexDistance = 100f;
        int closestIndex=0;
        if (handRPos == null || handLPos == null)
        {
            Debug.LogError("Hand positions haven't been assigned on the rope climb ability.");
            return closestIndex;
        }

        //Find nearest particle to players hand
        foreach (int solverIndex in ropeComp.solverIndices)
        {
            Vector3 pos = ropeComp.solver.positions[solverIndex];

            var handPos = ClimbingWithRightHand? handRPos.position : handLPos.position;

            float distance = Vector3.Distance(handPos, pos);
            if (distance < closestIndexDistance)
            {
                closestIndexDistance = distance;
                closestIndex = solverIndex;
            }
        }
        return closestIndex;
    }

    #endregion

    #region Ability Stopped
    /// <summary>
    /// Whenever the ability stops - error or on purpose - we will run this to reset the ability variables and stop the ability's motion updating.
    /// </summary>
    /// <param name="force"></param>
    protected override void AbilityStopped(bool force)
    {
        base.AbilityStopped(force);

        m_RopeProgress = 0;

        m_Animator.SetFloat("AbilityFloatData", 0f);
        m_Animator.SetBool("Moving", false); //Make sure the moving bool isn't set by this ability anymore.
        //Get a reference to the character locomotion component
        var characterLocomotion = m_Character.GetComponent<UltimateCharacterLocomotion>();
        characterLocomotion.UseGravity = true;
        characterLocomotion.AbilityMotor = Vector3.zero;

        m_Character.transform.SetParent(null);
        Debug.Log("Rope Climb Ability Stopped");
    }
    #endregion

    #region Climbing Logic
    /// <summary>
    /// This is the Rope Climbing Ability's Update Position logic, which basically decides how the character climbs.
    /// </summary>
    public override void UpdatePosition()
    {

        //Get a reference to the character locomotion component
        var characterLocomotion = m_Character.GetComponent<UltimateCharacterLocomotion>();

        if (characterLocomotion.GetAbility<RopeClimb>().IsActive)
        {

            base.UpdatePosition();
            //Set some empty vectors to allow for the movement to smooth between them.
            Vector3 upPart = new Vector3(0, 0, 0);
            Vector3 downPart = new Vector3(0, 0, 0);
          

            var vInp = Input.GetAxis("Vertical");
            if (vInp != 0)
            {
                //if the vertical input is more than 0 in either direction, set the perche position appropriately
                if (vInp > 0.2f || vInp < -0.2f)
                {
                    m_RopeProgress += vInp * m_ClimbingSpeed * Time.deltaTime;
                    //Running out of rope, but still climbing? Then we will go ahead and dismount.
                    if (m_CurrentParticleOnRopeLength == m_ActorParticles - 2 && vInp > 0.2f || m_CurrentParticleOnRopeLength == 0 && vInp < -0.2f)
                    {
                        //Dismounting code and check for top or bottom will go here.
                        StopAbility(true);
                        //We go ahead and return, so the code ends here instead of trying to continue controlling the movement - which would cause the character an endless flight.
                        return;
                    }
                }

                
                m_Animator.SetFloat("AbilityFloatData", 1f);
                m_Animator.SetBool("Moving", vInp > 0 ? true : false); //Is the character ascending or descending?
               // Debug.Log("reaching for particle number:" + m_CurrentParticleOnRopeLength);
               // Debug.Log(m_RopeProgress);
            }
            else
            {
                m_Animator.SetFloat("AbilityFloatData", 0f);
            }

            m_CurrentParticleOnRopeLength = (int)m_RopeProgress / m_ActorParticles;
            upPart = ropeComp.solver.positions[m_CurrentParticleOnRopeLength + 1];
            downPart = ropeComp.solver.positions[m_CurrentParticleOnRopeLength];

          
            var handPos = ClimbingWithRightHand ? handRPos.position : handLPos.position;


            //Making sure the handplacement is relatively in line with the rope. This code will have to change once the rope can move and interact with the character again.
            var xTarget = Mathf.Lerp(downPart.x, upPart.x, .5f);
            var zTarget = Mathf.Lerp(downPart.z, upPart.z, .5f);
            //Getting an alterable copy of the handPos.
            var inlineHandPos = handPos;
            //Altering it's x and z values. This is not totally bugless so I commented it out for now until I solve the bugs.
            /*inlineHandPos.x = xTarget;
            inlineHandPos.z = zTarget;*/
            //Getting the handplacement between two particles
            float handPlacement = Vector3.Distance(inlineHandPos, downPart) / Vector3.Distance(downPart, upPart);
            //Turning that into a Vector3
            Vector3 handPlacementVector = Vector3.Lerp(upPart, downPart, handPlacement) - inlineHandPos;

            /*This is for debugging the hand switching behavior.
             *
             var handName = ClimbingWithRightHand ? "right" : "left";
             Debug.Log("Climbing with "+ handName + " hand.");*/

            //Moving the handPos to the placement vector effectively.
            var deltaPosition = handPlacementVector;
            //Driving that movement into AbilityMotor
            m_CharacterLocomotion.AbilityMotor = deltaPosition / (m_CharacterLocomotion.TimeScaleSquared * Time.timeScale * TimeUtility.FramerateDeltaTime);
        }
    }
    #endregion

}
 
Progress!

Just glancing through your ability instead of setting the AnimatorFloatData manually you should use SetAbilityFloatDataParameter within the ability. This will allow the AnimatorMonitor to correctly track the float.
 
Sounds great. :D Now, I am just curious if I can switch it over to root motion, because I think if will be much more responsive. But perhaps, just using handplacement to drive the motion more carefully would be the ticket wince I have a stationary animation.

Do you have any advice on this? I want to make sure it's very reactive to the animation, and the other element is going to be the swinging and two way physics.
 
Last edited:
Top