AlignToGround and AlignToGravity questions

macusernick

New member
UPDATE: Please see my last two posts for a working solution!


I've been messing with the AlignToGround and AlignToGravity and noticed that when I make calls to start each respective ability, that they both indicate as starting the AlignToGround ability in the inspector. Also, AlignToGravity is not actually available in the list in the inspector.

I'm trying to start these abilities through code (UCL is a reference to UltimateCharacterLocomotion):

Code:
UCL.TryStartAbility(UCL.GetAbility<AlignToGround>());
or
Code:
UCL.TryStartAbility(UCL.GetAbility<AlignToGravity>());

I've also noticed that if you fall off an object with the AlignToGround and there is no object below the ground aligned gravity (i.e. when you are upside down), that you will keep falling forever. This makes sense and is the reason I'm trying to get AlignToGravity working correctly. Basically, I'm checking anytime the player Falls to see what is below the Player... if it is nothing (I check 4m) then I attempt to AlignToGravity to flip the player the correct way and fall to the real ground. What's odder is it seems like the AlignToGravity works (even though the AlignToGround says it's active), but it only works when the character is completely up-side down. If the player is only at a 90 degree angle, the system freaks out. Any thoughts? I've put some screenshots below to help illustrate my question/issue.

1617067409845.png
1617067459575.png
1617067686721.png
 
Last edited:
Align to Ground and Align to Gravity both inherit the same abstract class so for this you will need to check each ability individually. You can subscribe to the OnCharacterAbilityActive event to determine when your own logic should start. You could also subclass each ability to add in your own logic.
 
Align to Ground and Align to Gravity both inherit the same abstract class so for this you will need to check each ability individually. You can subscribe to the OnCharacterAbilityActive event to determine when your own logic should start. You could also subclass each ability to add in your own logic.

Thanks for the quick reply and moving this to the correct forum!! I guess my issue/question is, why there aren't 2 abilities in the list... one for AlignToGround and one for AlignToGravity... whenever I activate either of them, it shows that AlignToGround was activated (in the ability list). I do subscribe to the OnCharacterAbilityActive to check the Fall status (that fires my AlignToGravity). Do I just need to subclass the AlignToGravity to be able to see it as it's own ability in the list?

Also, as I dig a bit more into the code... I wonder if the alignment is only in the Y axis and if that isn't considered Up (or Down) then the camera freaks out? I've attached a video below to show what I'm wondering. Thanks for all the help as always!

 
Unless I've missed something, AlignToGround is actually a subclass of AlignToGravity. There's only one ability here - in your case, AlignToGround, which just extends AlignToGravity.

I believe there should be some debug lines drawn in the scene view when AlignToGround is active (as shown in the docs), are you seeing these as expected?
 
Unless I've missed something, AlignToGround is actually a subclass of AlignToGravity. There's only one ability here - in your case, AlignToGround, which just extends AlignToGravity.

I believe there should be some debug lines drawn in the scene view when AlignToGround is active (as shown in the docs), are you seeing these as expected?

I guess I was confused by the sub-class thing then. I figured that I could control AlignToGravity like it was an independent Ability and not just part of AlignToGround. I see I will need to extend AlignToGravity to get a second Ability that I can control. (Which is what Justin said originally oops)

I have Gizmos turned on, but don't see any Rays (I also didn't see an option to turn them on from the Ability either):
1617192694417.png

But the AlignToGround is working since I can run around on any surface with no issue. The real problem is when I "de-mount" or switch from AlignToGround and try to AlignToGravity (as illustrated in the video in my prior post). I find it interesting that it just works when you are upside down (Up is in the -Y direction I think), but screws up when Up is not in the Y direction (i.e. sideways). Is it possible the camera/player rotation only takes into account that direction? I'm not sure if that helps describe/clarify my issue better, my 3d math is not very good.

Thanks for the help and bearing with me learning your systems! :)
 
AlignToGround uses the direction of the ground the character is currently on, so it isn't restricted to any particular axes, and when no ground is found within the specified Distance parameter it resets the gravity direction to the Stop Gravity Direction parameter. Have you tried playing with the Distance and Rotation Speed parameters? I wonder if the ability is finding the ground and re-aligning to it whilst in the middle of rotating. I'm not too sure but maybe the ability needs to be stopped after the player falls off in order to prevent it trying to re-align whilst in midair.
 
AlignToGround uses the direction of the ground the character is currently on, so it isn't restricted to any particular axes, and when no ground is found within the specified Distance parameter it resets the gravity direction to the Stop Gravity Direction parameter.
See... I thought it should be able to do that, but I cannot get the ability to work like that. Anytime I "fall" off the track (i.e. there is no ground found below the character) it just keeps falling in that direction. Here is my ability setup:
1617304873092.png


Have you tried playing with the Distance and Rotation Speed parameters? I wonder if the ability is finding the ground and re-aligning to it whilst in the middle of rotating.
That would definitely explain it, if it's Raycasting and the player is flipped back towards the track, it might "see" it again and try to re-align. I had hoped my small delay in the "flip" mechanic would keep the system from freaking out, but no luck.


I'm not too sure but maybe the ability needs to be stopped after the player falls off in order to prevent it trying to re-align whilst in midair.
This is what I was attempting to do originally... Turn off AlignToGround (after that small delay) and then turn on AlignToGravity with the gravity direction normal again.


I feel like we're so close here.... Thanks for the continued help!
 
Ah so you just want the gravity direction to reset to down when they fall off whatever surface they're on? In that case I think you'll need to manually stop the ability when no ground is found. In the demo scene, the gravity zone area manually starts/stops the character's AlignToGravityZone ability when entering/exiting the zone. So you may need to subclass AlignToGround (or edit it directly) to have it stop itself when no ground is found within the max distance (or you could probably just do this in a simple script of your own if you want to keep things separate).
 
Ah so you just want the gravity direction to reset to down when they fall off whatever surface they're on? In that case I think you'll need to manually stop the ability when no ground is found. In the demo scene, the gravity zone area manually starts/stops the character's AlignToGravityZone ability when entering/exiting the zone. So you may need to subclass AlignToGround (or edit it directly) to have it stop itself when no ground is found within the max distance (or you could probably just do this in a simple script of your own if you want to keep things separate).

Thanks for the suggestions! I have edited the AlignToGround file and updated this block of code in UpdateRotation(). I noticed when there was no delay, I'd tend to flip over onto the other side of objects.

Code:
if (Physics.Raycast(m_Transform.TransformPoint(0, m_CharacterLocomotion.Radius, m_DepthOffset * -Mathf.Sign(m_CharacterLocomotion.InputVector.y)),
                        -m_CharacterLocomotion.Up, out raycastHit, m_Distance + m_CharacterLocomotion.Radius, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore)) {
    if (frontHit) {
        if (m_NormalizeDirection) {
            var backPoint = raycastHit.point;
            var direction = (frontPoint - backPoint).normalized;
            targetNormal = Vector3.Cross(direction, Vector3.Cross(m_CharacterLocomotion.Up, direction)).normalized;
        } else {
            targetNormal = (targetNormal + raycastHit.normal).normalized;
        }
    } else {
        targetNormal = raycastHit.normal;
    }

    if(m_FallDelay > 0) {
        if(m_AlignEvent != null && m_AlignEvent.Active) {
            Debug.Log("AlignEvent is not null and active, cancel it.");
            Scheduler.Cancel(m_AlignEvent);
        }
    }
} else {
    if(m_FallDelay > 0) {
        if(m_AlignEvent == null) {
            Debug.Log("AlignEvent is null, start a new one.");
            m_AlignEvent = Scheduler.Schedule(m_FallDelay, DoStopAlign);
        } else if(m_AlignEvent.Active == false) {
            Debug.Log("AlignEvent is not active, start a new one.");
            m_AlignEvent = Scheduler.Schedule(m_FallDelay, DoStopAlign);
        } else {
            //Debug.Log("AlignEvent already active, don't start again.");
        }
    } else {
        Debug.Log("No fall delay...");
        DoStopAlign();
    }
}

So, I have also added this:

Code:
[Tooltip("How long should the fall occur before flipping gravity back to normal?")]
[SerializeField] protected float m_FallDelay = 0.5f;
        
private ScheduledEventBase m_AlignEvent;


private void DoStopAlign() {
    Debug.Log("DoStopAlign()");
    WillTryStopAbility();
    StopAbility();
}

This code works for flipping the Character over when it is upside-down, but when the Character lands on the ground, the Camera doesn't align quite right. I have to Toggle the AlignToGround to snap the Camera correctly back into place. What would be the correct way to Stop the Ability and have the Camera correctly snap to position?

Thanks yet again!
 
So it looks like you're manually stopping the ability when no ground is detected, after a predefined amount of time? AlignToGravity has the ResetAlignToGravity method, which waits 1 full frame to allow the camera to reset, so you may want to take a look at that. Although as far as I know there's nothing specific about AlignToGravity or AlignToGround that affects the camera itself so not exactly sure if this would be needed - is the character's rotation definitely correct when they land?
 
So it looks like you're manually stopping the ability when no ground is detected, after a predefined amount of time? AlignToGravity has the ResetAlignToGravity method, which waits 1 full frame to allow the camera to reset, so you may want to take a look at that. Although as far as I know there's nothing specific about AlignToGravity or AlignToGround that affects the camera itself so not exactly sure if this would be needed - is the character's rotation definitely correct when they land?
Yep I'm manually stopping it after a small delay... doing the stop instantly would not allow the Character to jump over gaps. I see the ResetAlignToGravity is called in the AbilityStopped for AlignToGround already.

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

    ResetAlignToGravity();
}

I just checked and the Character's rotation is NOT correct when they land... it is off by small amount. I thought it was the Camera that was off, but it makes more sense being the Character itself.
 
In that case you may need to add some logic that waits for the character's rotation to be what you expect (e.g. the character's transform.up should be equal to Vector3.up when standing on the regular ground), rather than waiting an arbitrary amount of time like 0.5 secs. Either that or manually set the character's rotation yourself (even lerping if it needs to be a smooth transition).
 
Either that or manually set the character's rotation yourself (even lerping if it needs to be a smooth transition).

Thank you!!! That was the breakthrough I needed! I now do the rotation from inside the Ability itself and then attached to OnLandEvent I stop the Ability. Works like a charm no matter the angle now! I've included the modified code below if anyone else might like to use it!

Code:
/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------

namespace Opsive.UltimateCharacterController.Character.Abilities
{
    using Opsive.UltimateCharacterController.Utility;
    using System.Collections.Generic;
    using UnityEngine;

    /// <summary>
    /// The AlignToGround ability will orient the character to the direction of the ground's normal.
    /// </summary>
    public class AlignToGroundExt : AlignToGravity
    {
        [Tooltip("The distance from the ground that the character should align itself to.")]
        [SerializeField] protected float m_Distance = 4;
        [Tooltip("The depth offset when checking the ground normal.")]
        [SerializeField] protected float m_DepthOffset = 0.05f;
        [Tooltip("Should the direction from the align to ground depth offset be normalized? This is useful for generic characters whose length is long.")]
        [SerializeField] protected bool m_NormalizeDirection = false;
        [Tooltip("How long should the fall occur before flipping gravity back to normal?")]
        [SerializeField] protected float m_GravityDelay = 0.75f;

        public float Distance { get { return m_Distance; } set { m_Distance = value; } }
        public float DepthOffset { get { return m_DepthOffset; } set { m_DepthOffset = value; } }
        public bool NormalizeDirection { get { return m_NormalizeDirection; } set { m_NormalizeDirection = value; } }
        public float GravityDelay { get { return m_GravityDelay; } set { m_GravityDelay = value; } }

        private RaycastHit[] m_CombinedRaycastHits;
        private Dictionary<RaycastHit, int> m_ColliderIndexMap;
        private UnityEngineUtility.RaycastHitComparer m_RaycastHitComparer = new UnityEngineUtility.RaycastHitComparer();
        private float m_TimeSinceJump = 0f;

        /// <summary>
        /// Update the rotation forces.
        /// </summary>
        public override void UpdateRotation()
        {
            var updateNormalRotation = m_Stopping;
            var targetNormal = m_Stopping ? (m_StopGravityDirection.sqrMagnitude >  0 ? -m_StopGravityDirection : -m_CharacterLocomotion.GravityDirection) : m_CharacterLocomotion.Up;
            if (!m_Stopping) {
                // If the depth offset isn't zero then use two raycasts to determine the ground normal. This will allow a long character (such as a horse) to correctly
                // adjust to a slope.
                if (m_DepthOffset != 0) {
                    var frontPoint = m_Transform.position;
                    bool frontHit;
                    RaycastHit raycastHit;
                    if ((frontHit = Physics.Raycast(m_Transform.TransformPoint(0, m_CharacterLocomotion.Radius, m_DepthOffset * Mathf.Sign(m_CharacterLocomotion.InputVector.y)),
                                                    -m_CharacterLocomotion.Up, out raycastHit, m_Distance + m_CharacterLocomotion.Radius, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore))) {
                        frontPoint = raycastHit.point;
                        targetNormal = raycastHit.normal;
                    }

                    if (Physics.Raycast(m_Transform.TransformPoint(0, m_CharacterLocomotion.Radius, m_DepthOffset * -Mathf.Sign(m_CharacterLocomotion.InputVector.y)),
                                            -m_CharacterLocomotion.Up, out raycastHit, m_Distance + m_CharacterLocomotion.Radius, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore)) {
                        if (frontHit) {
                            if (m_NormalizeDirection) {
                                var backPoint = raycastHit.point;
                                var direction = (frontPoint - backPoint).normalized;
                                targetNormal = Vector3.Cross(direction, Vector3.Cross(m_CharacterLocomotion.Up, direction)).normalized;
                            } else {
                                targetNormal = (targetNormal + raycastHit.normal).normalized;
                            }
                        } else {
                            targetNormal = raycastHit.normal;
                        }

                        if(m_TimeSinceJump < m_GravityDelay) {
                            m_TimeSinceJump = 0f;
                        }
                    } else {
                        m_TimeSinceJump += Time.deltaTime;
                    }

                    updateNormalRotation = true;

                    if(m_TimeSinceJump < m_GravityDelay) {
                        m_CharacterLocomotion.GravityDirection = -targetNormal;
                    } else {
                        targetNormal = -m_StopGravityDirection.normalized;
                        m_CharacterLocomotion.GravityDirection = -targetNormal;
                    }
                } else {
                    var hitCount = m_CharacterLocomotion.Cast(-m_CharacterLocomotion.Up * m_Distance,
                        m_CharacterLocomotion.PlatformMovement + m_CharacterLocomotion.Up * m_CharacterLocomotion.ColliderSpacing, ref m_CombinedRaycastHits, ref m_ColliderIndexMap);

                    // The character hit the ground if any hit points are below the collider.
                    for (int i = 0; i < hitCount; ++i) {
                        var closestRaycastHit = QuickSelect.SmallestK(m_CombinedRaycastHits, hitCount, i, m_RaycastHitComparer);

                        // The hit point has to be under the collider for the character to align to it.
                        var activeCollider = m_CharacterLocomotion.ColliderCount > 1 ? m_CharacterLocomotion.Colliders[m_ColliderIndexMap[closestRaycastHit]] : m_CharacterLocomotion.Colliders[0];
                        if (!MathUtility.IsUnderCollider(m_Transform, activeCollider, closestRaycastHit.point)) {
                            continue;
                        }

                        targetNormal = m_CharacterLocomotion.GravityDirection = -closestRaycastHit.normal;
                        updateNormalRotation = true;
                        break;
                    }
                }
            }

            // The rotation is affected by aligning to the ground or having a different up rotation from gravity.
            if (updateNormalRotation) {
                Rotate(targetNormal);
            }
        }

        /// <summary>
        /// The ability has started.
        /// </summary>
        protected override void AbilityStarted()
        {
            base.AbilityStarted();
            m_TimeSinceJump = 0f;
        }

        /// <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);
            ResetAlignToGravity();
        }
    }
}

The actual change is very minimal and is a lot more robust then using the Scheduler (I feel). I use the settings below:

1618403942837.png

Then I added a function to OnLandEvent to Stop the Ability:

1618403991252.png
 
Last edited:
Had to split into two posts... Here is my Player class (I based it on @Haytam95 version and it's just a Singleton reference for some UCC functions):

Code:
using UnityEngine;
using Opsive.UltimateCharacterController.Character;
using Opsive.UltimateCharacterController.Character.Abilities;
using Opsive.UltimateCharacterController.Camera;
using Opsive.Shared.Input;
using Opsive.Shared.Events;
using Opsive.Shared.Integrations.InputSystem;

public class Player : MonoBehaviour
{
    private static Player _instance;

    public enum Abilities {
        AlignToGroundExt
    }

    [SerializeField] protected string m_GravityTag = "Gravity";

    private UltimateCharacterLocomotion _ucl;
    private CameraController _cc;

    private void Awake() {
        if(_instance == null) {
            _instance = this;
        }
    }

    public static Player Get { get => _instance; }

    public UltimateCharacterLocomotion UCL {
        get {
            if(_ucl == null) {
                _ucl = CC.Character.GetComponent<UltimateCharacterLocomotion>();
            }
            return _ucl;
        }
    }

    public CameraController CC {
        get {
            if(_cc == null) {
                _cc = FindObjectOfType<CameraController>();
            }
            return _cc;
        }
    }

    public void StopAlignToGround() {
        RaycastHit hit;
        if(Physics.Raycast(UCL.transform.position, -UCL.transform.up, out hit, 2f)) {
            if(hit.collider.tag != m_GravityTag) {
                TryStopAbility(Abilities.AlignToGroundExt);
            }
        }
    }

    public bool TryStartAbility(Abilities ability) {
        switch(ability) {
            case Abilities.AlignToGroundExt:
                return UCL.TryStartAbility(UCL.GetAbility<AlignToGroundExt>());
            default:
                return false;
        }
    }

    public bool TryStopAbility(Abilities ability) {
        switch(ability) {           
            case Abilities.AlignToGroundExt:
                return UCL.TryStopAbility(UCL.GetAbility<AlignToGroundExt>());
            default:
                return false;
        }
    }
}

Last but not least... add a new Tag (I called my Gravity) and assign it to any objects you want to be able to AlignToGround with. That way when you Fall and Land on an object, the system will know if it should stop the ability. This lets you fall off and land on objects besides the ground (like cars, blocks, etc.) but continue aligning to the correct gravity and not that object. Hopefully this makes sense and helps someone out!!
 
Top