0
\$\begingroup\$

I have a player entity with Rigidbody2D component and I have a moving platform which is a kinematic Rigidbody2D. Player movement is done by setting it's velocity (body.velocity = newVelocity) and platform is moved via body.MovePosition.

I've made it so that the platform has a sensor which, when triggered, catches the Rigidbody2D of the entity that is on (currently only does that for player) and then I tried the following:

  1. Moving the player along the platform with caughtRigidbody.MovePosition. This keeps the player on the platform but the player is unable to move. It seems that MovePosition overrides any velocity change on the player and keeps him on the platform indefinitely. This would be ideal if I could have this behavior but keeping player movement.

  2. Moving the caught rigidbody by adding platform velocity to the body but it seems jittery and also happens to behave strangely when platform direction changes.

I got the player movement how I want it so I'm not willing to tear out the whole movement code and change the player to kinematic or something like that.

This is the relevant platform code:

public class MovingPlatform
{
    private void FixedUpdate()
    {
        // Only one node (initial position). Nowhere to move to
        if (worldNodes.Length == 1)
        {
            return;
        }

        // Target position reached?
        if (body.position == worldNodes[index])
        {
            // Is last (or first in backwards direction) target?
            if (direction == Direction.Forwards && index == worldNodes.Length - 1 ||
                direction == Direction.Backwards && index == 0)
            {
                // Change direction (forwards = 1, backwards = -1)
                direction = (Direction) ((int) direction * -1);
            }

            // set next target index
            index += (int) direction;
        }

        // Get target
        Vector2 target = worldNodes[index];
        // Get current platform position
        Vector2 position = body.position;
        // Calculate single-frame movement towards target
        Vector2 movementThisFrame = Vector2.MoveTowards(position, target, settings.Speed * Time.deltaTime);
        // Move platform by calculated movement
        body.MovePosition(movementThisFrame);
        // Calculate platform movement velocity (displacement / time delta)
        velocity = (movementThisFrame - position) / Time.deltaTime;
        MoveCaughtRigidbodies();
    }

    private void MoveCaughtRigidbodies()
    {
        foreach (Rigidbody2D caughtRigidbody in caughtRigidbodies)
        {
            // This keeps the player on the platform without jittering, but the player cant move
            //caughtRigidbody.MovePosition(caughtRigidbody.position + body.position - lastPosition);

            // This is spotty. It launches the player off the platform sometime when the platform changes direction
            // if (caughtRigidbody.velocity != velocity)
            // {
            //     caughtRigidbody.velocity += velocity;   
            // }
        }

        lastPosition = body.position;
    }
}
\$\endgroup\$
1
\$\begingroup\$

[EDIT: I have written an answer above which solves the question without friction. I keep this one however since it is still valid in many situations and the comments give some context to the solution.]


As you described it the player should follow along with the platform automatically - you are using physics and that should take care of it.

Ok, I am not an expert in 2D physics but I think it is the movement system of the player which causes the error. To be more precise, you move the player by setting a fixed velocity, which cancels out the friction from the platform.

Instead of setting it like this...

body.velocity = newVelocity

...I think you need to add/subtract from the current velocity, like:

body.AddForce(xxx)

Depending on how your movement system is implemented it shouldn't be too hard to convert. You could start by simply remove your movement system and check if the character now follows the platform automatically.

\$\endgroup\$
8
  • \$\begingroup\$ Unfortunately, that did not help. Even when the character is in idle state (velocity is not being touched in that state), the character slides off. This seems to be the default behavior of unity physics bodies. Even if I upped the friction that might help on keeping the player on the platform but it would affect his movement, and if the platform is going down the player would continually fall to catch up with the platform. \$\endgroup\$ – MrPlow Apr 6 at 7:13
  • \$\begingroup\$ That is strange. 1. Are you sure you are using the 2D versions of rigid bodies and colliders (Rigidbody2D, XXXCollider2D) on both objects. 2. The rigid body on the player is not kinematic. 3. The player velocity is never touched. \$\endgroup\$ – Anders Bjerin Apr 6 at 10:30
  • \$\begingroup\$ Note that all physical objects have friction in Unity unless you create a physical material without friction. The default friction is 0.4 (if I remember it correctly). \$\endgroup\$ – Anders Bjerin Apr 6 at 10:34
  • \$\begingroup\$ Try and create a simple sprite with a BoxCollider2D, a Rigidbody2D (non-kinematic and some gravity). Does it move with the platform? \$\endgroup\$ – Anders Bjerin Apr 6 at 10:39
  • \$\begingroup\$ Player is frictionless, but even if I gave him friction, the player would still be sliding on the platform during horizontal direction change and jumping when vertical direction changes. The idea is to make the player seemingly "stick" to the platform by moving him in tandem with the platform and that is where I am stuck. I have tried using AddForce instead of setting the velocity directly but I was still getting the same problem \$\endgroup\$ – MrPlow Apr 6 at 11:11
1
\$\begingroup\$

Here is a simple solution on how to implement moving platforms without parenting or using the default friction. Since the requirement was not to use any friction we have to create a physical material and set the friction to 0 and add it to the rigidbody of our character (player). For 2D it must be a RigidBody2D, and it must be Dynamic since we want forces to affect it.

There are hundreds of character controlers on the Internet, this is just a simple demonstration on how to solve this question. We start with some standard initialization.

using System;
using UnityEngine;

public class PlatformCharacter : MonoBehaviour
{
    /// <summary>
    /// We calculate the delta-movement between each fixed update and use it to
    /// move ourself. Physicla movements should normally only be executed in FixedUpdate.
    /// </summary>
    float horizontalMovement = 0;
    float verticalMovement = 0;

    public float moveSpeed = 20f;
    Rigidbody2D ourRigidBody;
    BoxCollider2D boxColliderPlayer;
    int layerMaskGround;
    float heightTestPlayer;

    /// <summary>
    /// The ground/platform we are currently on, or <c>null</c>.
    /// </summary>
    Collider2D currentGround = null;

    /// <summary>
    /// The position of the ground in the last FixedUpdate (after we have moved our character).
    /// </summary>
    Vector3 currentGroundPosition;

    void Start()
    {
        // Get a reference to our body.
        ourRigidBody = GetComponent<Rigidbody2D>();
        // We collect our collider so we can calculate our height.
        boxColliderPlayer = GetComponent<BoxCollider2D>();
        // We do the height test from the center of the player, so we should only check
        // halft the height of the player + some extra to ignore rounding off errors.
        heightTestPlayer = boxColliderPlayer.bounds.extents.y + 0.05f;
        // We are only interested to get colliders on the ground layer. If we would
        // like to jump ontop of enemies we should add their layer too (which then of
        // course can't be on the same layer as the player).
        layerMaskGround = LayerMask.GetMask("Ground");
    }

Note that we collect user input in the Update() function so we don't miss any user input. In this example, we simply check if the A or D keys are down, but once you start to check if a key was just pressed or released (or similar one-time events) it is important to do it in Update().

Note that we adjust the current horizontal movement, we don't set it to a fixed value.

private void Update()
{
    // This is a very very simple character movement, just to show the principles.
    if (Input.GetKey(KeyCode.A))
    {
        horizontalMovement = -moveSpeed * Time.deltaTime;
    }
    if (Input.GetKey(KeyCode.D))
    {
        horizontalMovement = moveSpeed * Time.deltaTime;
    }
}

Any physical movements should be done in the FixedUpdate() function.

We start by doing a ground check to see if we are on a platform/ground and if so, we adjust the horizontal movement accordingly to how the platform has moved since last time. If we are in the air, or have just landed, we don't do any adjustments.

void FixedUpdate()
{
    // Do a ground check to see if we are on a platform, and if so, has it moved since last update?
    RaycastHit2D hit = Physics2D.Raycast(boxColliderPlayer.bounds.center, Vector2.down, heightTestPlayer, layerMaskGround);
    bool isGrounded = hit.collider != null;
    // It is soo easy to make misstakes so do a lot of Debug.DrawRay calls when working with colliders...
    Debug.DrawRay(boxColliderPlayer.bounds.center, Vector2.down * heightTestPlayer, isGrounded ? Color.green : Color.red, 0.5f);

    // Hande the ground check.
    if (hit.collider != currentGround)
    {
        // We have landed on a new platform - or are in the air. 
        currentGround = hit.collider;
        if(hit.collider != null)
            currentGroundPosition = hit.collider.transform.position;
    }
    else
    {
        if(currentGround == null)
        {
            // We are still in the air (jumping or falling).
        }
        else
        {
            // We are still on the same ground. Has it moved?
            Vector2 groundMoved = hit.collider.transform.position - currentGroundPosition;
            currentGroundPosition = hit.collider.transform.position;
            // Adjust our character's desired position accordingly to how the ground has moved.
            horizontalMovement += groundMoved.x;
            verticalMovement += groundMoved.y;
        }
    }

    // We can now try to move our character (but still obeying colliders)
    ourRigidBody.MovePosition( new Vector2(ourRigidBody.position.x + horizontalMovement, ourRigidBody.position.y + verticalMovement) );

    // Reset the movement until next time.
    horizontalMovement = 0;
    verticalMovement = 0;
}

Note that we use the MovePosition() function to move the player. It will try to move the player to the desired position, hitting colliders/triggers as it should in a physical movement. If we hit a solid collider we might even be stopped - so exactly where the player ends up is up to the physic system.

Finally, we reset the horizontalMovement and are ready for the next loop.

\$\endgroup\$
1
  • \$\begingroup\$ Got an external question about gravity. Since we use MovePosition() there is of course no gravity, we are telling Unity where to place something and we can't expect Unity then to place it somewhere else. How correctly the gravity should be calculated is up the the game - can be as simple as Physics2D.gravity * ourRigidBody.gravityScale * Time.deltaTime. \$\endgroup\$ – Anders Bjerin Apr 10 at 7:01
0
\$\begingroup\$

After some fiddling, I have implemented a variation of my earlier attempt and Anders' answer. Unfortunately Anders' answer was incompatible with my project as I am not using MovePosition to move my character normally. I use velocity or AddForce.

First. Instead of detecting if the actor is on a platform using a trigger collider, I've decided to use the OnCollisionEnter2D event and check if the contact is on top by getting a dot product between the actor contact normal and the down vector. If the resulting dot product is 1 then the actor is standing on the platform.

    private void OnCollisionEnter2D(Collision2D other)
    {
        if (other.rigidbody.bodyType == RigidbodyType2D.Static)
        {
            return;
        }

        IMovingGroundAware actor = other.collider.GetComponent<IMovingGroundAware>();
        if (actor == null)
        {
            return;
        }
        
        int count = other.GetContacts(contactPoints);
        for (int i = 0; i < count; i++)
        {
            // Check contacts to see where the contact took place
            ContactPoint2D contactPoint2D = contactPoints[i];
            // Get the dot product
            float dot = Vector2.Dot(contactPoint2D.normal, Vector2.down);
            // If the contact is at the top of the platform the dot product should be ~1
            if (dot > 0.9f)
            {
                // Set the moving ground on the actor motor so that it may be moved along
                // with the platform
                actor.SetMovingGround(this);
            }
        }
    }

and OnCollisionExit2D i set the moving ground to null

    private void OnCollisionExit2D(Collision2D other)
    {
        IMovingGroundAware actor= other.collider.GetComponent<IMovingGroundAware>();

        actor?.SetMovingGround(null);
    }

Then, on the actor which implements the IMovingGroundAware interface I have a method which runs after all other movement and checks if the actor is in idle state. If the actor is in idle state (no movement input) then move the actor along the moving ground using body.MovePosition

    private void FixedUpdateMovingGroundProcessing()
    {
        if (movingGround == null)
        {
            return;
        }           

        // If the player is idle an is on moving ground
        if (movementStateMachine.CurrentState.Id == MovementStateId.Idle)
        {
            // Move him with the platform
            body.MovePosition(body.position + movingGround.Velocity * Time.deltaTime);
        }
    }

This is not perfect, but it works for my use case. If anyone has any ideas on how to improve this. Feel free to let me know.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.