Moving platform script does not work correctly on different objects

This one is a real head scratcher for me…

For my 3d platformer, I have an empty game object “MovingPlatformParent” with a platform (unity cube shape) as child “MovingPlatform”. Attached to “MovingPlatformParent” is a box collider (set as trigger) and a script that

a) moves the platform via transform.positon (in FixedUpdate)

b) makes itselt the parent of everything in its box collider

private void OnTriggerStay(Collider other)

private void OnTriggerStay(Collider other)

{

other.transform.parent = transform;

}



private void OnTriggerExit(Collider other)

{

other.transform.parent = null;

This works. The player jumps on the platform, becomes a child of the parent and moves with the moving platform - so far so good.

Now my problem:

I want multiple moving platforms. I dublicate the “MovingPlatformParent” in the hierarchy sidebar (Cntrl + D) and drag the whole parent inclusive the child platform to a different point in the world. Now if my player jumps on the new platform, he becomes a child of the parent but doesn’t move with it.

The same issue happens if I create a prefab consisting of parent inclusive script and trigger collider + child physical platform.

Here is a video of the issue: p2kewl

What am I missing?

That seems weird. Put in some Debug.Log() calls to see what is being called when. You can also print the names of the collider other to see who hit you.

The only non-standard thing I see is you are using OTStay()… usually you would use OTEnter() and parent it only once, but then you need to make the trigger thin enough that it hits right when your tootsies hit the platform.

PS looks like a cute little playground you made there… I love seeing stuff like that. :slight_smile:

1 Like

As KD says, that’s strange. Does it matter which platform you jump on first?

What are you using to stick your character to the platform, is it just the parenting that drags it along, or something else?

It looks like you’re caching a reference to the first platform you land on somewhere and not updating it when you land on the other platform as you seem to stick to the first platform again when you return.

Long shot, but does it still happen if you deselect all objects in the heirarchy?

Got anymore code to show?

Here’s another idea: put a rigidbody on the platform and drive its position with .MovePosition() instead of moving the transform.position… I wonder if moving the transform of the player is bypassing its move, but I have no idea why it would only be the cloned object.

Thanks for your replies and the compliment on my “playground”. I’m new to Unity and am just in the last phase of writing a movement script to refreshen my coding skills.

The full script on the platforms:

public class MovingPlatform : MonoBehaviour
{
    public Vector3[] travelPoints;
    private int currentPoint = 0;
    private Vector3 currentTarget;
    public float tolerance;
    public float speed;
    public float delayTime;

    private float delayStart;

    public bool automatic;
    public bool isReversable;

    private void Start()
    {
        if (travelPoints.Length > 0) currentTarget = travelPoints[0];
        tolerance = speed * Time.deltaTime;
    }

    private void FixedUpdate()
    {
        if (transform.position != currentTarget) MovePlatform();
        else UpdateTarget();
    }

    void MovePlatform()
    {
        Vector3 heading = currentTarget - transform.position;

        transform.position += (heading / heading.magnitude) * speed * Time.deltaTime;

        if (heading.magnitude < tolerance)
        {
            transform.position = currentTarget;
            delayStart = Time.time;
        }
    }

    void UpdateTarget()
    {
        if (automatic)
        {
            if (Time.time - delayStart > delayTime)
            {
                NextPlatform();
            }
        }
    }

    public void NextPlatform()
    {
        currentPoint++;
        if (currentPoint >= travelPoints.Length)
        {
            if (isReversable)
            {
                System.Array.Reverse(travelPoints);

            }
            currentPoint = 0;
        }
        currentTarget = travelPoints[currentPoint];
    }
  
    private void OnTriggerEnter(Collider other)
    {
        other.transform.parent = transform;
    }

    private void OnTriggerExit(Collider other)
    {
        other.transform.parent = null;
    }
}
  • As you can see, I changed it to “OnTriggerEnter” but the behaviour is unchanged.
  • It does not matter, which platform I jump on first. Even if the working one is disabled, the other one does not work or vice versa.
  • The player is just being made a child, nothing else to stick it to the platform.
  • I deselected all objects, no difference.
  • Btw, I move the character via the CharacterController.move function in FixedUpdate.
  • I will try your suggestion with a rigidbody for the platform tomorrow, it is getting a bit late here.

Thanks for looking at it!

Edit: Copying the script here, I noticed it has way too many unnecessary public variables and functions… I copied it originally from another source and should probably address this and make most of them private

Since you just Ctrl-D copied the platform, this has got to be something really simple.

Try disabling the above movement script (or removing it entirely), then pausing the editor, and just hand-parenting the player (in the editor window) to the each platform, physically dragging him up on top of it, then unpausing and activating the platform movement, see what happens.

I tried this now. Instead of moving with the platform, the player would still slide off. In addition, the movement from player input was very buggy and the player moved fast in all sorts of directions…

I now disabled the script on the gameobject “MovingPlatform”, there are also no scripts on it’s child, the actual platform body. I disabled all scripts from the player. Then, in Play-Mode I manually moved the player into the box collider trigger and it still became a child of “MovingPlatform”… I then created a new cube and tragged it in the box collider trigger - this also became a child. I disabled the box collider trigger and neither Player nor cube are not becoming children…

How does a box collider trigger cause this with no scripts enabled at all on either of the objects?

OnTrigger functions are called even if the script is disabled. This one is normal at least. I recall reading that it was made like that so scripts can easily be enabled when a trigger/colission happens.

If you don’t want collision behaviour, you’d remove the script that contains OnTrigger and OnCollision functions from your GameObject.

Is it possible that multiple collision/trigger functions are clashing with eachother and doing different things?

I didn’t know that, thanks for clarifying.
I now removed the movement script from the platform and after re-adding, it now does not move the player anymore and behaves the same way as the duplicated platform - it moves, it makes the player a child but it does not move the player (or any other object, unless the object has a rigidbody and physically falls on the platform)

This also makes no difference - the player is becoming a child, but is not moving with the platform

I am squinting intently trying to imagine how this can be… are you SURE there is nothing else specially connected between the player and the platform, perhaps some other script somewhere, on the platform or globally???

Things that are moved will move all their children, even if those children have Rigidbodies. Try it right now! Make an empty scene, make a cube, parent a sphere to the cube, place the sphere above (atop) the cube, put a rigidbody on the sphere, then move the cube… the sphere goes right along with it! Same goes if you wiggle the position in the inspector, or make a MOVE script that moves the cube a tiny bit right each frame.

The longer this goes on the harder we are going to collectively hit ourselves in our foreheads when you figure this out. This has to be something SUPER simple and obvious that we are just overlooking.

I removed all scripts, closed unity and restarted the project.
I’ve created two platform parents, one with a rigidbody, one without. The one with the rigidbody uses a motified script to move via .MovePosition(). Everything else is identical, both have a box collider as trigger and a child which is a physical platform(cube).

I re-added the movement scripts to the platform parent. Now all children move with the platform (if I drag random cubes or spheres in the hierarchy to make them children).

However, two things do not work now:

A)
If I place a cube without any scripts in the movement path just above of platform1 (rigidbody), the platform “grabs” it, as it collides with its box collider trigger and moves it along.
This does not happen for the platform2, moved via transform.position. The cube does not become a child when the platform moves underneath and the cube is contacting the box collider trigger.

B)
The player does become a child of the correct platform. However, it does not move along with either. Commenting out //controller.Move(moveDir * Time.deltaTime); in my PlayerMovement scripts solves this issue. If I now place the player manually on top of the platform, it becomes a child and moves along. It works for both platforms, unlike with the standard cube.

EDIT:
This is just keeping on getting worse… trying out various situations I commented controller.Move() on the Player back in, walked with the player on platform2, and it works as expected… however platform1(rigidbody) does not move it now.

The inconsistency does not make this any easier to pinpoint…

This part sounds correct. The lack of collision detection in this case isn’t related to the difference in how they’re moved. :slight_smile: In order for a collision to be detected, at least one of the colliding objects must have a Rigidbody component, so no collision with your second Rigidbody-less platform.

If it works as intended when you remove the controller, maybe it needs a different approach. Is it running something in one of the update functions that’s disagreeing with how it’s being dragged along by its parent?

Can we get a look at the full controller script?

Right now you probably regret asking, but here is the player controller script (and the only script on the player):
I’m sorry for the mess it is, but it’s my first script I wrote myself since starting unity recently.

Most of the functions can probably be ignored as they are just re-calculating and altering the variable moveDir which is just the direction vector, which is then used in FixedUpdate with charactercontroller.Move().

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerControllerV6 : MonoBehaviour
{  
    [Header("References")]
    public CharacterController controller;
    public Transform cam;
    public PlayerControls controls;
    //LAYERS SLOPE CALCULATION, SLIDE & JUMP FORGIVENESS
    [SerializeField]
    private LayerMask walkableLayers;
    [Space(10)]

    [Header("Movement")]
    //MOVEMENT
    [SerializeField] [Range(0.0f, 15.0f)]
    private float speed = 10;
    private Vector3 moveDir;
    //DIRECTION
    private float turnSmoothVelocityGround;
    private float turnSmoothVelocityAir;
    private float turnSmoothVelocitySlide;
    private float turnSmoothTime = 0.1f;
    [SerializeField]
    //JUMPING
    private float jumpHeight = 2f;
    [SerializeField]
    private float cutJumpSpeedLimit = 2.5f; // 1/10th of gravity is generally good, smooth value // better feel with just above jump height, or 1.5 of jump height
    private bool canDoubleJump = true;
    private bool canSlideJump = true;
    [SerializeField] [Range(0.1f, 1.0f)]
    private float doubleJumpFactor = 0.7f;
    private bool willJump;
    private bool willCancelJump;
    //AIR CONTROL
    [SerializeField] [Range(0.1f, 1.0f)]
    private float airControlFactor = 0.8f;
    [SerializeField] [Range(45.0f, 50.0f)]
    private float airMovementDampeningFactor = 48f; // 0 is full dampening, 50 is none
    private Vector2 directionBufferAirControl = new Vector2(0, 0);
    private Vector2 directionBufferAirControlStatic = new Vector2(0, 0);
    private float maxMagnitudeAirControl;
    private bool isInAirControlCenterLock = false;
    [Space(10)]

    [Header("World & Design Adjustments")]
    //GRAVITY
    private float floorStickiness = -2f;
    private float gravityValue = -9.81f;
    [SerializeField] [Range(1.0f, 3.0f)]
    private float gravityValueModifier = 2; //1 is no modification, <1 reduced gravity >1 increases - calculation in Start function
    private bool isGravityModified = false;
    private float normalGravityValue;
    private float modifiedGravityValue;
   
    [Header("General")]
    //INPUTS
    private bool playerInputJump;
    private bool playerInputJumpCancel;
    private Vector2 playerInputXY;
    //CONTROLLER GROUND CHECK
    private bool isPlayerGrounded;
    //SLIDING
    private bool isSliding;
    private Vector3 slopeColliderHitPoint;
    [SerializeField]
    private float slideLimit = 51; //should be slope limit (or slightly higher to avoid jittering on slope from "isSliding true -> false -> true)
    //GRACE TIMES
    private int offLedgeGraceTime = 5; //5 is a good value (0.1s)
    private int offLedgeGraceTimer = 0;
    private int toSlideGraceTime = 5;
    private int toSlideGraceTimer;
   
   
    private void Start()
    {
        normalGravityValue = gravityValue;
        modifiedGravityValue = gravityValue * gravityValueModifier;
               
    }

    //NEW INPUT SYSTEM
    private void Awake()
    {
        controls = new PlayerControls();
        controls.Player.Move.performed += ctx => MoveInput(ctx.ReadValue<Vector2>());
        controls.Player.Move.canceled += ctx => MoveInput(Vector2.zero);
        controls.Player.Jump.performed += ctx => JumpInput();
        controls.Player.Jump.canceled += ctx => CancelJumpInput();
    }
    private void OnEnable()
    {
        controls.Player.Enable();
    }
    private void OnDisable()
    {
        controls.Player.Disable();   
    }

    private void MoveInput(Vector2 direction)
    {
        playerInputXY = direction;
    }
    private void JumpInput()
    {
        willJump = true;
        //Debug.Log("Pressed Jump");
    }
    private void CancelJumpInput()
    {
        willCancelJump = true;
        //Debug.Log("Cancel Jump");
    }

    //OLD INPUT SYSTEM
    private void Update()
    {
    /*    //grab inputs
        playerInputXY.x = Input.GetAxis("Horizontal");
        playerInputXY.y = Input.GetAxis("Vertical");
        playerInputJump = Input.GetButtonDown("Jump");
        playerInputJumpCancel = Input.GetButtonUp("Jump");
       
        if (playerInputJump)
        {
            willJump = true;
        }
       
        if (playerInputJumpCancel) //no "else if" because both can happen in the same frame - then a hop should occur
        {
            willCancelJump = true;
        }*/
    }


    void FixedUpdate()
    {
        CalculateMovement(playerInputXY, willJump, willCancelJump);
        controller.Move(moveDir * Time.deltaTime);
        AdjustFallingGravity();
    }

    private Vector3 CalculateMovement(Vector2 xyInput, bool jumpTrigger, bool jumpCancelTrigger)
    {
        //GRAVITY PART1
        //check if grounded and negate falling
        isPlayerGrounded = controller.isGrounded;
        if (isPlayerGrounded == true && moveDir.y <= 0)
        {
            //always apply small gravity to make isGrounded work properly
            moveDir.y = floorStickiness;
        }

        //FORGIVING MECHANICS RAY
        //raycast down from player when in the air
        RaycastHit hitBelowPlayerGracejump;
        bool rayPlayerDownHitSmthGracejump = false;
        if (isPlayerGrounded == false)
        {
            CastRayDown(transform.position, 1.2f, out hitBelowPlayerGracejump, out rayPlayerDownHitSmthGracejump);
        }

        //INPUT CONTROLS
        //get sidewards movement direction and clamp at 1 (normalize above 1)
        xyInput = Vector2.ClampMagnitude(xyInput, 1f);
        //ignore minor joystick movements
        if (xyInput.magnitude < 0.1f)
        {
            xyInput.x = 0f;
            xyInput.y = 0f;
        }

        //HORIZONTAL MOVEMENT
        Vector2 basicMovement = HorizontalDirectionCalculation(xyInput);
        moveDir.x = basicMovement.x;
        moveDir.z = basicMovement.y;

        //AIRCONTROL
        if (isPlayerGrounded == true)
        {
            //reset variables on landing and store values of ground movement
            directionBufferAirControlStatic = new Vector2(moveDir.x, moveDir.z);
            directionBufferAirControl = directionBufferAirControlStatic;
            maxMagnitudeAirControl = directionBufferAirControl.magnitude;
            isInAirControlCenterLock = false;
        }
        else //if in air, then re-calculate moveDir under new movement conditions
        {
            Vector2 airMovement = AirControl(new Vector2(moveDir.x, moveDir.z));
            moveDir.x = airMovement.x;
            moveDir.z = airMovement.y;
        }

        //SLOPE CHECK (ANTI-BUMP, SPEED ADJUSTMENT, SLIDING)
        if (isPlayerGrounded == true)
        {
            //raycast down from player when on ground
            CastRayDown(transform.position, 1.05f, out RaycastHit hitBelowPlayerSlope, out bool rayPlayerDownHitSmthSlope);
            //if nothing is hit, recast from character collider
            if (rayPlayerDownHitSmthSlope == false)
            {
                CastRayDown(new Vector3(slopeColliderHitPoint.x, slopeColliderHitPoint.y + 0.15f, slopeColliderHitPoint.z), 0.25f, out hitBelowPlayerSlope, out rayPlayerDownHitSmthSlope);
            }
            //do slope adjustment only if: ray hits something && the player won't jump in this frame (no need to calculate in air)
            if (rayPlayerDownHitSmthSlope == true)
            {
                if (Vector3.Angle(hitBelowPlayerSlope.normal, Vector3.up) < slideLimit)
                {
                    isSliding = false;
                    toSlideGraceTimer = toSlideGraceTime;
                    if (jumpTrigger == false) //this is so the player doesn't jump flat when jumping while moving downhill
                    {
                        moveDir = SlopeCalculation(hitBelowPlayerSlope, moveDir);
                    }
                }
                else
                {
                    moveDir = SlidingCalculation(hitBelowPlayerSlope);
                }
            }
        }
        else
        {
            isSliding = false;
            toSlideGraceTimer = toSlideGraceTime;
        }
       
        //apply speed
        moveDir.x *= speed;
        moveDir.z *= speed;

        //remove sliding and random jittering around
        if (new Vector2(moveDir.x, moveDir.z).magnitude / speed < 0.1f)
        {
            moveDir.x = 0f;
            moveDir.z = 0f;
        }

        //JUMPING
        Jump();

        //GRAVITY PART 2
        //gravity (2* Time.deltaTime as it's applied as acceleration, not velocity)
        moveDir.y += gravityValue * Time.deltaTime;

        return moveDir;


        //FUNCTIONS
        Vector2 HorizontalDirectionCalculation(Vector2 _directionXYIn)
        {

            //calculate direction from Input + Camera
            float targetAngle = Mathf.Atan2(_directionXYIn.x, _directionXYIn.y) * Mathf.Rad2Deg + cam.eulerAngles.y;
            float angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref turnSmoothVelocityGround, turnSmoothTime);

            //apply movement to moveDir
            Vector2 _moveDir;
            _moveDir.x = (Quaternion.Euler(0f, targetAngle, 0f) * new Vector3(0, 0, _directionXYIn.magnitude)).x; //quaternion * vector3 returns a rotated vector (adds rotation to vector) //changed for direction.magnitude to make controller sensitive
            _moveDir.y = (Quaternion.Euler(0f, targetAngle, 0f) * new Vector3(0, 0, _directionXYIn.magnitude)).z;

            //rotate player
            if (_directionXYIn.magnitude >= 0.1f && isPlayerGrounded == true && isSliding == false) //xyInput so player does not rotate with camera when no input
            {
                transform.rotation = Quaternion.Euler(0f, angle, 0f);
            }

            return _moveDir;
        }
        Vector2 AirControl(Vector2 _directionXYIn)
        {
            Vector2 _airMovement;
            if (_directionXYIn.magnitude >= 0.2f && Vector2.Angle(_directionXYIn, directionBufferAirControlStatic) < 150 && isInAirControlCenterLock == false) //needed to avoid "snapping" when controller returns to 0 && needed to always be able to go back to 0, independent of AC factor && to disable this movement once in center
            {
                // overlay original direction with input
                _airMovement = directionBufferAirControlStatic + (_directionXYIn * airControlFactor); //no time.deltatime as not an acceleration
            }
            else //keep direction when no input
            {
                //keep old direction and also slow down by dampening factor
                _airMovement = directionBufferAirControl * airMovementDampeningFactor * Time.deltaTime; //time.deltatime as it is a deceleration (Buffer not static but getting less each frame)
            }

            //add -0.3 moveability in all directions if returned to 0
            if (_airMovement.magnitude <= 0.3f)
            {
                _airMovement = Vector2.ClampMagnitude(_directionXYIn, 0.3f);
                isInAirControlCenterLock = true;
            }
            else //clamp to current magnitude to avoid returning to old speed without input //"else" because not needed when under 0.3, also causes jittering on rotation for unknown reason
            {
                _airMovement = Vector2.ClampMagnitude(_airMovement, maxMagnitudeAirControl);
                maxMagnitudeAirControl = MaxClampVector(_airMovement);
            }

            //double jump controls
            if (willJump && canDoubleJump && rayPlayerDownHitSmthGracejump == false)
            {  
                //overlay half the current movement with half the input
                _airMovement = _airMovement * 0.5f + _directionXYIn * 0.5f;
                directionBufferAirControlStatic = _airMovement;
                maxMagnitudeAirControl = MaxClampVector(_airMovement);
                isInAirControlCenterLock = false;
            }

            //rotation in air with if requirement so it does not reset the direction to 0 when jumping)
            if (_airMovement.magnitude >= 0.15f) //only rotate when moving
            {
                //rotate player
                float targetAngleAir = Mathf.Atan2(_airMovement.x, _airMovement.y) * Mathf.Rad2Deg;
                float angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngleAir, ref turnSmoothVelocityAir, turnSmoothTime * 2f);
                transform.rotation = Quaternion.Euler(0f, angle, 0f);
            }

            //store direction in case of no input in next frame
            directionBufferAirControl = _airMovement;

            return _airMovement;

            //FUNCTIONS
            float MaxClampVector(Vector2 _vectorToClamp)
            {
                float _maxMagnitude = _vectorToClamp.magnitude;
                //always allow certain move ability
                if (_maxMagnitude < 0.3f)
                {
                    _maxMagnitude = 0.3f;
                }
                return _maxMagnitude;
            }

            //OLD APPROACHES FOR FUTURE REFERENCE
            //_directionXYIn = Vector2.Lerp(directionBufferAirControl, _directionXYIn, airControlFactor * Time.deltaTime);
            //_directionXYIn = Vector2.SmoothDamp(directionBufferAirControl, _directionXYIn, ref airControlSmoothVelocity, airControlFactor);
            //Debug.Log(_directionXYIn);

            //NOTES:
            //- possible issue: jump from standing. Currently a jump in a direction is possible in all directions e.g. 0 +- 0.8(ACF). However, this is then being clamped as soon as it is above 0.2, but at a "random" value, depending on how fast the input is registered.
            //  However, it can never be more than the ACF (e.g. 0.8) or less than the input clamping requirement (e.g. 0.15), so it might be acceptable.
        }
        void CastRayDown(Vector3 rayOrigin, float rayLength, out RaycastHit _hit, out bool _hitSomething)
        {
            Ray rayPlayerDown = new Ray(rayOrigin, Vector3.down);
            _hitSomething = Physics.Raycast(rayPlayerDown, out _hit, rayLength, walkableLayers);
        }
        Vector3 SlopeCalculation(RaycastHit _hitBelowPlayer, Vector3 _directionXYZIn)
        {
            float _slopeAngle = Vector3.Angle(_hitBelowPlayer.normal, new Vector3(_directionXYZIn.x, 0, _directionXYZIn.z)) - 90;

            //calculate slopefactor to adjust speed accordingly (only apply to Y downwards, keep -floorStickiness for upwards)
            float slopeSpeedFactor = 1;
            if (_slopeAngle > -90 && _slopeAngle < 90)
            {
                slopeSpeedFactor = 1 / Mathf.Cos(_slopeAngle * Mathf.Deg2Rad); //always 1, not directionXZ.magnitude to not cancel it out
            }

            //move character y according to slope; only when moving down to prevent "overshoot" at end of slope
            if (_slopeAngle < 0)
            {
                _directionXYZIn.y += -(Quaternion.Euler(_slopeAngle, 0f, 0f) * new Vector3(0, 0, new Vector2(_directionXYZIn.x, _directionXYZIn.z).magnitude) * speed / slopeSpeedFactor).y;
            }

            //adjust XZ speed to slope

            _directionXYZIn.x /= slopeSpeedFactor;
            _directionXYZIn.z /= slopeSpeedFactor;

            return _directionXYZIn;
        }
        Vector3 SlidingCalculation(RaycastHit _hitBelowPlayer)
        {
            //forgiving mechanics
            GraceTimeToSlide();
            isSliding = true;

            if (toSlideGraceTimer == 0)
            {
                Vector3 _slopeNormal = _hitBelowPlayer.normal;
                Vector3 _temp = Vector3.Cross(Vector3.up, _slopeNormal); //left/right to slope
                Vector3 _moveDir = (Vector3.Cross(_temp, _slopeNormal).normalized); //90 to left right = parallel to slope
                _moveDir.y = floorStickiness + (_moveDir.y * speed); //apply speed to y as this is usually not done (same is done in SlopeCalculation)

                //rotate player
                float targetAngleSlide = Mathf.Atan2(_moveDir.x, _moveDir.z) * Mathf.Rad2Deg;
                float angleSlide = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngleSlide, ref turnSmoothVelocitySlide, turnSmoothTime * 2f);
                transform.rotation = Quaternion.Euler(0f, angleSlide, 0f);
               
                return _moveDir;
            }
            else
            {
                return moveDir;
            }
                       
            //FUNCTIONS
            void GraceTimeToSlide()
            {
                toSlideGraceTimer -= 1;
                if (toSlideGraceTimer < 0)
                {
                    toSlideGraceTimer = 0;
                }
            }

        }
        void Jump()
        {
            //Forgiving mechanics
            GraceTimeOffLedge();
           
            //reset double / slide jump ability
            if (isPlayerGrounded)
            {
                canDoubleJump = true;
            }
            if (isPlayerGrounded && isSliding == false)
            {
                canSlideJump = true;
            }
           
            //initiate normal jump
            if (offLedgeGraceTimer > 0 && jumpTrigger)
            {
                moveDir.y += Mathf.Sqrt(jumpHeight * -2.0f * normalGravityValue) - floorStickiness;
                offLedgeGraceTimer = 0;
            }
            //initiate slide jump //play around with different jumps... for example normal or double jump
            else if (isSliding == true && canSlideJump && jumpTrigger)
            {
                moveDir.y = Mathf.Sqrt(jumpHeight * doubleJumpFactor * -2.0f * normalGravityValue);
                offLedgeGraceTimer = 0;
                canSlideJump = false;
            }
            //initiate double jump
            else if (canDoubleJump && jumpTrigger && rayPlayerDownHitSmthGracejump == false && isSliding == false) //add additional requirement here and to aircontrol function //rayPlayerDownHitSmthGraceump == false to avoid double jump just before landing
            {
                moveDir.y = Mathf.Sqrt(jumpHeight * doubleJumpFactor * -2.0f * normalGravityValue);
                canDoubleJump = false;
            }

            //jump cancellation
            //check if player is currently jumping state by checking if moveDir.y is positive. If it's positive and very small because it's the end of the jump, don't do anything (cutJumpSpeedLimit). Add a minor upwards movement for fluent movement (same as cut-off).
            //PROBLEM: No effective "is jumping" check; if moved up by other force and then released, it will stop that movement... possibly not a problem if nothing else moves the Character up while controls enabled.
            if (jumpCancelTrigger && moveDir.y > cutJumpSpeedLimit)
            {
                moveDir.y = cutJumpSpeedLimit;
            }

            //forgiving mechanics part 2 and jump trigger reset
            //jump trigger stays active when pressed in air unless ray is not hitting ground || player moves upwards (to reset it directly after a jump)
            //same for cancelTrigger
            if (rayPlayerDownHitSmthGracejump == false || moveDir.y > 0)
            {
                willJump = false;
                willCancelJump = false;
            }


            //FUNCTIONS
            void GraceTimeOffLedge()
            {
                if (isPlayerGrounded && isSliding == false) // MAYBE && SLOPE LESS THAN 50 - THIS WILL ALLOW A JUMP SHORTLY AFTER "SLIPPING" //or maybe "is not sliding"
                {
                    offLedgeGraceTimer = offLedgeGraceTime;
                }
                else
                {
                    offLedgeGraceTimer -= 1;
                    if (offLedgeGraceTimer < 0)
                    {
                        offLedgeGraceTimer = 0;
                    }
                }
            }
        }
    }
    private void AdjustFallingGravity()
    {  
        if (isPlayerGrounded == false && moveDir.y < 0 && isGravityModified == false)
        {
            gravityValue = modifiedGravityValue;
            isGravityModified = true;
        }
        if ((isPlayerGrounded == true || moveDir.y > 0) && isGravityModified == true)
        {
            gravityValue = normalGravityValue;
            isGravityModified = false;
        }
    }
    private void OnControllerColliderHit(ControllerColliderHit hit)
    {  
        slopeColliderHitPoint = hit.point;
    }
   
}

Man, I just love seeing stuff like this… clearly V1 through V5 were earlier experiments and you are onto V6!

If I had a dollar for every time I did this… I already tend to suffix everything I do with a 1 just in case I want a version 2 in the same project.

I recommend making a quick super-stripped-down PlayerControllerV7 that does like the ultra super minimum of move around and leap onto the platform.

Also, check out some other tutorials, because I have seen platform-attach games do their thing by using a FixedJoint to temporarily “bolt” the player in place, then detach it. I’ve never tried that, but I’ve seen the tutorials advocating it.

1 Like

Alternately you can try turning off some of those sub-functions, like the slope and sliding and air control and whatnot, see if it is one of those causing it to push off the platform oddly.

Absolutely! It’s my project to get familiar with scripting and Unity’s functions, so there were definitely some failed iterations before :slight_smile:

I’ve stripped it down to some basics:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerControllerSimplified : MonoBehaviour
{  
    [Header("References")]
    public CharacterController controller;
    public Transform cam;
    public PlayerControls controls;
    [Space(10)]

    [Header("Movement")]
    //MOVEMENT
    [SerializeField] [Range(0.0f, 15.0f)]
    private float speed = 10;
    private Vector3 moveDir;
    //DIRECTION
    private float turnSmoothVelocityGround;
    private float turnSmoothTime = 0.1f;
    [SerializeField]
    //JUMPING
    private float jumpHeight = 2f;
    [SerializeField]
    private bool willJump;
   
    [Header("World & Design Adjustments")]
    //GRAVITY
    private float floorStickiness = -2f;
    private float gravityValue = -9.81f;
       
   
    [Header("General")]
    //INPUTS
    private Vector2 playerInputXY;
   
    private bool isPlayerGrounded;
   
   
   
    private void Start()
    {
                       
    }

    //NEW INPUT SYSTEM
    private void Awake()
    {
        controls = new PlayerControls();
        controls.Player.Move.performed += ctx => MoveInput(ctx.ReadValue<Vector2>());
        controls.Player.Move.canceled += ctx => MoveInput(Vector2.zero);
        controls.Player.Jump.performed += ctx => JumpInput();
       
    }
    private void OnEnable()
    {
        controls.Player.Enable();
    }
    private void OnDisable()
    {
        controls.Player.Disable();   
    }

    private void MoveInput(Vector2 direction)
    {
        playerInputXY = direction;
    }
    private void JumpInput()
    {
        willJump = true;
        //Debug.Log("Pressed Jump");
    }
   

    void FixedUpdate()
    {
        CalculateMovement(playerInputXY, willJump);
        controller.Move(moveDir * Time.deltaTime);
    }

    private void CalculateMovement(Vector2 xyInput, bool jumpTrigger)
    {
        //GRAVITY PART1
        //check if grounded and negate falling
        isPlayerGrounded = controller.isGrounded;
        if (isPlayerGrounded == true && moveDir.y <= 0)
        {
            //always apply small gravity to make isGrounded work properly
            moveDir.y = floorStickiness;
        }

       
        //INPUT CONTROLS
        //get sidewards movement direction and clamp at 1 (normalize above 1)
        xyInput = Vector2.ClampMagnitude(xyInput, 1f);
        //ignore minor joystick movements
        if (xyInput.magnitude < 0.1f)
        {
            xyInput.x = 0f;
            xyInput.y = 0f;
        }

        //HORIZONTAL MOVEMENT
        Vector2 basicMovement = HorizontalDirectionCalculation(xyInput);
        moveDir.x = basicMovement.x;
        moveDir.z = basicMovement.y;
                     
       
        //apply speed
        moveDir.x *= speed;
        moveDir.z *= speed;

        //remove sliding and random jittering around
        if (new Vector2(moveDir.x, moveDir.z).magnitude / speed < 0.1f)
        {
            moveDir.x = 0f;
            moveDir.z = 0f;
        }

        //JUMPING
        Jump();

        //GRAVITY PART 2
        //gravity (2* Time.deltaTime as it's applied as acceleration, not velocity)
        moveDir.y += gravityValue * Time.deltaTime;

      
        //FUNCTIONS
        Vector2 HorizontalDirectionCalculation(Vector2 _directionXYIn)
        {

            //calculate direction from Input + Camera
            float targetAngle = Mathf.Atan2(_directionXYIn.x, _directionXYIn.y) * Mathf.Rad2Deg + cam.eulerAngles.y;
            float angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref turnSmoothVelocityGround, turnSmoothTime);

            //apply movement to moveDir
            Vector2 _moveDir;
            _moveDir.x = (Quaternion.Euler(0f, targetAngle, 0f) * new Vector3(0, 0, _directionXYIn.magnitude)).x; //quaternion * vector3 returns a rotated vector (adds rotation to vector) //changed for direction.magnitude to make controller sensitive
            _moveDir.y = (Quaternion.Euler(0f, targetAngle, 0f) * new Vector3(0, 0, _directionXYIn.magnitude)).z;

            //rotate player
            if (_directionXYIn.magnitude >= 0.1f && isPlayerGrounded == true) //xyInput so player does not rotate with camera when no input
            {
                transform.rotation = Quaternion.Euler(0f, angle, 0f);
            }

            return _moveDir;
        }
       
        void Jump()
        {
                       
            //initiate normal jump
            if (jumpTrigger)
            {
                moveDir.y += Mathf.Sqrt(jumpHeight * -2.0f * gravityValue) - floorStickiness;
               
            }
           
             willJump = false;
            
            }
        }
    }

The results are the same… it randomly seems to work on and off on one platform, but never on a duplicate.
I will now try and simplify the movement script further by removing some more of the functionoality.

Thank you by the way for how helpful you are with this! I’d definitely be lost here on my own as I’m not even sure where to start looking anymore…

2 Likes

I think I can now rule out my PlayerController Script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerControllerSimplified : MonoBehaviour
{
    public CharacterController controller;
    public Transform cam;
    public PlayerControls controls;

    //INPUTS
    private Vector2 playerInputXY;

    //NEW INPUT SYSTEM
    private void Awake()
    {
        controls = new PlayerControls();
        controls.Player.Move.performed += ctx => MoveInput(ctx.ReadValue<Vector2>());
        controls.Player.Move.canceled += ctx => MoveInput(Vector2.zero);
    }
    private void OnEnable()
    {
        controls.Player.Enable();
    }
    private void OnDisable()
    {
        controls.Player.Disable();
    }

    private void MoveInput(Vector2 direction)
    {
        playerInputXY = direction;
    }

    void FixedUpdate()
    {
        controller.Move(new Vector3(playerInputXY.x, 0, playerInputXY.y) * 10 * Time.deltaTime);
    }

}

With the last line commented out everything works. With the line in, the platform movement is buggy / inconsistend…

Maybe I really need another approach to this. I’ve read of people suggesting to add the platform movement to the charactercontroller.Move() Vector… I might try this.

Although this one is still bugging me out. Especially as I’m doing it with the goal to learn, it doesn’t quite sit right with me to ignore it and move on…

Pretty sure the CharacterController.Move function should run in Update. Try changing that FixedUpdate to Update if you’re getting inconsistent results, as FixedUpdate doesn’t run every frame. Use Fixed if you’re modifying physics, and Update if you’re using position changes or Translate.

1 Like

Well, that seems to work… :hushed: Thank you so much for pointing this out!
I am very surprised though, as I thought I read once it belongs in FixedUpdate and actively moved it there at the time.
Running the function in Update() now causes other issues with my controller script (like jittering) but I guess I have to go through it and see what might not agree with the dynamic frame rate.

@Kurt-Dekker I think now is the time to hit our foreheads…

1 Like