1
\$\begingroup\$

I have a player controller script that I'm writing that uses the CharacterController component to move around and handle collisions. I've been able to implement basically every feature that I want except for moving platforms.

Some things to note:

-For testing purposes, the platform moves via the Update() function and does not have a Rigidbody component attached to it. I don't think that rigidbodies would make a difference in the first place, but I could be wrong.

-Currently, when my player stands on the platform, it kind of does what it's supposed to do, but it moves with a large amount of jitter. In other words, the player isn't able to depenetrate itself as fast as the platform moves, therefore causing the jitter.

Here's what I've tried:

My initial thought was to create a script on the moving platform that keeps track of its own velocity. Then, on the player's OnControllerColliderHit(...) function, I would get the platform's velocity and add it to the player's velocity. This caused the same jittery effect as before.

My next idea was to parent the player to the platform whenever the player collided with it. This solution also did not work because apparently CharacterControllers do NOT like it when they are a child of a moving object. The platform's transformations had little to no effect on the player, regardless of its speed. So once again, the jitteriness continued.

Just to test out possible solutions, I also tried combining the two previous fixes and still, the jitteriness lives among us.

Closing thoughts:

I'm beginning to think that it's not entirely possible, but I'm still holding out hope. I would also prefer to not to purchase an existing asset or switch to a Rigidbody component because neither would fit the style of my game.

Feel free to ask questions for more details and thank you in advance!

\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

I am not very familiar with the built-in CharacterController, but I can tell you how I have implemented this for my own character controller (although, I should warn you it is almost definitely more convoluted than necessary).

I made this class

using UnityEngine;
public class CopyMovement : MonoBehaviour
{
   [SerializeField] public GameObject target;
    Transform target_transform;
    Transform common_ancestor;

    Quaternion previousAncestorRotation;
    Vector3 previousAncestorPosition;

    Matrix4x4 previousTargetWorldToLocal;
    Quaternion previousTargetRotation;

    void Start(){
        if(target!=null)
            Initialize();
    }

    public void Initialize(){
        target_transform = target.transform;
        common_ancestor = Utils.FindCommonAncestor(transform, target_transform);
        if(common_ancestor!=null){
            previousAncestorPosition= common_ancestor.position;
            previousAncestorRotation = common_ancestor.rotation;
        }

        previousTargetWorldToLocal = target_transform.worldToLocalMatrix;
        previousTargetRotation = target_transform.rotation;
    }

    void LateUpdate()
    {
        //if the objects do not share ancestors, it's fairly straightforward: calculate the position and rotation of the constrained object in the local space of the previous parent pose, and 
        //move it to the same pose in the _current_ local space

        //if they are part of the same hierarchy, however, the transformations of the common ancestors get applied twice to the constrained object (once naturally, once because of this script)
        //so, we just apply the inverse of the movement of the common ancestors to correct it. Simple, right?
        if(target!=null){

            Vector3 pointInPreviousLocalSpace = previousTargetWorldToLocal.MultiplyPoint(transform.position);
            Quaternion rotationInPreviousLocalSpace =  Quaternion.Inverse(previousTargetRotation) * transform.rotation;

            //set up the whole common-ancestor thing
            Quaternion offsetAncestorRotation = Quaternion.identity;
            Matrix4x4 ancestorMatrix = Matrix4x4.identity;
            if(common_ancestor!=null){
                offsetAncestorRotation = Quaternion.Inverse(previousAncestorRotation) * common_ancestor.rotation;
                ancestorMatrix = Matrix4x4.TRS(
                    common_ancestor.worldToLocalMatrix.MultiplyVector(previousAncestorPosition-common_ancestor.position), 
                    Quaternion.Inverse(offsetAncestorRotation),
                    Vector3.one
                );
            }

            //the thing happens!
            Matrix4x4 theMatrix;
            if(common_ancestor!=null)
                theMatrix = common_ancestor.localToWorldMatrix* ancestorMatrix * common_ancestor.worldToLocalMatrix * target_transform.localToWorldMatrix;
            else
                theMatrix = target_transform.localToWorldMatrix;
                
            transform.SetPositionAndRotation( theMatrix.MultiplyPoint(pointInPreviousLocalSpace),  
                Quaternion.Inverse(offsetAncestorRotation) * target_transform.rotation * rotationInPreviousLocalSpace);
            

            //store the current local space information for next time
            previousTargetWorldToLocal = target_transform.worldToLocalMatrix;
            previousTargetRotation = target_transform.rotation;

            if(common_ancestor!=null){
                previousAncestorRotation = common_ancestor.rotation;
                previousAncestorPosition = common_ancestor.position;
            }

        }
    }
}

And added a component of this type to the character. Then, inside of the character controller, when checking the ground that it is standing on, I change the settings of the CopyMovement. The abridged version of that code is something like this:

CopyMovement groundParentConstraint;

bool checkGround(){
    if(castRayDownwards()){
        if(groundParentConstraint.target != hit.collider.gameObject){
            groundParentConstraint.target = hit.collider.gameObject;
            groundParentConstraint.Initialize();
        }
        return true;
    }
    else{
        groundParentConstraint.target = null;
        return false;
    }
}

On top of this being a bit convoluted, it also does not work when the "parent" (the platform, in this case) has root motion animation, but do not ask me why, because I have no clue. But still, it's better than nothing.

Edit: The "find common ancestor" function:

public static Transform FindCommonAncestor(Transform tr1, Transform tr2){
    HashSet<Transform> ancestors1 = new HashSet<Transform>();
    Transform p = tr1;
    while(p!=null){
        ancestors1.Add(p);
        p=p.parent;
    }
    p=tr2;

    while(p!=null){
        if(ancestors1.Contains(p))
            return p;
        p=p.parent;
    }

    return null;
}
\$\endgroup\$
2
  • \$\begingroup\$ I'm a little bit confused. I don't work with matrices all that often so it's a little bit hard for me to read. Also, what are ancestors in this case? And how are you finding them in your Utils.FindCommonAncestor function? \$\endgroup\$ Commented Jun 20, 2022 at 21:27
  • \$\begingroup\$ Oh, yeah, sorry, that function is a custom thing. I added the code for it to the answer. What I mean by "common ancestor" is just a GameObject (or, Transform, I guess) that is in the hierarchy of both the constrained object (the player) and the target (the platform). So, for example, a shared parent. For this specific use case a common ancestor probably does not even exist, but the code was meant to be more generic and work in more cases. \$\endgroup\$
    – PepeOjeda
    Commented Jun 21, 2022 at 6:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .