Skip to main content
added 493 characters in body
Source Link
PepeOjeda
  • 866
  • 2
  • 7

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;
}

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;
}
Source Link
PepeOjeda
  • 866
  • 2
  • 7

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.