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.