CharacterController falls through or slips off moving platforms

I made an animated elevator/platform by creating an animation in Maya and then attached a box collider as a child of the animation so that they move together. However my player uses the CharacterController and it falls thru the box collider (floor) as soon as the elevator starts animating*.

There are a couple of threads on the unity forum (here and here), but one of them is very old and neither of them has current or clear explanations of why the CharacterController has issues with attaching to moving objects and how best to solve the problem.

*note if I keep the player moving while the elevator is in motion, he does not fall thru the floor - he only falls thru when standing still. Also he's more likely to fall thru while the elevator is moving up than down.

The 2D Gameplay Tutorial has a character that can stay perfectly on platforms. The needed code, derived from that tutorial, is below.

This solution is rather flexible.

  • It supports moving in any direction (vertically, horizontally, diagonally, you name it).

  • It works no matter if the platforms are moving through scripting, or through animation. If done by animation, the Animation component of the platform must have the checkbox "Animate Physics" turned on to avoid glitches.

  • I have enhanced it here so that it also supports rotating platforms, but you can leave out that part if you don't need or want it.

  • It does not rely on making the platform a parent to the character, as that can have certain disadvantages.

Note that the below is NOT a complete script. It's only some code fragments needed for making the character stay on platforms. For a complete script, see the 2D Gameplay Tutorial.

// Moving platform support
private var activePlatform : Transform;
private var activeLocalPlatformPoint : Vector3;
private var activeGlobalPlatformPoint : Vector3;
private var lastPlatformVelocity : Vector3;

// If you want to support moving platform rotation as well:
private var activeLocalPlatformRotation : Quaternion;
private var activeGlobalPlatformRotation : Quaternion;

function Update () {

    // Perform gravity and jumping calculations here
    ...

    // Moving platform support
    if (activePlatform != null) {
        var newGlobalPlatformPoint = activePlatform.TransformPoint(activeLocalPlatformPoint);
        var moveDistance = (newGlobalPlatformPoint - activeGlobalPlatformPoint);
        if (moveDistance != Vector3.zero)
                controller.Move(moveDistance);
        lastPlatformVelocity = (newGlobalPlatformPoint - activeGlobalPlatformPoint) / Time.deltaTime;

        // If you want to support moving platform rotation as well:
        var newGlobalPlatformRotation = activePlatform.rotation * activeLocalPlatformRotation;
        var rotationDiff = newGlobalPlatformRotation * Quaternion.Inverse(activeGlobalPlatformRotation);

        // Prevent rotation of the local up vector
        rotationDiff = Quaternion.FromToRotation(rotationDiff * transform.up, transform.up) * rotationDiff;

        transform.rotation = rotationDiff * transform.rotation;
    }
    else {
        lastPlatformVelocity = Vector3.zero;
    }

    activePlatform = null;

    // Actual movement logic here
    ...
    collisionFlags = myCharacterController.Move (calculatedMovement);
    ...

    // Moving platforms support
    if (activePlatform != null) {
        activeGlobalPlatformPoint = transform.position;
        activeLocalPlatformPoint = activePlatform.InverseTransformPoint (transform.position);

        // If you want to support moving platform rotation as well:
        activeGlobalPlatformRotation = transform.rotation;
        activeLocalPlatformRotation = Quaternion.Inverse(activePlatform.rotation) * transform.rotation; 
    }

}

function OnControllerColliderHit (hit : ControllerColliderHit) {
    // Make sure we are really standing on a straight platform
    // Not on the underside of one and not falling down from it either!
    if (hit.moveDirection.y < -0.9 && hit.normal.y > 0.5) {
        activePlatform = hit.collider.transform;    
    }
}

Notes:

The lastPlatformVelocity variable is not used for anything in the code above, but you can use it in the jumping logic to make the jumping speed get the inertia from the moving platform, such that the character is jumping relative to the platform reference and not to the global reference. You could also calculate a lastPlatformRotationalVelocity to get the rotational inertia from the platform when jumping, but for most games this would be overkill and only confuse the user.

The CharacterController is very simple in function.

Every FixedUpdate it applies the Move() function to determine where it should go. Typically, gravity is assigned, so it always attempts to move down. The Move() function detects objects between the controllers current and intended positions, and will stop moving when a collision is detected. Of course it's much more complex than that, but that is the idea.

However, if between calls to Move() in FixedUpdate, you move the ground above the current position of the CharacterController, the check the CharacterController performs will say "There is nothing below me" and so it will fall. Makes sense.

Now, onto fixing it.

First, to make things behave well, one trick is to make sure all objects the CharacterController interact with are also being adjusted in FixedUpdate. Animations will run in Update. I would suggest animating the platform in code rather than in Maya, and also in the FixedUpdate loop. While there are ways to make what you suggest work, I think it will be far less complicated in this manner.

Depending on your game logic, there are a few options. Personally I dislike parenting the CharacterController to other objects because it can result in some strange behavior. Instead, create some code to tell the CharacterController when it is interacting with a moving platform, and allow the moving platform to tell the CharacterController how far it should move this frame, and then reposition the CharacterController manually.

Connect a script to the platform that detects if someone is on it, in LateUpdate adjust the y pos to the new y pos of the platform.

EDIT: I added an example implementation that should keep any character controller happily on a platform. It's a bit more complex than just adjusting the y position as it also handles horizontal movement and works with more than one char on the platform (so it maintains a list of all players on the platforms).

The platform probably has already a collider so you can't walk through it and stuff, to use the script, add a child GameNode to the platform, give it a trigger collider that's slightly smaller as the platform and extends a bit above the platform. Add the script and adjust the Vertical Offset var. If you need an example project, let me know.

EDIT2: Made a few modifications to handle jumping :-)

using UnityEngine;
using System.Collections;

/** 
 * Helps keeping charactercontroller entities nicely on the platform
 * Needs a Collider set as trigger in the gameobject this script is added to
 * works best if collider is bit smaller as platform but extends quite a lot
 * (say .5m or so) above the platform, As the platform possibly already has
 * a normal collider the easiest way is to add a GameObject to the platform,
 * give it a trigger collider and add this script. The yOffset is the vertical 
 * offset the character should have above the platform (a good value to start
 * with is half the y value of the Collider size).
 */
public class JKeepCharOnPlatform : MonoBehaviour {

    // helper struct to contain the transform of the player and the
    // vertical offset of the player (how high the center of the
    // charcontroller must be above the center of the platform)
    public struct Data {
    	public Data(CharacterController ctrl, Transform t, float yOffset) {
    		this.ctrl = ctrl;
    		this.t = t;
    		this.yOffset = yOffset;
    	}
    	public CharacterController ctrl; // the char controller
    	public Transform t; // transform of char
    	public float yOffset; // y offset of char above platform center
    };

    public float verticalOffset = 0.25f; // height above the center of object the char must be kept

    // store all playercontrollers currently on platform
    private Hashtable onPlatform = new Hashtable();

    // used to calculate horizontal movement
    private Vector3 lastPos;

    void OnTriggerEnter(Collider other) {
    	CharacterController ctrl = other.GetComponent(typeof(CharacterController)) as CharacterController;

    	// make sure we only move objects that are rigidbodies or charactercontrollers.
    	// this to prevent we move elements of the level itself
    	if (ctrl == null) return;

    	Transform t = other.transform; // transform of character

    	// we calculate the yOffset from the character height and center
    	float yOffset = ctrl.height / 2f - ctrl.center.y + verticalOffset;

    	Data data = new Data(ctrl, t, yOffset);

    	// add it to table of characters on this platform
    	// we use the transform as key
    	onPlatform.Add(other.transform, data);
    }

    void OnTriggerExit(Collider other) {
    	// remove (if in table) the uncollided transform
    	onPlatform.Remove(other.transform);
    }

    void Start() {
    	lastPos = transform.position;
    }

    void Update () {
    	Vector3 curPos = transform.position;
    	float y = curPos.y; // current y pos of platform

    	// we calculate the delta
    	Vector3 delta = curPos - lastPos;
    	float yVelocity = delta.y;

    	// remove y component of delta (as we use delta only for correcting
    	// horizontal movement now...
    	delta.y = 0f;

    	lastPos =curPos;

    	// let's loop over all characters in the table
    	foreach (DictionaryEntry d in onPlatform) {
    		Data data = (Data) d.Value; // get the data
    		float charYVelocity = data.ctrl.velocity.y;

    		// check if char seems to be jumping
    		if ((charYVelocity <= 0f) || (charYVelocity <= yVelocity)) {
    			// no, lets do our trick!
    			Vector3 pos = data.t.position; // current charactercontroller position
    			pos.y = y + data.yOffset; // adjust to new platform height
    			pos += delta; // adjust to horizontal movement
    			data.t.position = pos; // and write it back!
    		}
    	}
    }
}

There's several ways to do this of various complexity, depending on what you need. For a simple elevator, you probably want the simplest and easiest way - which would be as follows:

  1. Create a new cube game object which is a child of your elevator platform.
  2. Give the cube a collider component and set it as a trigger
  3. Adjust the size of the cube so that it more-or-less covers the entire floor of your elevator. Y-axis doesn't much matter, but it's important that anyone standing on the platform must be inside the trigger.
  4. Disable the mesh renderer component.
  5. Create a new .js script with the following code in it and add it to the trigger collider:

    function OnTriggerEnter (other : Collider) { other.transform.parent = gameObject.transform; } function OnTriggerExit (other : Collider) { other.transform.parent = null; }

What's happening here is fairly obvious. When anything enters your trigger (like, for example, a CharacterController), it becomes parented to it. Anything parented to the trigger will move when the elevator does. When you step out of the trigger, the parent link is removed. Simple. :)

Why don’t you just make the CharacterController as the child to the floor of the elevator. That solves it. Simple!

I found another way that I think is not mentioned here.

1- Attach the player to the platform.

2- IMPORTANT STEP: Turn the Moving Platform - Enabled Property of Character Motor off!

I don’t know why it does it, but it works like a charm! No jitters for me.

I hope this helps someone.

you could just animate the Player with the Elavator and It will be forced to move with the Elavator

Dj

this works on platforms: horizontal, vertical, rotating, diagonal, up and down, animated, everything.

not affect physics, no bounce, no fall.
does not affect the performance.

  • create a new java script with this.

  • added a new tag. → “movim”

  • add this script to your character
    controller, or Rigidbody.

     #pragma strict
     //for Character Controller, Rigidbody, collider or more.
     //you can modify *movim* for what you want.
      var dentro : boolean = false; 
     function Start () {
      dentro = false;
     }
     function OnTriggerStay ( plataformas : Collider ){
      if(!(dentro) && plataformas.gameObject.tag == "movim"){
      this.transform.parent = plataformas.transform.parent; 
      dentro = true;
      //Debug.Log ("In");
      }
     }
     function OnTriggerExit ( plataformas : Collider){
      if(plataformas.gameObject.tag == "movim"){
      this.transform.parent = null; 
      dentro = false;
      //Debug.Log ("out");
      }
     }
    

11694-elevador.png

Hi, we solved this issue with a SliderJoint and by reducing the gravity scale of the character Rigidbody to 0 until he rests on the moving platform. You can find the full explanation here:
http://spacelizardstudio.com/devblog/index.php/2016/03/02/unity-a-guide-to-moving-platforms/