Move a player on a moving platform?

I’m having trouble making my player move along with the platform he is standing on.

To make the player control correctly, pretty much of of the player’s movement is calculated within the player’s script, which is then applied as a vector to its Rigigbody2D. So there is no force pulling the player down unless my code has deemed the player to be in the air (done by checking for collision at three points beneath the player) and no motion to the left or right unless the player is trying to move (or some other special circumstance.)

So by default, if the player stands on something and that thing moves, it has no effect on the player. The platform can move forward and the player will remain absolutely still until the platform is gone, and then the player falls.

So the first method I tried to fix this was to have the moving platform send its velocity to the player’s script, which then adds that velocity to the player.

if (bGrounded && PlayerOnGround == GroundType.MovingPlatform) {
            newVelocity += currentGround.GetComponent<MovingPlatform>().publicVelocity;
        }
        rigidbody2D.velocity = newVelocity;

It was largely functional, but the player is somewhat delayed from this motion. Overall it’s fine, but when the platform goes down, that delay can cause the player to lag too far behind the platform, and the player script fails to find any ground beneath them, and they go airborne. Also most of the motions I want to use for my platforms don’t work as well as velocities, so I have to convert their motion into velocity to send it to the player, and that data gets much more off because the player is fed inaccurate motion.

So I tried a different method where the platform would calculate its difference in position each frame, then adjust the player’s actual position according to that difference.

        if (standee != null) {
            Vector3 change = transform.position - lastPostion;
            standee.transform.position = standee.transform.position + change;

            standee = null;
        }

This method kept the player exactly in sync with the moving platform, and I had no problems with moving down or in any direction, and it overall works perfectly. However, I now ran into a problem when the player tries to move while on the moving platform. Suddenly the player walks incredibly slowly. Some effect of having its position changed every frame is causing the rigidbody’s velocity to not have a proper effect on the object.

I’m wondering if anyone has any ideas for something else I could try. I honestly don’t know what else I could do; I tried moving the player’s position and I tried moving the player’s velocity, and that’s pretty much the extent of the ways you can move an object. And if changing an object’s position causes its velocity to change as well, well, I have no earthly idea how to fix that.

So I’m going to share with you how I take care of this.

using UnityEngine;
using System.Collections.Generic;

using com.spacepuppy;
using com.spacepuppy.Movement;
using com.spacepuppy.Utils;

namespace com.apoc.Movement
{

    [AddComponentMenu("Apoc/Movement/Resolver: Moving Platform")]
    [RequireComponent(typeof(ApocMovementMotor))]
    public class MovingPlatformResolver : SPComponent
    {

        #region Fields

        private ApocMovementMotor _motor;

        private Collider _platform;
        private Vector3 _localPoint;
        private Vector3 _globalPoint;

        private Vector2 _lastMove;
        private Vector2 _lastVelocity;

        #endregion

        #region CONSTRUCTOR

        protected override void Awake()
        {
            base.Awake();

            _motor = this.GetComponent<ApocMovementMotor>();
        }

        protected override void OnStartOrEnable()
        {
            base.OnStartOrEnable();

            //Notification.RegisterObserver<MovementController.MovementControllerHitNotification>(this.entityRoot, this.OnMovementControllerHit);
            _motor.Controller.MovementControllerHit -= this.OnMovementControllerHit;
            _motor.Controller.MovementControllerHit += this.OnMovementControllerHit;

            _motor.BeforeUpdateMovement -= this.OnBeforeUpdateMovement;
            _motor.UpdateMovementComplete -= this.OnUpdateMovementComplete;
            _motor.BeforeUpdateMovement += this.OnBeforeUpdateMovement;
            _motor.UpdateMovementComplete += this.OnUpdateMovementComplete;
        }

        protected override void OnDisable()
        {
            base.OnDisable();

            //Notification.RemoveObserver<MovementController.MovementControllerHitNotification>(this.entityRoot, this.OnMovementControllerHit);
            _motor.Controller.MovementControllerHit -= this.OnMovementControllerHit;

            _motor.BeforeUpdateMovement -= this.OnBeforeUpdateMovement;
            _motor.UpdateMovementComplete -= this.OnUpdateMovementComplete;
        }

        #endregion

        #region Properties

        public Vector2 LastMove { get { return _lastMove; } }

        public Vector2 LastVelocity { get { return _lastVelocity; } }

        #endregion

        #region Methods

        void OnMovementControllerHit(object sender, MovementController.MovementControllerHitEventArgs e)
        {
            if (!e.Collider.gameObject.IntersectsLayerMask(Constants.MASK_SURFACE)) return;

            this.ManuallySetPlatformCollider(e.Collider);
        }

        public void ManuallySetPlatformCollider(Collider coll)
        {
            _platform = coll;
            _globalPoint = this.entityRoot.transform.position;
            _localPoint = _platform.transform.InverseTransformPoint(this.entityRoot.transform.position);
        }

        #endregion

        #region IMovementEnhancer Interface

        private void OnBeforeUpdateMovement(object sender, System.EventArgs e)
        {
            if (_platform != null)
            {
                var p = _platform.transform.TransformPoint(_localPoint);
                _lastMove = _motor.ProjectPosition2D(p - _globalPoint);
                _lastVelocity = _lastMove / Time.deltaTime;
                if (_lastMove != Vector2.zero)
                {
                    _motor.AtypicalMove(_lastMove);
                }
            }
            else
            {
                _lastMove = Vector2.zero;
                _lastVelocity = Vector2.zero;
            }

            _platform = null;
        }

        private void OnUpdateMovementComplete(object sender, System.EventArgs e)
        {
            if (_platform != null)
            {
                _globalPoint = this.entityRoot.transform.position;
                _localPoint = _platform.transform.InverseTransformPoint(this.entityRoot.transform.position);
            }
        }

        #endregion

    }

}

Now note, this is technically 3D but projected onto a 2D surface.

Furthermore I have special classes with which I do the actual update of movement. That being the ApocMovementMotor.

This resolves how to actually update of position occurs. This way I abstract out the difference of rigidbodies vs character controller vs whatever else.

Furthermore, I don’t work in the ‘Update’ function. I have special BeforeUpdateMovement and UpdateMovement functions that do it, again defined in the ApocMovementMotor, and they also give off events so that I can create resolvers like this one that perform tasks before the ApocMovementMotor allows the actual movement script to do its thing. But really, it happens during the update/fixedupdate function, just with these extra bells and whistles abstracted out.

You’ll notice that I perform the actual move through a call to a function called ‘AtypicalMove’. This is because I have 2 ‘Move’ functions on the ApocMovementMotor. One called ‘Move’ which just moves, and stores the amount that was moved so we can calculate velocity. The other being this ‘AtypicalMove’, this moves the actual object… but does not store the amount moved. This way the calculated velocity does not include this move. This is because if you’re standing still on a moving platform, you’re rigidbody velocity will be that of the moving platform… but the velocity as the movement script considers it is that you’re NOT moving. This is the critical part not in the code I’m sharing that you need to do. You need to keep track of “real” velocity, and “velocity as it is considered by the movement script”

Lastly the OnMovementControllerHit event I hook into. Really all that is is a forwarding event for the ‘OnColliderEnter/Stay’ game messages. 1) so that a script doesn’t necessarily have to be attached to the gameobject, and 2) because the controller maybe a CharacterController, or may be a Rigidbody, which have DIFFERENT game messages. This abstracts it out into a single event.

Dang, I only understand about half of what you are saying, and less than half of your code.

Does your custom move code still move the player by changing the rigidbody’s velocity; by giving a value to rigidbody.velocity?

“This way the calculated velocity does not include this move. This is because if you’re standing still on a moving platform, you’re rigidbody velocity will be that of the moving platform… but the velocity as the movement script considers it is that you’re NOT moving.”
This sounds good in theory, but yet I don’t see how it would be done differently than what I have in actual execution. I still calculate my player’s movement code separately, to determine what the velocity would be in “normal” conditions. But the velocity I actually feed to the rigidbody includes both this and the velocity from the platform. Functionally, isn’t that the same as what you are describing? I didn’t separate thing into special functions, but the final result should be the same – from what I can tell.
But yet I still wind up having a part of my platform’s path where the player gets disconnected from the platform, and my falling animation begins. (Unless I move the platform slower, but the speed it is moving at already is reasonable.)

So, I looked through my code, and I apologize, I said that I adjusted the velocity always. Turns out I don’t. My AtypicalMove for Rigidbody just alters the position.

So… in my example above, inside the ‘Motor’ where I abstract the code for different setups. One of which is DirectRigidbodyMover. So I looked in there…

Stuff in spoiler is over kill, mostly there for demonstration of where I pulled it from.

I do this by having various classes that implement an interface ‘IGameObjectMover’.

        public interface IGameObjectMover : IIgnorableCollision, System.IDisposable
        {

            bool PrefersFixedUpdate { get; }

            float Mass { get; set; }
            float StepOffset { get; set; }
            float SkinWidth { get; set; }
            bool CollisionEnabled { get; set; }
            Vector3 Velocity { get; set; }

            void Reinit(MovementController controller);
            void OnEnable();
            void OnDisable();

            /// <summary>
            /// Occurs before any Moves for this frame are called, and before the LastPosition and LastVelocity are set. This is useful for resolving anything that deals with
            /// the FixedUpdate loop and Rigidbodies, since the actual effects caused by moving a rigidbody aren't accessible until after the fact.
            /// </summary>
            void OnBeforeUpdate();
            /// <summary>
            /// Occurs after all Moves for this frame have been called.
            /// </summary>
            void OnUpdateComplete();

            void Move(Vector3 mv);
            void AtypicalMove(Vector3 mv);
            void AddForce(Vector3 f, ForceMode mode);
            void AddForceAtPosition(Vector3 f, Vector3 pos, ForceMode mode);

        }

And here’s the one I use for Rigidbody. Well I have 2 for Rigidbody, but this is the one that effects the ‘velocity’, as opposed to using ‘MovePosition’.

        /// <summary>
        /// This is a rigidbody controller that manipulates the velocity directly.
        /// </summary>
        [System.Serializable()]
        public class DirectRigidBodyMover : IGameObjectMover
        {

            #region Fields

            [System.NonSerialized()]
            private MovementController _owner;
            [System.NonSerialized()]
            private Rigidbody _rigidbody;
            [System.NonSerialized()]
            private Collider _coll;
            [System.NonSerialized()]
            private CollisionEventHooks _hooks;

            [SerializeField()]
            private float _stepOffset;
            [SerializeField()]
            private float _skinWidth;
            [SerializeField()]
            private bool _freeMovement;

            [System.NonSerialized()]
            private Vector3 _talliedMove;
            [System.NonSerialized()]
            private Vector3 _lastVel;


            //enabled cache
            [System.NonSerialized()]
            private bool _cacheIsKinematic;

            #endregion

            #region CONSTRUCTOR

            public DirectRigidBodyMover()
            {
            }

            #endregion

            #region Properties

            public bool FreeMovement
            {
                get { return _freeMovement; }
                set { _freeMovement = value; }
            }

            #endregion

            #region IGameObjectMover Interface

            public bool PrefersFixedUpdate { get { return true; } }

            public float Mass
            {
                get { return (_rigidbody != null) ? _rigidbody.mass : 0f; }
                set { if (_rigidbody != null) _rigidbody.mass = value; }
            }
            public float StepOffset { get { return _stepOffset; } set { _stepOffset = value; } }
            public float SkinWidth { get { return _skinWidth; } set { _skinWidth = value; } }
            public bool CollisionEnabled
            {
                get
                {
                    return (_coll != null) ? _coll.enabled : false;
                }
                set
                {
                    if(_coll != null) _coll.enabled = value;
                }
            }
            public Vector3 Velocity
            {
                get { return _rigidbody.velocity; }
                set
                {
                    _rigidbody.velocity = value;
                    _talliedMove = value;
                }
            }

            public void Reinit(MovementController controller)
            {
                if (Object.Equals(controller, null)) throw new System.ArgumentNullException("controller");
                _owner = controller;

                if (_owner.rigidbody == null) throw new System.InvalidOperationException("MovementController requires Rigidbody component to operate in Rigidbody mode.");
                //if (!_owner.HasComponent<SphereCollider>() && !_owner.HasComponent<CapsuleCollider>()) throw new System.InvalidOperationException("MovementController requires a supported Sphere or Capsule Collider component to operate in Rigidbody mode.");

                _rigidbody = _owner.rigidbody;
                _coll = _owner.GetFirstLikeComponent<Collider>();
            }

            public void OnEnable()
            {
                _cacheIsKinematic = _rigidbody.isKinematic;
                _rigidbody.isKinematic = false;

                _lastVel = _rigidbody.velocity;

                _hooks = _owner.AddOrGetComponent<CollisionEventHooks>();
                _hooks.OnCollisionBasic -= this.OnCollision;
                _hooks.OnCollisionBasic += this.OnCollision;
            }

            public void OnDisable()
            {
                _rigidbody.isKinematic = _cacheIsKinematic;

                if (_hooks != null)
                {
                    _hooks.OnCollisionBasic -= this.OnCollision;
                    _hooks = null;
                }
            }

            public void OnBeforeUpdate()
            {
                _talliedMove = Vector3.zero;
            }

            public void OnUpdateComplete()
            {
                if(_freeMovement)
                {
                    _lastVel = _rigidbody.velocity;
                }
                else
                {
                    if (!_owner.MoveCalled)
                    {
                        _rigidbody.velocity = _lastVel;
                    }
                    else
                    {
                        _lastVel = _rigidbody.velocity;
                    }
                }
            }

            public Capsule GetGeom(bool ignoreSkin)
            {
                if (_coll == null)
                {
                    var p = _owner.transform.position;
                    if (ignoreSkin)
                        return new Capsule(p, p, 0f);
                    else
                        return new Capsule(p, p, _skinWidth);
                }
                else
                {
                    var c = Capsule.FromCollider(_coll);
                    if (ignoreSkin) c.Radius -= _skinWidth;
                    return c;
                }
            }

            public void Move(Vector3 mv)
            {
                _talliedMove += mv;

                Vector3 v = _talliedMove / Time.deltaTime;
                //v -= _owner.LastVelocity; //remove the old velocity so it's setting to, not adding to
                //_rigidbody.AddForce(v, ForceMode.VelocityChange);
                _rigidbody.velocity = v;
            }

            public void AtypicalMove(Vector3 mv)
            {
                //_rigidbody.MovePosition(_rigidbody.position + mv);
                _owner.transform.position += mv; //for some reason moveposition doesn't work with moving platforms
            }

            public void AddForce(Vector3 f, ForceMode mode)
            {
                _rigidbody.AddForce(f, mode);
            }

            public void AddForceAtPosition(Vector3 f, Vector3 pos, ForceMode mode)
            {
                _rigidbody.AddForceAtPosition(f, pos, mode);
            }

            #endregion

            #region Event Handlers

            private void OnCollision(Collision c, bool bStayed)
            {
                if(_owner.HasHitListeners)
                {
                    //TODO - need to think of a better way to deal with more than 1 contact, currently just going to treat as the FIRST contact
                    if (c.contacts.Length >= 1)
                    {
                        //var contact = c.contacts[0];
                        //var e = new MovementControllerHitEventArgs(_owner, c.collider, contact.normal, contact.point);
                        var e = new MovementControllerHitEventArgs(_owner, c.collider, (from cont in c.contacts select new MovementControllerHitContact(cont.normal, cont.point)).ToArray());
                        _owner.SignalHit(e);
                    }
                }
            }

            #endregion

            #region IIGnorableCollision Interface

            public void IgnoreCollision(Collider coll, bool ignore)
            {
                if (_coll != null && _coll.enabled) Physics.IgnoreCollision(_coll, coll, ignore);
            }

            public void IgnoreCollision(IIgnorableCollision coll, bool ignore)
            {
                if (_coll != null && coll != null) coll.IgnoreCollision(_coll, ignore);
            }

            #endregion

            #region Disposable Interface

            private bool _isDisposed = false;

            void System.IDisposable.Dispose()
            {
                if (!_isDisposed)
                {
                    if (_hooks != null)
                    {
                        _hooks.OnCollisionBasic -= this.OnCollision;
                    }
                
                    _owner = null;
                    _coll = null;
                    _hooks = null;
                    _isDisposed = true;
                }
            }

            #endregion

        }

In it note my Move vs AtypicalMove:

            public void Move(Vector3 mv)
            {
                _talliedMove += mv;

                Vector3 v = _talliedMove / Time.deltaTime;
                //v -= _owner.LastVelocity; //remove the old velocity so it's setting to, not adding to
                //_rigidbody.AddForce(v, ForceMode.VelocityChange);
                _rigidbody.velocity = v;
            }

            public void AtypicalMove(Vector3 mv)
            {
                //_rigidbody.MovePosition(_rigidbody.position + mv);
                _owner.transform.position += mv; //for some reason moveposition doesn't work with moving platforms
            }

So yeah, try just setting the position for the actual update of the position.[/spoiler]