Mantle/Climb ability

I have a method that allows a character to pull him/her-self up onto objects up to about 1/2 a metre above his/her head. It lacks an animation (but this could be added), but it works pretty damned well. The character needs to get right up to the object, face it, press Climb, and he/she then moves up the object into a standing position on its uppermost surface. The obstacle can be climbed from any side.

It requires a new input 'Climb' and a new layer 'Climbable' which the climbable objects should be placed in. It also requires, of course, a new ability script, which I have. I need, however, to check that it's OK with Opsive to share said script, since it's basically a modified version of the Climb from Water script in the Swimming pack (and that's not my intellectual property).

If I get the green light to do so, I'll share the entire script and detail the method. If not, I guess I could outline the changes that need to be made to a copy of the original script?

Anasta.
 
Code:
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
///
namespace Opsive.UltimateCharacterController.AddOns.Agility
{
    using Opsive.Shared.Events;
    using Opsive.UltimateCharacterController.Character.Abilities;
    using UnityEngine;

    [DefaultAbilityIndex(303)]
    [DefaultStartType(AbilityStartType.ButtonDown)]
    [DefaultInputName("Climb")]
    [DefaultAllowRotationalInput(false)]
    [DefaultAllowPositionalInput(false)]
    [DefaultUseRootMotionPosition(AbilityBoolOverride.True)]
    [DefaultDetectHorizontalCollisions(AbilityBoolOverride.False)]
    [DefaultDetectVerticalCollisions(AbilityBoolOverride.False)]
    [DefaultUseGravity(AbilityBoolOverride.False)]
    [DefaultUseLookDirection(false)]
    [DefaultCastOffset(0, 0, 0)]
    [DefaultEquippedSlots(0)]
    public class Mantle : DetectObjectAbilityBase
    {
        [Tooltip("The offset that the character should start climbing from.")]
        [SerializeField] protected Vector3 m_ClimbOffset = new Vector3(0, -1.6f, -0.25f);
        [Tooltip("The speed that the character should move towards the target when getting into position.")]
        [SerializeField] protected float m_MoveToPositionSpeed = 0.05f;

        public Vector3 ClimbOffset { get { return m_ClimbOffset; } set { m_ClimbOffset = value; } }
        public float MoveToPositionSpeed { get { return m_MoveToPositionSpeed; } set { m_MoveToPositionSpeed = value; } }

        private Vector3 m_DetectedObjectNormal;
        private Vector3 m_TopClimbPosition;
        private bool m_InPosition;
        private bool m_Moving;

        public override int AbilityIntData { get { return m_Moving ? 1 : 0; } }

        /// <summary>
        /// Initialize the default values.
        /// </summary>
        public override void Awake()
        {
            base.Awake();
            EventHandler.RegisterEvent(m_GameObject, "OnAnimatorClimbComplete", OnClimbComplete);
        }

        /// <summary>
        /// Can the ability be started?
        /// </summary>
        /// <returns>True if the ability can be started.</returns>
        public override bool CanStartAbility()
        {

            // An attribute may prevent the ability from starting.
            if (!base.CanStartAbility()) {
                return false;
            }

            return true;
        }

        /// <summary>
        /// The ability has started.
        /// </summary>
        protected override void AbilityStarted()
        {
            base.AbilityStarted();

            m_CharacterLocomotion.SingleCast(m_Transform.forward, Vector3.zero, m_CharacterLayerManager.SolidObjectLayers, ref m_RaycastResult);
            m_DetectedObjectNormal = Vector3.ProjectOnPlane(m_RaycastResult.normal, m_CharacterLocomotion.Up).normalized;

            // The character should be positioned relative to the top of the hit object.
            var closestPoint = m_RaycastResult.collider.ClosestPointOnBounds(m_Transform.position);
            var localClosestPoint = m_RaycastResult.transform.InverseTransformPoint(closestPoint);
            var localMaxBounds = m_RaycastResult.transform.InverseTransformPoint(m_RaycastResult.collider.bounds.max);
            localClosestPoint.y = localMaxBounds.y;
            m_TopClimbPosition = m_RaycastResult.transform.TransformPoint(localClosestPoint);

            m_InPosition = false;
            m_Moving = m_CharacterLocomotion.Moving;
        }

        /// <summary>
        /// Called when another ability is attempting to start and the current ability is active.
        /// Returns true or false depending on if the new ability should be blocked from starting.
        /// </summary>
        /// <param name="startingAbility">The ability that is starting.</param>
        /// <returns>True if the ability should be blocked.</returns>
        public override bool ShouldBlockAbilityStart(Ability startingAbility)
        {
            return startingAbility is Sneak;
        }

        /// <summary>
        /// Called when the current ability is attempting to start and another ability is active.
        /// Returns true or false depending on if the active ability should be stopped.
        /// </summary>
        /// <param name="activeAbility">The ability that is currently active.</param>
        /// <returns>True if the ability should be stopped.</returns>
        public override bool ShouldStopActiveAbility(Ability activeAbility)
        {
            return activeAbility is Sneak;
        }

        /// <summary>
        /// Update the controller's rotation values.
        /// </summary>
        public override void UpdateRotation()
        {
            // Keep the character facing the object that they are climbing out on.
            var localLookDirection = m_Transform.InverseTransformDirection(-m_DetectedObjectNormal);
            localLookDirection.y = 0;
            var deltaRotation = m_CharacterLocomotion.DeltaRotation;
            deltaRotation.y = Utility.MathUtility.ClampInnerAngle(Quaternion.LookRotation(localLookDirection.normalized, m_CharacterLocomotion.Up).eulerAngles.y);
            m_CharacterLocomotion.DeltaRotation = deltaRotation;
        }

        /// <summary>
        /// Update the controller's position values.
        /// </summary>
        public override void UpdatePosition()
        {
            if (m_InPosition) {
                return;
            }

            // Move the character into the starting position. The animation will put up as soon as the character is in position.
            var direction = Utility.MathUtility.TransformPoint(m_TopClimbPosition, m_Transform.rotation, m_ClimbOffset) - m_Transform.position;
            m_CharacterLocomotion.AbilityMotor = Vector3.MoveTowards(Vector3.zero, direction, m_MoveToPositionSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale);

            if (direction.magnitude < 0.01f) {
                m_CharacterLocomotion.AbilityMotor = Vector3.zero;
                m_InPosition = true;
                m_CharacterLocomotion.UpdateAbilityAnimatorParameters();
            }
        }

        /// <summary>
        /// The climb animation has completed.
        /// </summary>
        private void OnClimbComplete()
        {
            StopAbility();
        }

        /// <summary>
        /// The ability has stopped running.
        /// </summary>
        /// <param name="force">Was the ability force stopped?</param>
        protected override void AbilityStopped(bool force)
        {
            base.AbilityStopped(force);

            m_CharacterLocomotion.AbilityMotor = Vector3.zero;
        }

        /// <summary>
        /// The character has been destroyed.
        /// </summary>
        public override void OnDestroy()
        {
            base.OnDestroy();

            EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorClimbComplete", OnClimbComplete);
        }
    }
}

To use it, activate the ability in the normal way. Make a cube about 2 or 3 metres tall and place it in a 'Climbable' layer. Add a 'Climb' input. The inspector settings I use are:

mantle.png

The thing that decides how high you can mantle is the Move To Position Speed Value. Set this too high, and you've got a jet-pack!

All you need to do in game is approach the obstacle, face it, press 'Climb' (and move forward a little). Tada! You scale the obstacle and end up standing on the top.

BTW - You will need to change the Ability Index Parameter in the code and inspector if you wish to use this in conjunction with the Climb From Water ability.

Anasta
 
Whenever i use this, i climb but then just kidna get stuck at the top stuck and need to jump to sometimes get my control back. where in the list did you place this? this does seem super neat btw, i was doing the same thing by adapting Hang but adapting getouttawater might be closer to the sort of behavior i was after.
 
Hi Leth,

I had a few issues like that. After playing around with the settings I managed to smooth things out a lot. Here's my setup now:

order.png
settings.png

As you can see, I now use the Vertical input instead of a dedicated Climb button. It feels smoother and more natural to climb up things just by walking into them, and the Angle Threshold means that the ability does not start unless the character is really facing the obstacle (instead of just being near it). The Climb Offset moves the character back a bit before the climb, which smooths out the motion and prevents him from getting stuck at the top. I've gone for a lower Move To Position Speed now, as the character was making some impressive, but implausible climbs with 0.15. Obviously, your own character will be slightly different from mine (taller/shorter, longer/shorter limbs) - not sure how much that affects things.

(You can ignore the Attribute Name bit. I'm working on a stealth game where movement increases the character's visibility, along with light and shadows).

I considered Hang too as a starting point for mantling, but I think I will repurpose that into a ladder-climb ability. If I disallow shimmying, it should allow characters to move vertically up/down in steps and, as a bonus, approach from the correct angle when going down. Watch this space.

Hope this helps!
Anasta
 
Yeah, i experimented with alot of ways to use this earlier today. i esp like the idea mixing it with hang, and using some states/extra code to trigger some "arm scrambling" anim to help scramble up a tall wall then use hang to crawl over.

Though the problem i have it moreso where this is should be placed on the ability list. if it's below jump but above fall, it works fine and pulls you right up but never exits. if it's below fall and jump then it launches you up but only lasts for like a couple frames and you more lurch and slam into the wall than anything. and above both of them freezes as well, but then i can't jump out to cancel the ability.


EDIT: Infact here's a vid, in the first half i have it below fall and jump, and it starts for a moment then seems to disable once fall takes over or somehting. makes it kinda unusable and i Presume this isn't right. In the second go i put it between fall and jump, and then it launches up in the way i presume it's supposed to work, but it locks me at the top till i cancel it out by jumping. and if orgot to add it but if i put it above both of them, the same thing happens but i can't jump out.

 
Last edited:
Strange. I have it between Fall and Jump, as you can see. I've checked if there are any mantle presets in my Fall and Jump abilities - no. So I'm not sure why it doesn't work as well for you. I'll try to put a vid of my build in for you later.
 
... fuck i somehow didn't see the huge screenshot of your ability list. aaaaaa. my bad about that.

i mean it's super possible i got some other stuff interfeering on a testmodel and might need to deel with some other stuff, i'll need to keep messing with it later.
 
Thank you for sharing this. This is great and going to save me a lot of time.

I do have one question/issue. The ability works for me when I have start type "button down" and stop stype "button up" so the player has to hold the climb button. This is honestly mostly ok for the project I am making.

When I have stop type "manual" the ability never stops being active, even after the player reaches the top. Any idea why this might be happening?
 
Top